
    |+jq             	         U d Z ddlmZ ddlZddlZddlZddlZddlZddlm	Z	 ddl
mZ ddlmZmZ ddlmZmZmZmZmZmZmZmZmZ ddlmZ dd	lmZmZ dd
lm Z  ddlm!Z"  ej#        e$          Z% e            Z&ddZ'ddZ(dddZ)g dZ*de+d<   dZ,ddddZ-dd!Z.dd$Z/dd'Z0dd*Z1d+Z2	 ddd1Z3dd5Z4dd9Z5e&6                    d:           edd;<           ed=           edd><           edd?<           edd@<          fddE            Z7e&6                    dF           ed           eddG<           eddH<          fddK            Z8 G dL dMe          Z9e&:                    dN           ed          fddP            Z;dQZ<ddSZ=e&6                    dT           ed          fddU            Z>e&:                    dT           edV           ed           ed          fddZ            Z?e&6                    d[           ed          fdd^            Z@e&A                    d[           ed          fdd_            ZB G d` dae          ZCe&D                    dF           ed          fddb            ZEe&A                    dF           ed          fddc            ZFddeZGddgZH G dh die          ZIe&:                    dj           ed          fddk            ZJ G dl dme          ZKe&:                    dn           ed          fddo            ZLe&A                    dn           edV           edV           ed          fddr            ZM G ds dte          ZNe&:                    du           ed          fddv            ZOe&6                    dw           edd><           eddx<          fddz            ZP	 ddlQZRn# eS$ r dZRY nw xY we&6                    d{           edd><          fdd|            ZTe&6                    d}           edd><          fdd            ZUe&6                    d           edd><          fdd            ZV G d de          ZWe&:                    d           edd><          fdd            ZX G d de          ZYe&:                    d           ed          fdd            ZZ G d de          Z[e&:                    d           ed          fdd            Z\ G d de          Z]e&:                    d           ed          fdd            Z^e&6                    d          d             Z_ddZ`ddZaddZbe&6                    d           ed           ed          fdd            Zce&:                    d           ed          fdd            Zde&A                    d           ed          fdd            Zee&6                    d           ed          fdd            Zfe&6                    d           ed          fdd            Zge&6                    d           eddd           ed          fd d            Zhe&:                    d           ed=           edd           ed          fdd            Zi G d de          Zj G d de          ZkddZle&6                    d           ed=          fdd            Zme&:                    d          dd            Zne&D                    d          dd            Zoe&A                    d           ed=d<          fdd            Zpe&:                    d          dd            ZqdZr G dĄ de          Zs G dƄ de          Zte&6                    dȦ          dɄ             Zue&D                    dʦ          dd̄            Zve&:                    dͦ          d	d΄            Zw G dτ de          Zxe&:                    dѦ           ed          fd
d҄            Zy G dӄ de          Zze&6                    dզ          dք             Z{e&|                    dզ          ddׄ            Z}e&~                    dئ          ddڄ            ZdS (  um  Kanban dashboard plugin — backend API routes.

Mounted at /api/plugins/kanban/ by the dashboard plugin system.

This layer is intentionally thin: every handler is a small wrapper around
``hermes_cli.kanban_db`` or a direct SQL query. Writes use the same code
paths the CLI and gateway ``/kanban`` command use, so the three surfaces
cannot drift.

Live updates arrive via the ``/events`` WebSocket, which tails the
append-only ``task_events`` table on a short poll interval (WAL mode lets
reads run alongside the dispatcher's IMMEDIATE write transactions).

Security note
-------------
Plugin HTTP routes go through the dashboard's session-token auth middleware
(``web_server.auth_middleware``) just like core API routes — every
``/api/plugins/...`` request must present the session bearer token (or the
session cookie set when you load the dashboard HTML). The token is the
random per-process ``_SESSION_TOKEN`` printed at startup; the dashboard's
own pages inject it via ``window.__HERMES_SESSION_TOKEN__`` so logged-in
browsers don't have to handle it manually.

For the ``/events`` WebSocket we still require the session token as a
``?token=`` query parameter (browsers cannot set the ``Authorization``
header on an upgrade request), matching the established pattern used by
the in-browser PTY bridge in ``hermes_cli/web_server.py``.

This means ``hermes dashboard --host 0.0.0.0`` is safe to run on a LAN:
plugin routes are no longer an unauthenticated exception. The auth still
isn't multi-user — anyone who can read the printed URL+token gets full
dashboard access — but they can't ride along just because they can reach
the port.
    )annotationsN)asdictPath)AnyOptional)		APIRouterFileFormHTTPExceptionQuery
UploadFile	WebSocketWebSocketDisconnectstatus)FileResponse)	BaseModelField)	kanban_dbkanban_diagnosticsws'WebSocket'returnboolc                x    	 ddl m} n# t          $ r Y dS w xY wt          |                    |                     S )aK  Authorize a WebSocket upgrade by delegating to the dashboard's canonical
    WS auth gate (``hermes_cli.web_server._ws_auth_ok``).

    Delegating (rather than re-implementing a ``_SESSION_TOKEN``-only check)
    means this endpoint transparently accepts whatever the core gate accepts
    in each mode:

      * loopback / ``--insecure``: legacy ``?token=<_SESSION_TOKEN>``
      * gated OAuth: single-use ``?ticket=`` (the browser SDK's
        ``buildWsUrl`` mints one per connect)
      * server-internal: the process-lifetime ``?internal=`` credential

    The previous bespoke check only understood ``_SESSION_TOKEN``, so the
    kanban live-events WS was rejected on every OAuth-gated deployment even
    though the rest of the dashboard worked. Routing through the shared gate
    also means this can never drift from core auth again.

    Imported lazily so the plugin still loads in test contexts where the
    dashboard ``web_server`` module isn't importable (e.g. the bare-FastAPI
    test harness); there we accept so the tail loop stays testable, matching
    the prior behaviour.
    r   )
web_serverT)
hermes_clir   	Exceptionr   _ws_auth_ok)r   _wss     B/usr/local/lib/hermes-agent/plugins/kanban/dashboard/plugin_api.py_ws_upgrade_authorizedr#   @   s\    .0000000    tt	
 ##$$$s   	 
boardOptional[str]c                   | | dk    rdS 	 t          j        |           }n0# t          $ r#}t          dt	          |                    d}~ww xY w|r9|t           j        k    r)t          j        |          st          dd|d          |S )aI  Validate and normalise a board slug from a query param.

    Raises :class:`HTTPException` 400 on malformed slugs so the browser
    sees a clean error instead of a 500. Returns the normalised slug,
    or ``None`` when the caller omitted the param (which then falls
    through to the active board inside ``kb.connect()``).
    N   status_codedetail  board  does not exist)r   _normalize_board_slug
ValueErrorr   strDEFAULT_BOARDboard_exists)r$   normedexcs      r"   _resolve_boardr6   a   s     }t>077 > > >CHH====> 
&I333I<RSY<Z<Z35F555
 
 
 	
 Ms   ! 
AA		Ac                    	 t          j        |            n2# t          $ r%}t                              d|           Y d}~nd}~ww xY wt          j        |           S )u  Open a kanban_db connection, creating the schema on first use.

    Every handler that mutates the DB goes through this so the plugin
    self-heals on a fresh install (no user-visible "no such table"
    error if somebody hits POST /tasks before GET /board).
    ``init_db`` is idempotent.

    ``board`` is the query-param slug (already normalised by
    :func:`_resolve_board`). When ``None`` the active board is used
    via the resolution chain (env var → ``current`` file → ``default``).
    r$   zkanban init_db failed: %sN)r   init_dbr   logwarningconnect)r$   r5   s     r"   _connr=   w   su    6&&&&& 6 6 6/5555555565))))s    
AAA)triagetodo	scheduledreadyrunningblockedreviewdone	list[str]BOARD_COLUMNS   latest_summarytaskkanban_db.TaskrJ   dict[str, Any]c                   t          |           }	 t          j        |           |d<   n# t          $ r d d d d|d<   Y nw xY w||d<   |S )Nage)created_age_secondsstarted_age_secondstime_to_complete_secondsrJ   )r   r   task_ager   )rK   rJ   ds      r"   
_task_dictrU      st    
 	tAp%d++% p p p+/jnoo%p )AHs   ) ??eventkanban_db.Eventc                P    | j         | j        | j        | j        | j        | j        dS )Nidtask_idkindpayload
created_atrun_idrY   )rV   s    r"   _event_dictr`      s0    h=
=&,      ckanban_db.Commentc                D    | j         | j        | j        | j        | j        dS )NrZ   r[   authorbodyr^   re   )rb   s    r"   _comment_dictrh      s*    d9(l  ra   akanban_db.Attachmentc           	     h    | j         | j        | j        | j        | j        | j        | j        | j        dS )zSerialise an Attachment for the drawer. ``stored_path`` is the
    absolute on-disk path workers read; the UI uses ``id`` for download.rZ   r[   filenamecontent_typesizeuploaded_bystored_pathr^   rl   )ri   s    r"   _attachment_dictrr      s;     d9J}}l	 	 	ra   rkanban_db.Runc                   i d| j         d| j        d| j        d| j        d| j        d| j        d| j        d| j        d	| j        d
| j	        d| j
        d| j        d| j        d| j        d| j        d| j        S )z5Serialise a Run for the drawer's Run history section.rZ   r[   profilestep_keyr   
claim_lockclaim_expires
worker_pidmax_runtime_secondslast_heartbeat_at
started_atended_atoutcomesummarymetadataerror)rZ   r[   rv   rw   r   rx   ry   rz   r{   r|   r}   r~   r   r   r   r   rs   s    r"   	_run_dictr      s    ad19 	19 	AJ	
 	!( 	al 	 	al 	q4 	Q0 	al 	AJ 	19 	19 	AJ  	! ra   ) completion_blocked_hallucination!suspected_hallucinated_referencesconnsqlite3.Connectiontask_idsOptional[list[str]]dict[str, list[dict]]c           	     h   ddl m} ddlm}  |j         |                      }|d|si S d                    dgt          |          z            }|                     d| dt          |                    	                                }n'|                     d	          	                                }|si S d
 |D             }d                    dgt          |          z            }d |D             }|                     d| dt          |                    	                                D ]1}	|
                    |	d         g                               |	           2d |D             }
|                     d| dt          |                    	                                D ]1}|

                    |d         g                               |           2i }|D ]W}|d         } |j        ||                    |g           |
                    |g           |          }|rd |D             ||<   X|S )u  Run the diagnostic rule engine against every task (or a subset)
    and return ``{task_id: [diagnostic_dict, ...]}``.

    Tasks with no active diagnostics are omitted from the result.
    Uses ``hermes_cli.kanban_diagnostics`` — see that module for the
    rule definitions.
    r   r   load_configN,?z!SELECT * FROM tasks WHERE id IN ()z.SELECT * FROM tasks WHERE status != 'archived'c                    g | ]
}|d          S rZ    .0rs   s     r"   
<listcomp>z-_compute_task_diagnostics.<locals>.<listcomp>  s    %%%1qw%%%ra   c                    i | ]}|g S r   r   r   tids     r"   
<dictcomp>z-_compute_task_diagnostics.<locals>.<dictcomp>!  s    &B&B&B3sB&B&B&Bra   z,SELECT * FROM task_events WHERE task_id IN (z) ORDER BY idr[   c                    i | ]}|g S r   r   r   s     r"   r   z-_compute_task_diagnostics.<locals>.<dictcomp>'  s    $@$@$@S"$@$@$@ra   z*SELECT * FROM task_runs WHERE task_id IN (rZ   )configc                6    g | ]}|                                 S r   )to_dictr   rT   s     r"   r   z-_compute_task_diagnostics.<locals>.<listcomp>8  s     333		333ra   )r   r   hermes_cli.configr   config_from_runtime_configjoinlenexecutetuplefetchall
setdefaultappendcompute_task_diagnosticsget)r   r   kdr   diag_configplaceholdersrowsrow_idsevents_by_taskev_rowruns_by_taskrun_rowoutrs   r   diagss                   r"   _compute_task_diagnosticsr      s    433333------/"/>>K
  	IxxH 566||????(OO
 
 (** 	
 ||<
 

(** 	  	 &%%%%G88SECLL011L&B&B'&B&B&BN,,R|RRRg  hjjH H 	!!&"3R88??GGGG$@$@$@$@$@L<<P\PPPg  hjjH H 		 2B77>>wGGGG!#C 	4 	4g++sB''S"%%	
 
 
  	433U333CHJra   diagnostics
list[dict]Optional[dict]c                   | sdS ddl m} i }d}d}d}d}| D ]}|                    |d         d          |                    dd          z   ||d         <   ||                    dd          z  }|                    d          pd}||k    r|}|                    d	          }	|	|v r|                    |	          }
|
|k    r|
}|	}||||d
S )u(  Compact summary for cards: {count, highest_severity, kinds,
    latest_at}. Replaces the old hallucination-only ``warnings`` object
    — same shape additions plus ``highest_severity`` so the UI can color
    badges per diagnostic severity.

    Returns None when ``diagnostics`` is empty.
    Nr   SEVERITY_ORDERr\   count   last_seen_atseverity)r   kinds	latest_athighest_severity)hermes_cli.kanban_diagnosticsr   r   index)r   r   r   latesthighest_idxhighest_sevr   rT   lasevidxs              r"   "_warnings_summary_from_diagnosticsr   <  s     t<<<<<<EFK!%KE " " 99QvY22QUU7A5F5FFaiw"""UU>""'a;;FeeJ.   &&s++C[  !!'	  ra   r[   r1   dict[str, list[str]]c                    d |                      d|f          D             }d |                      d|f          D             }||dS )z8Return {'parents': [...], 'children': [...]} for a task.c                    g | ]
}|d          S )	parent_idr   r   s     r"   r   z_links_for.<locals>.<listcomp>e  s,        	
+  ra   zFSELECT parent_id FROM task_links WHERE child_id = ? ORDER BY parent_idc                    g | ]
}|d          S )child_idr   r   s     r"   r   z_links_for.<locals>.<listcomp>l  s,        	
*  ra   ESELECT child_id FROM task_links WHERE parent_id = ? ORDER BY child_idparentschildren)r   )r   r[   r   r   s       r"   
_links_forr   c  sz     TJ
 
  G SJ
 
  H H555ra   z/boardzFilter to a single tenant)descriptionFz$Kanban board slug (omit for current)z1Restrict to tasks using this workflow template idz+Restrict to tasks at this workflow step keytenantinclude_archivedworkflow_template_idcurrent_step_keyc                   t          |          }t          |          }	 t          j        || |||          }i }|                    d                                          D ]\}|                    |d         ddd          dxx         dz  cc<   |                    |d	         ddd          d
xx         dz  cc<   ]d |                    d          D             }	i }
|                    d                                          D ]M}|
                    |d         ddd          }|dxx         dz  cc<   |d         dk    r|dxx         dz  cc<   Nt          |d          }|                    d                                          d         }d t          D             |rg d<   t          j
        |d |D                       }|D ]}|                    |j                  }|r|dt                   nd}t          ||          }|                    |j        ddd          |d<   |	                    |j        d          |d<   |
                    |j                  |d<   |                    |j                  }|r||d<   t          |          |d<   |j        v r|j        nd }|                             |           d! |                    d"          D             }d# |                    d$          D             }fd%                                D             ||t'          |          t'          t)          j                              d&|                                 S # |                                 w xY w)'u{  Return the full board grouped by status column.

    ``_conn()`` auto-initializes ``kanban.db`` on first call so a fresh
    install doesn't surface a "failed to load" error on the plugin tab.

    ``board`` selects which board to read from. Omitting it falls
    through to the active board (``HERMES_KANBAN_BOARD`` env → on-disk
    ``current`` pointer → ``default``).
    r8   )r   r   r   r   z*SELECT parent_id, child_id FROM task_linksr   r   r   r   r   r   r   c                ,    i | ]}|d          |d         S )r[   nr   r   s     r"   r   zget_board.<locals>.<dictcomp>  s2     *
 *
 *
 iL!C&*
 *
 *
ra   zASELECT task_id, COUNT(*) AS n FROM task_comments GROUP BY task_idzbSELECT l.parent_id AS pid, t.status AS cstatus FROM task_links l JOIN tasks t ON t.id = l.child_idpid)rE   totalr   cstatusrE   Nr   z1SELECT COALESCE(MAX(id), 0) AS m FROM task_eventsmc                    i | ]}|g S r   r   r   rb   s     r"   r   zget_board.<locals>.<dictcomp>  s    )G)G)GA!R)G)G)Gra   archivedc                    g | ]	}|j         
S r   r   )r   ts     r"   r   zget_board.<locals>.<listcomp>  s    7L7L7L7L7L7Lra   rI   link_countscomment_countprogressr   warningsr?   c                    g | ]
}|d          S )r   r   r   s     r"   r   zget_board.<locals>.<listcomp>  s,     
 
 
 hK
 
 
ra   zJSELECT DISTINCT tenant FROM tasks WHERE tenant IS NOT NULL ORDER BY tenantc                    g | ]
}|d          S )assigneer   r   s     r"   r   zget_board.<locals>.<listcomp>  s,     
 
 
 jM
 
 
ra   ziSELECT DISTINCT assignee FROM tasks WHERE assignee IS NOT NULL AND status != 'archived' ORDER BY assigneec                &    g | ]}||         d S ))nametasksr   )r   r   columnss     r"   r   zget_board.<locals>.<listcomp>  s2       ;?66  ra   )r   tenants	assigneeslatest_event_idnow)r6   r=   r   
list_tasksr   r   r   r   fetchonerG   latest_summariesr   rZ   _CARD_SUMMARY_PREVIEW_CHARSrU   r   r   r   keysinttimeclose)r   r   r$   r   r   r   r   r   rowcomment_countsr   pdiagnostics_per_taskr   summary_mapr   fullpreviewrT   r   colr   r   r   s                          @r"   	get_boardr  z  sV   * 5!!EuDm$-!5-
 
 
 24<<8
 

(**	 	C ""3{#3PQ5R5RSS      ""3z?q4Q4QRR      
*
 *
\\S *
 *
 *
 /1<<B
 
 (**	 	C ##CJQ0G0GHHAgJJJ!OJJJ9~''&			Q			  9MMM,,?
 

(**S *H)G)G)G)G 	%"$GJ  07L7Le7L7L7LMM 	# 	#A??14((D6:D11122  1W555A*qtPQ5R5RSSAm!/!3!3AD!!<!<Ao$LL..AjM(,,QT22E J $)-  B5 I I*h'11!((vCCL""""
 
\\\ 
 
 

 
\\= 
 
 
	   CJ<<>>   ""?33ty{{##
 
 	



s   L7M. .Nz/tasks/{task_id}z@With run_state_name: filter runs by column 'status' or 'outcome'z4With run_state_type: exact value for that run columnrun_state_typerun_state_namec                B   t          |          }t          |          }	 |d u |d u z  rt          dd          ||dvrt          dd          t          j        ||           }|t          dd|  d	          t          j        ||           }t          ||
          }t          || g          }|                    |           pg }	|	r|	|d<   t          |	          |d<   |d t          j
        ||           D             d t          j        ||           D             d t          j        ||           D             t          ||           d t          j        || ||          D             d|                                 S # |                                 w xY w)Nr8   r(   zDrun_state_type and run_state_name must be passed together or omittedr)   )r   r   z,run_state_type must be 'status' or 'outcome'r,   task 
 not foundrI   r   r   r   c                ,    g | ]}t          |          S r   )rh   r   s     r"   r   zget_task.<locals>.<listcomp>.  s     ZZZaq))ZZZra   c                ,    g | ]}t          |          S r   )r`   )r   es     r"   r   zget_task.<locals>.<listcomp>/  s    TTT!{1~~TTTra   c                ,    g | ]}t          |          S r   rr   r   ri   s     r"   r   zget_task.<locals>.<listcomp>0  s!    cccA,Q//cccra   c                ,    g | ]}t          |          S r   )r   r   s     r"   r   zget_task.<locals>.<listcomp>2  s.        !  ra   )
state_type
state_name)rK   commentseventsattachmentslinksruns)r6   r=   r   r   get_taskrJ   rU   r   r   r   list_commentslist_eventslist_attachmentsr   	list_runsr  )
r[   r$   r  r  r   rK   full_summarytask_dr   	diag_lists
             r"   r  r    s    5!!EuD+d"~'=> 	]    %.@U*U*UE    !$00<C8S8S8S8STTTT !/g>>D>>> *$'CCCIIg&&,"	 	O$-F=!!CI!N!NF:ZZ93J4QX3Y3YZZZTTy/DT7/S/STTTcc9STXZa9b9bcccg.. ",--	    
 
" 	



s   EF Fc                      e Zd ZU ded<   dZded<   dZded<   dZded<   dZd	ed
<   dZded<   dZ	ded<    e
e          Zded<   dZded<   dZded<   dZded<   dZded<   dZded<   dZded<   dS )CreateTaskBodyr1   titleNr%   rg   r   r   r   r   priorityscratchworkspace_kindworkspace_path)default_factoryrF   r   Fr   r>   idempotency_keyOptional[int]r{   r   skills	goal_modegoal_max_turns)__name__
__module____qualname____annotations__rg   r   r   r*  r,  r-  r   listr   r>   r/  r{   r1  r2  r3  r   ra   r"   r(  r(  D  s        JJJD"H"""" F    H#N####$(N((((t444G4444F%)O)))))-----"&F&&&&I$(N((((((ra   r(  z/tasksr]   c                   t          |          }t          |          }	 t          j        |f| j        | j        | j        d| j        | j        | j	        | j
        | j        | j        | j        | j        | j        | j        | j        d}t          j        ||          }d|rt'          |          nd i}|r@|j        dk    r5|j        r.	 ddlm}  |            \  }}|s|r||d<   n# t.          $ r Y nw xY w||                                 S # t2          $ r#}	t5          d	t7          |	          
          d }	~	ww xY w# |                                 w xY w)Nr8   	dashboard)r)  rg   r   
created_byr,  r-  r   r*  r   r>   r/  r{   r1  r2  r3  rK   rA   r   )_check_dispatcher_presencer;   r(   r)   )r6   r=   r   create_taskr)  rg   r   r,  r-  r   r*  r   r>   r/  r{   r1  r2  r3  r  rU   r   hermes_cli.kanbanr<  r   r  r0   r   r1   )
r]   r$   r   r[   rK   rg   r<  rB   messager  s
             r"   r=  r=  U  s   5!!EuD''
-%""1"1>%O>#3 ' ;>'"1!
 
 
$ !$00 &D(J
4(8(8(8dK  	DK7**t}*HHHHHH#=#=#?#?  .7 .&-DO     	

  < < <CFF;;;;< 	

sH   B$D	 C# "D	 #
C0-D	 /C00D	 	
D6D11D66D9 9Ei  rawc                r   | pd                     dd                              d          d                                         }d                    d |D                                                       }|                    d                                          }|st          dd	          |d
d         S )av  Reduce a client-supplied filename to a safe basename.

    Strips any directory components (``os.path.basename`` on both
    separators) so a malicious ``../../etc/passwd`` or ``C:\x`` collapses
    to its leaf. Rejects empty / dotfile-only names. The result is only
    ever joined under the per-task attachments dir, never used verbatim
    as a path from the client.
    r'   \/r   c              3  J   K   | ]}|                                 |d v|V  dS ) N)isprintable)r   chs     r"   	<genexpr>z(_safe_attachment_name.<locals>.<genexpr>  s;      NN"(8(8NRv=M=M2=M=M=M=MNNra   .r(   zinvalid attachment filenamer)   NrH   )replacesplitstripr   lstripr   )r@  r   s     r"   _safe_attachment_namerN    s     I2tS))//44R8>>@@D 77NNNNNNNTTVVD;;s!!##D S4QRRRR:ra   z/tasks/{task_id}/attachmentsc                2   t          |          }t          |          }	 t          j        ||           t	          dd|  d          dd t          j        ||           D             i|                                 S # |                                 w xY w)Nr8   r,   r  r  r)   r  c                ,    g | ]}t          |          S r   r  r  s     r"   r   z)list_task_attachments.<locals>.<listcomp>  s.       () ##  ra   )r6   r=   r   r  r   r"  r  )r[   r$   r   s      r"   list_task_attachmentsrQ    s    5!!EuD	dG,,4C8S8S8S8STTTT  -6-Gg-V-V  
 	



s   A
B   B.filer   rp   c           	     ^  K   t          |          }t          |          }	 t          j        ||           t	          dd|  d          t          |j        pd          }t          j        | |          }|                    dd	           |	                    d
          \  }}}	|}
d}||
z  
                                r(| d| d| |	 }
|dz  }||
z  
                                (||
z  }d}	 t          |d          5 }	 |                    d           d{V }|snz|t          |          z  }|t          k    rG|                                 |                    d           t	          ddt          dz   d          |                    |           	 ddd           n# 1 swxY w Y   n0# t          $ r  t$          $ r}t	          dd|           d}~ww xY wt          j        || |
t)          |                                          |j        ||pd          }t          j        ||          }d|rt1          |          ndi|                                 S # t2          $ r#}t	          dt)          |                    d}~ww xY w# |                                 w xY w)a  Store an uploaded file for a task and record its metadata.

    The blob lands under ``attachments_root(board)/<task_id>/`` with a
    sanitised, collision-resolved name. The worker reads it via the
    absolute path surfaced in ``build_worker_context``.
    r8   Nr,   r  r  r)   r'   T)r   exist_okrI  r    (r   r   wbi   )
missing_oki  zattachment exceeds z	 MB limit  zfailed to store attachment: r:  )rm   rq   rn   ro   rp   
attachmentr(   )r6   r=   r   r  r   rN  rm   task_attachments_dirmkdir	partitionexistsopenreadr   _MAX_ATTACHMENT_BYTESr  unlinkwriteOSErroradd_attachmentr1   resolvern   get_attachmentrr   r0   )r[   rR  r$   rp   r   	safe_namedest_dirstemdotext	candidater   	dest_pathr   r   chunkr5   att_idattr  s                       r"   upload_task_attachmentrq    s      5!!EuD9dG,,4C8S8S8S8STTTT)$-*=2>>	 1'GGGtd333 #,,S11c3	)#++-- 	11111s1C11IFA )#++-- 	 y(		^i&& %#%"&))K"8"8888888E  SZZ'E444		!((D(999+(+ g6KP[6\ g g g    IIe$$$% 	% % % % % % % % % % % % % % %   	 	 	 	^ 	^ 	^C8\WZ8\8\]]]]	^ )I--//00*$3
 
 
 &tV44sD.s333E 	

  < < <CFF;;;;< 	

sn   CI& 4F7 BF+F7 +F//F7 2F/3F7 6I& 7G$GG$$A-I& &
J0JJJ J,z/attachments/{attachment_id}attachment_idr   c                   t          |          }t          |          }	 t          j        ||           }|t	          dd          t          j        |                                          }	 t          |j                                                  }|	                    |           n&# t          t          f$ r t	          dd          w xY w|                                st	          dd          t          t          |          |j        |j        pd          |                                 S # |                                 w xY w)	Nr8   r,   attachment not foundr)   zattachment file unavailablezattachment file missing on diskzapplication/octet-stream)pathrm   
media_type)r6   r=   r   rf  r   attachments_rootre  r   rq   relative_tor0   rc  is_filer   r1   rm   rn   r  )rr  r$   r   rp  rootstoreds         r"   download_attachmentr|    sV   5!!EuD&t];;;C8NOOOO )666>>@@	W#/**2244Ft$$$$G$ 	W 	W 	WC8UVVVV	W~~ 	[C8YZZZZV\'E+E
 
 
 	



s%   AD7 1;B- ,D7 -#CAD7 7Ec                    t          |          }t          |          }	 t          j        ||           }|t	          dd          d| d|                                 S # |                                 w xY w)Nr8   r,   rt  r)   T)okrZ   )r6   r=   r   delete_attachmentr   r  )rr  r$   r   rp  s       r"   remove_attachmentr    s{    5!!EuD)$>>;C8NOOOO-00



s   ,A" "A8c                      e Zd ZU dZded<   dZded<   dZded<   dZded<   dZded<   dZ	ded	<   dZ
ded
<   dZded<   dZded<   dS )UpdateTaskBodyNr%   r   r   r0  r*  r)  rg   resultblock_reasonr   r   r   )r4  r5  r6  r   r7  r   r*  r)  rg   r  r  r   r   r   ra   r"   r  r  &  s          F    "H"""""H""""ED F    "&L&&&& "G!!!!#H######ra   r  c                r	   t          |          }t          |          }	 t          j        ||           }|t	          dd|  d          |j        b	 t          j        || |j        pd           }n0# t          $ r#}t	          dt          |                    d }~ww xY w|st	          dd          |j	        |j	        }d}|d	k    r)t          j
        || |j        |j        |j        
          }n|dk    rt          j        || |j                  }n|dk    rt          j        || |j                  }n|dk    rHt          j        ||           }|r|j	        dv rt          j        ||           }not%          || d          }n]|dk    rt          j        ||           }nA|dk    rt	          dd          |dv rt%          || |          }nt	          dd|           |s`|dk    rEt)          ||           }	|	r3d                    d |	D                       }
t	          dd|
           t	          dd|d          |j        t          j        |          5  |                    dt3          |j                  | f           |                    d| t5          j        dt3          |j                  i          t3          t9          j                              f           d d d            n# 1 swxY w Y   |j        |j        Pt          j        |          5  g g }}|j        k|j                                        st	          dd          |                     d           |                     |j                                                   |j        /|                     d            |                     |j                   |                     |            |                    d!d                    |           d"|           |                    d#| t3          t9          j                              f           d d d            n# 1 swxY w Y   t          j        ||           }d$|rtC          |          nd i|"                                 S # |"                                 w xY w)%Nr8   r,   r  r  r)     ztask not foundTrE   r  r   r   rC   reasonr@   rA   rC   r@   r   rB   r(   FCannot set status to 'running' directly; use the dispatcher/claim path)r?   r>   r@   zunknown status: z, c              3  P   K   | ]!}|d          d|d          d|d          dV  "dS )r)  rU  rZ   z	, status=r   r   Nr   r   r  s     r"   rH  zupdate_task.<locals>.<genexpr>r  s[       * * !  !zOOqwOO8OOO* * * * * *ra   u:   Cannot move to 'ready': blocked by parent(s) not done — zstatus transition to z not valid from current state*UPDATE tasks SET priority = ? WHERE id = ?^INSERT INTO task_events (task_id, kind, payload, created_at) VALUES (?, 'reprioritized', ?, ?)r*  ztitle cannot be emptyz	title = ?zbody = ?zUPDATE tasks SET z WHERE id = ?zZINSERT INTO task_events (task_id, kind, payload, created_at) VALUES (?, 'edited', NULL, ?)rK   )#r6   r=   r   r  r   r   assign_taskRuntimeErrorr1   r   complete_taskr  r   r   
block_taskr  schedule_taskunblock_task_set_status_directarchive_task_parents_blocking_readyr   r*  	write_txnr   r   jsondumpsr  r)  rg   rL  r   rU   r  )r[   r]   r$   r   rK   r~  r  scurrentblockersnamessetsvalsupdateds                 r"   update_taskr  5  s   5!!EuDp!$00<C8S8S8S8STTTT 'D*'7#3#;t    D D D#CFFCCCCD N#<LMMMM >%ABF{{,'">#O$-	   i)$@TUUUk!!,T77CWXXXg#,T7;; Dw~1III"/g>>BB ,D'7CCBBj+D'::i# #c    555'gq99#<Rq<R<RSSSS  <<6tWEEH  $		 * *%-* * * ! ! ,(+!805!8 !8    $ #U1UUU    '$T** 
 
@)**G4   8dj*c':J6K6K)LMM%%'  
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 =$(@$T**  d=,"=..00 ]+D[\\\\KK,,,KK 3 3 5 5666<+KK
+++KK---G$$$F		$FFF   4c$)++../                ( $T733w@
7+++DA



sz   3R  A3 2R  3
B =BB  GR  $BK8,R  8K<<R  ?K< &R  &D%QR  QR  Q,R   R6c                    t          |          }t          |          }	 t          j        ||           }|st	          dd|  d          d| d|                                 S # |                                 w xY w)Nr8   r,   r  r  r)   T)deletedr[   )r6   r=   r   delete_taskr   r  )r[   r$   r   r~  s       r"   r  r    s    5!!EuD"411 	UC8S8S8S8STTTTG44



s   0A& &A<r8  c                l    |                      d|f                                          }d |D             S )a  Return parent rows (``id``, ``title``, ``status``) that aren't ``done``
    and therefore prevent ``task_id`` from being promoted to ``ready``.

    Used to enrich the 409 response from :func:`update_task` so the
    dashboard can show an actionable toast (#26744) instead of a silent
    no-op.  Returns ``[]`` when nothing blocks the transition (e.g. no
    parents, or all parents already done).
    zSELECT t.id, t.title, t.status FROM tasks t JOIN task_links l ON l.parent_id = t.id WHERE l.child_id = ? AND t.status != 'done'c                >    g | ]}|d          |d         |d         dS )rZ   r)  r   )rZ   r)  r   r   r   s     r"   r   z+_parents_blocking_ready.<locals>.<listcomp>  s>        w7q{CC  ra   )r   r   )r   r[   r   s      r"   r  r    sR     <<	6 

	 
 hjj 	    ra   
new_statusc                   t          j        |           5  |                     d|f                                          }|	 ddd           dS |dk    rR|                     d|f                                          }|r't          d |D                       s	 ddd           dS |d         dk    }|d         d	v o|d	v}|                     d
|||||f          }|j        dk    r	 ddd           dS d}|r+|dk    r%|d         rt          j        | |ddd| d          }|                     d||t          j	        d|i          t          t          j                              f           |r|                     d|f                                          D ]y}	|	d         }
|                     d|
f          }|j        dk    rM|                     d|
t          j	        dd|d          t          t          j                              f           zddd           n# 1 swxY w Y   |dv rt          j        |            dS )a   Direct status write for drag-drop moves that aren't covered by the
    structured complete/block/unblock/archive verbs (e.g. todo<->ready,
    running<->ready). Appends a ``status`` event row for the live feed.

    When this transitions OFF ``running`` to anything other than the
    terminal verbs above (which own their own run closing), we close the
    active run with outcome='reclaimed' so attempt history isn't
    orphaned. ``running -> ready`` via drag-drop is the common case
    (user yanking a stuck worker back to the queue).
    z5SELECT status, current_run_id FROM tasks WHERE id = ?NFrA   zYSELECT t.status FROM tasks t JOIN task_links l ON l.parent_id = t.id WHERE l.child_id = ?c              3  .   K   | ]}|d          dk    V  dS )r   rE   Nr   r  s     r"   rH  z%_set_status_direct.<locals>.<genexpr>  s<       + +*+(v%+ + + + + +ra   r   rB   >   rE   r   a   UPDATE tasks SET status = ?,   claim_lock = CASE WHEN ? = 'running' THEN claim_lock ELSE NULL END,   claim_expires = CASE WHEN ? = 'running' THEN claim_expires ELSE NULL END,   worker_pid = CASE WHEN ? = 'running' THEN worker_pid ELSE NULL END WHERE id = ?r   current_run_id	reclaimedzstatus changed to z (dashboard/direct))r   r   r   zbINSERT INTO task_events (task_id, run_id, kind, payload, created_at) VALUES (?, ?, 'status', ?, ?)r   r   zBUPDATE tasks SET status = 'todo' WHERE id = ? AND status = 'ready'zWINSERT INTO task_events (task_id, kind, payload, created_at) VALUES (?, 'status', ?, ?)r?   parent_reopened)r   r  parent>   rE   rA   T)r   r  r   r   r   allrowcount_end_runr  r  r   r  recompute_ready)r   r[   r  prevparent_statuseswas_runningreopening_satisfied_parentcurr_   r  r   demoteds               r"   r  r    s    
	T	"	" R R||CJ
 
 (** 	 <R R R R R R R R   "ll' 
	 
 hjj   s + +/>+ + + ( (  -R R R R R R R R0 8n	1N22 7"66 	#
 ll
 ZWE
 
 <1OR R R R R R R RP  	:22t<L7M2'g#KLZLLL  F
 	,fdj(J)?@@#dikkBRBRS	
 	
 	

 & 	
 ||W
  hjj  z?,,8K 
 #q((LL5 % J.4.?.5!" !"   	,,
  IR R R R R R R R R R R R R R Rh &&&!$'''4s&   -H!AH!'AH!5D H!!H%(H%c                  (    e Zd ZU ded<   dZded<   dS )CommentBodyr1   rg   r:  r%   rf   N)r4  r5  r6  r7  rf   r   ra   r"   r  r  >  s,         III'F''''''ra   r  z/tasks/{task_id}/commentsc                   |j                                         st          dd          t          |          }t	          |          }	 t          j        ||           t          dd|  d          t          j        || |j        pd|j         	           d
di|	                                 S # |	                                 w xY w)Nr(   zbody is requiredr)   r8   r,   r  r  r:  )rf   rg   r~  T)
rg   rL  r   r6   r=   r   r  add_commentrf   r  )r[   r]   r$   r   s       r"   r  r  C  s    < H4FGGGG5!!EuDdG,,4C8S8S8S8STTTT''."?Kgl	
 	
 	
 	
 d|



s   AB1 1Cc                  $    e Zd ZU ded<   ded<   dS )LinkBodyr1   r   r   N)r4  r5  r6  r7  r   ra   r"   r  r  X  s"         NNNMMMMMra   r  z/linksc                D   t          |          }t          |          }	 t          j        || j        | j                   ddi|                                 S # t          $ r#}t          dt          |                    d }~ww xY w# |                                 w xY w)Nr8   r~  Tr(   r)   )
r6   r=   r   
link_tasksr   r   r  r0   r   r1   )r]   r$   r   r  s       r"   add_linkr  ]  s    5!!EuDT7#4g6FGGGd| 	

  < < <CFF;;;;< 	

s#   #A 
B#BBB	 	Br   r   c                    t          |          }t          |          }	 t          j        || |          }dt	          |          i|                                 S # |                                 w xY w)Nr8   r~  )r6   r=   r   unlink_tasksr   r  )r   r   r$   r   r~  s        r"   delete_linkr  j  si     5!!EuD#D)X>>d2hh



s   &A A2c                      e Zd ZU ded<   dZded<   dZded<   dZded<   d	Zd
ed<   dZded<   dZ	ded<   dZ
ded<   d	Zd
ed<   dS )BulkTaskBodyrF   idsNr%   r   r   r0  r*  Fr   archiver  r   r   r   reclaim_first)r4  r5  r6  r7  r   r   r*  r  r  r   r   r  r   ra   r"   r  r  }  s         NNN F    "H"""""H""""G F    !G!!!!#H####Mra   r  z/tasks/bulkc                   d | j         pg D             }|st          dd          g }t          |          }t          |          }	 |D ]}|dd}	 t	          j        ||          }|-|                    d	d
           |                    |           M| j        r,t	          j	        ||          s|                    d	d           | j
        ]| j        sU| j
        }|dk    r*t	          j        ||| j        | j        | j                  }	n|dk    rt	          j        ||          }	n|dk    rHt	          j        ||          }
|
r|
j
        dv rt	          j        ||          }	nt#          ||d          }	n|dk    r.|                    d	d           |                    |           e|dk    rt	          j        ||          }	nG|dv rt#          |||          }	n1|                    d	d|           |                    |           |	s|                    d	d|d           | j        	 | j        r t	          j        ||| j        pdd          }	nt	          j        ||| j        pd          }	|	s|                    d	d           n;# t.          $ r.}|                    d	t1          |                     Y d}~nd}~ww xY w| j        t	          j        |          5  |                    dt9          | j                  |f           |                    d|t;          j        dt9          | j                  i          t9          t?          j                              f           ddd           n# 1 swxY w Y   n;# t@          $ r.}|                    d	t1          |                     Y d}~nd}~ww xY w|                    |           d|i|!                                 S # |!                                 w xY w)u   Apply the same patch to every id in ``payload.ids``.

    This is an *independent* iteration — per-task failures don't abort
    siblings. Returns per-id outcome so the UI can surface partials.
    c                    g | ]}||S r   r   )r   is     r"   r   zbulk_update.<locals>.<listcomp>  s    
/
/
/Q
/1
/
/
/ra   r(   zids is requiredr)   r8   T)rZ   r~  NFz	not found)r~  r   zarchive refusedrE   r  rC   rA   r  rB   r  r@   >   r?   r>   zunknown status ztransition to z refused)r  zassign refusedr  r  r*  results)"r  r   r6   r=   r   r  updater   r  r  r   r  r  r   r   r  r  r  r  r   r  reassign_taskr  r  r1   r*  r  r   r   r  r  r  r   r  )r]   r$   r  r  r   r   entryrK   r  r~  r  r  s               r"   bulk_updater    s    0
/w{(b
/
/
/C G4EFFFFG5!!EuDQ M	" M	"C+.d$;$;EJ5 )$44<LLEL===NN5)))? H$1$<< H5FGGG>-go-AF{{&4 ##*>$+O%,%5	   i&1$<<g'0s;; H3:1I#I#I!*!7c!B!BBB!3D#w!G!GBBi$!@ %     u--- k))&4T3??000/c1==5Lq5L5LMMMu---  U5Sa5S5S5STTT#/="0 !*!8 $c7+;+Ct.2" " "BB
 "+!6 $c7+;+Ct" "B  " K!LLE9ILJJJ' = = =SVV<<<<<<<<=#/",T22 
 
H !122C8   @ $*j#g>N:O:O-P"Q"Q --/  
 
 
 
 
 
 
 
 
 
 
 
 
 
 
  5 5 5SVV444444445NN5!!!!7#



s   	
O6 ANO6 DN.O6 0A!NO6 $N8AJN
K $K	N	KN,BN 4N N	NN	NO6 
O$N?:O6 ?OO6 6Pz/diagnosticsz*Filter by severity: warning|error|criticalr   c           	     ,   t          |           } t          |           }	 t          |d          }|sg dd|                                 S rNi }|                                D ]\  }}fd|D             }|r|||<   |}|sg dd|                                 S t          |                                          }d                    dgt          |          z            }	d	 |	                    d
|	 dt          |                                                    D             }
g }|                                D ]S\  }}|
                    |          }|                    ||r|d         nd|r|d         nd|r|d         nd|d           Tddlm} d t!          |          D             fd}|                    |           |t%          d |D                       d|                                 S # |                                 w xY w)a  Return ``[{task_id, task_title, task_status, task_assignee,
    diagnostics: [...]}, ...]`` for every task on the board with at
    least one active diagnostic.

    Severity-filterable so the UI can render "just the critical ones"
    or the CLI can grep. Useful for the board-header attention strip
    AND for ``hermes kanban diagnostics`` which shells to this
    endpoint when the dashboard's running, or invokes the engine
    directly when it isn't.
    r8   Nr   r   )r   r   c                d    g | ],}t          j        |                    d                     *|-S )r   )r   severity_at_or_abover   )r   rT   r   s     r"   r   z$list_diagnostics.<locals>.<listcomp>  s8    ^^^a)@zARART\)])]^^^^ra   r   r   c                     i | ]}|d          |S r   r   r   s     r"   r   z$list_diagnostics.<locals>.<dictcomp>  s.     
 
 
 dGQ
 
 
ra   z;SELECT id, title, status, assignee FROM tasks WHERE id IN (r   r)  r   r   )r[   
task_titletask_statustask_assigneer   r   c                    i | ]\  }}||	S r   r   )r   r  r  s      r"   r   z$list_diagnostics.<locals>.<dictcomp>,  s    >>>DAq1a>>>ra   c                    | d         d         }                     |                     d          d           |                     d          pd fS )Nr   r   r   r   r   )r   )r  topsev_idxs     r"   	_sort_keyz#list_diagnostics.<locals>._sort_key-  sS    m$Q'CSWWZ00"555''.)).Q/ ra   keyc              3  @   K   | ]}t          |d                    V  dS )r   N)r   r   s     r"   rH  z#list_diagnostics.<locals>.<genexpr>7  s/      <<1Q}-..<<<<<<ra   )r6   r=   r   r  itemsr8  r   r   r   r   r   r   r   r   r   r   	enumeratesortsum)r$   r   r   diags_by_taskfilteredr   dlkeepr  r   r   r   rs   r   r  r  s    `             @r"   list_diagnosticsr    s   $ 5!!EuD61$FFF 	3#%22f 	

a  	7.0H(..00 ) )R^^^^2^^^ )$(HSM$M  7')A66P 	

I =%%''((xxC 011
 
\\]l]]]c

  hjj
 
 
 $**,, 	 	GCAJJ,-7ajj4./9q{{T23!=:!      	A@@@@@>>In$=$=>>>	 	 	 	 	 	Y <<<<<<<
 

 	



s   G= ;G= E	G= =Hz/workers/activec                d   t          |           } t          |           }	 |                    d                                          }d |D             }|t	          |          t          t          j                              d|                                 S # |                                 w xY w)a  Return every currently-running worker on the board.

    A worker is a ``task_runs`` row whose ``ended_at`` is NULL and whose
    ``worker_pid`` is non-NULL, belonging to a task with ``status='running'``.

    Returns ``{workers: [...], count: N, checked_at: <epoch>}``.  Each
    worker entry carries enough context for the dashboard to link back to
    its task without a second round-trip.
    r8   a  
            SELECT
                r.id          AS run_id,
                r.task_id,
                t.title       AS task_title,
                t.status      AS task_status,
                t.assignee    AS task_assignee,
                r.profile,
                r.worker_pid,
                r.started_at,
                r.claim_lock,
                r.claim_expires,
                r.last_heartbeat_at,
                r.max_runtime_seconds
            FROM task_runs r
            JOIN tasks t ON t.id = r.task_id
            WHERE r.ended_at IS NULL
              AND r.worker_pid IS NOT NULL
              AND t.status = 'running'
            ORDER BY r.started_at ASC
            c                    g | ]Y}|d          |d         |d         |d         |d         |d         |d         |d         |d         |d	         |d
         |d         dZS )r_   r[   r  r  r  rv   rz   r}   rx   ry   r|   r{   )r_   r[   r  r  r  rv   rz   r}   rx   ry   r|   r{   r   )r   r  s     r"   r   z'list_active_workers.<locals>.<listcomp>o  s     
 
 
  h-y>!,/"=1!$_!5y>!,/!,/!,/!$_!5%()<%='*+@'A 
 
 
ra   )workersr   
checked_at)r6   r=   r   r   r   r   r  r  )r$   r   r   r  s       r"   list_active_workersr  H  s     5!!EuD+||
 
, (**- 	.
 
 
 
 
" #S\\TY[[IYIYZZ



s   A#B B/z/runs/{run_id}r_   c                   t          |          }t          |          }	 t          j        ||           }|t	          dd|  d          dt          |          i|                                 S # |                                 w xY w)zDirect lookup of a ``task_runs`` row by its integer id.

    Returns ``{run: {...}}`` using the same serialisation as the
    per-task run history embedded in ``GET /tasks/{task_id}``.
    404 when no such run exists.
    r8   Nr,   run r  r)   run)r6   r=   r   get_runr   r   r  )r_   r$   r   rs   s       r"   get_run_endpointr    s     5!!EuDdF++9C8Qv8Q8Q8QRRRRy||$



s   <A2 2Bz/runs/{run_id}/inspectc                   t          |          }t          |          }	 t          j        ||           }|t	          dd|  d          	 |                                 n# |                                 w xY w|j        | ddd	S |j        | dd
d	S |j        }t          | d|ddS 	 t          j	        |          }|
                    g d          }	 |                                }n# t          $ r d}Y nw xY w|                    d          }| d||                    d          |r|j        nd|r|j        nd|                    d          ||                    d          |                    d          |                    d          dS # t          j        $ r
 | d|ddcY S t          j        $ r
 | d|ddcY S w xY w)a7  Live PID stats for a run's worker process via psutil.

    If the run has already ended, or has no recorded ``worker_pid``,
    returns ``{alive: false}`` with a human-readable ``reason``.

    When the process is live, returns CPU, memory, thread count, fd count,
    status, create_time, and cmdline.  ``access_denied`` is set when the
    OS refuses inspection rather than raising a 500.

    psutil availability: if psutil is not installed the endpoint still
    works but ``alive`` is always returned as ``false`` with
    ``reason="psutil not available"``.
    r8   Nr,   r  r  r)   Fzrun already ended)r_   aliver  zno worker_pid recordedzpsutil not available)r_   r  r   r  )cpu_percentmemory_infonum_threadsr   create_timecmdline)attrsr  Tr  r  r   r  r  )r_   r  r   r  memory_rss_bytesmemory_vms_bytesr  num_fdsr   r  r  zprocess not foundzaccess denied)r_   r  r   r   )r6   r=   r   r  r   r  r~   rz   _psutilProcessas_dictr  AttributeErrorr   rssvmsNoSuchProcessAccessDenied)	r_   r$   r   rs   r   procinfor  mems	            r"   inspect_run_endpointr    sF   $ 5!!EuDdF++9C8Qv8Q8Q8QRRRR  	



z 5<OPPP| 5<TUUU
,C 5H^___Ws##|| #
 #
 #
|  
	llnnGG 	 	 	GGG	hh}%%88M22+. 8D+. 8D88M22hhx((88M22xx	**
 
 	
   ] ] ] 5H[\\\\\ W W W 4oVVVVVWsH   ,A# #A9,,F C. -F .C=:F <C==BF G,GGc                      e Zd ZU dZded<   dS )TerminateRunBodyNr%   r  r4  r5  r6  r  r7  r   ra   r"   r  r    #          F      ra   r  z/runs/{run_id}/terminatec                   t          |          }t          |          }	 t          j        ||           }|t	          dd|  d          |j        t	          dd|  d          t          j        ||j        |j        	          }|st	          dd
|  d|j         d          d| |j        d|	                                 S # |	                                 w xY w)a  Terminate the worker process backing an in-flight run.

    Resolves ``run_id`` to its parent ``task_id`` and routes through
    :func:`kanban_db.reclaim_task` so the SIGTERM->SIGKILL flow,
    run-outcome bookkeeping, and event-log append all match what the
    existing ``POST /tasks/{task_id}/reclaim`` endpoint does.

    Responses:
      * 200 ``{"ok": true, "run_id": ..., "task_id": ...}`` on success.
      * 404 when ``run_id`` is unknown.
      * 409 when the run has already ended, or the task is no longer in
        a claimable state.

    Closes the gap left by PR #28432, which shipped the read-only
    sibling endpoints (``/workers/active``, ``/runs/{run_id}``,
    ``/runs/{run_id}/inspect``) but no termination control surface.
    r8   Nr,   r  r  r)   r  z already endedr  zcannot terminate run z: task z$ is no longer in a reclaimable stateT)r~  r_   r[   )
r6   r=   r   r  r   r~   reclaim_taskr[   r  r  )r_   r]   r$   r   rs   r~  s         r"   terminate_run_endpointr    s!   . 5!!EuDdF++9C8Qv8Q8Q8QRRRR:!4f444    #D!)GNKKK 	4F 4 419 4 4 4    fCC



s   BC Cc                      e Zd ZU dZded<   dS )ReclaimBodyNr%   r  r  r   ra   r"   r  r    r  ra   r  z/tasks/{task_id}/reclaimc                   t          |          }t          |          }	 t          j        || |j                  }|st          dd|  d          d| d|                                 S # |                                 w xY w)	a+  Release an active worker claim on a running task.

    Used by the dashboard recovery popover when an operator wants to
    abort a stuck worker (e.g. one that keeps hallucinating card ids)
    without waiting for the claim TTL. Maps 1:1 to
    ``hermes kanban reclaim <task_id> --reason ...``.
    r8   r  r  zcannot reclaim z7: not in a claimable state (not running, or unknown id)r)   T)r~  r[   )r6   r=   r   r  r  r   r  r[   r]   r$   r   r~  s        r"   reclaim_task_endpointr    s     5!!EuD#D''.III 	3g 3 3 3    w//



s   7A- -Bc                  "    e Zd ZU dZdZded<   dS )SpecifyBodyu   Optional author override. Nothing else is configurable from the
    dashboard — model + prompt come from ``auxiliary.triage_specifier``
    in config.yaml, same as the CLI.Nr%   rf   )r4  r5  r6  __doc__rf   r7  r   ra   r"   r  r  9  s/         ( ( !F      ra   r  z/tasks/{task_id}/specifyc                (   t          |          }t          j        |pt          j                  5  ddlm} |                    | |j        pd          }ddd           n# 1 swxY w Y   t          |j	                  |j
        |j        |j        dS )u  Flesh out a triage-column task via the auxiliary LLM and promote
    it to ``todo``. Maps 1:1 to ``hermes kanban specify <task_id>``.

    Returns the outcome shape used by the CLI: ``{ok, task_id, reason,
    new_title}``. A non-OK outcome is NOT an HTTP error — the UI renders
    the reason inline (e.g. "no auxiliary client configured") so the
    operator knows what to fix, and retries without a page reload.

    This endpoint runs in FastAPI's threadpool (sync ``def``) because
    the underlying LLM call can take tens of seconds to minutes on
    reasoning models, which would block the event loop if we used
    ``async def`` without an explicit ``run_in_executor``.
    r   )kanban_specifyNrf   )r~  r[   r  	new_title)r6   r   scoped_current_boardr2   r   r  specify_taskrf   r   r~  r[   r  r  )r[   r]   r$   r  r   s        r"   specify_task_endpointr!  A  s    & 5!!E 
	'(H1H	I	I 
 
 	.----- --N*d . 
 

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 7:?.&	     %A!!A%(A%c                  :    e Zd ZU dZded<   dZded<   dZded<   dS )ReassignBodyNr%   rv   Fr   r  r  )r4  r5  r6  rv   r7  r  r  r   ra   r"   r$  r$  m  sE         !G!!!!M F      ra   r$  z/tasks/{task_id}/reassignc                R   t          |          }t          |          }	 t          j        || |j        pdt          |j                  |j                  }|st          dd|  d          d| |j        pdd	|	                                 S # |	                                 w xY w)
aa  Reassign a task to a different profile, optionally reclaiming first.

    Used by the dashboard recovery popover when an operator wants to
    retry a task with a different worker profile (e.g. switch to a
    smarter model after the assigned profile keeps hallucinating).
    Maps 1:1 to ``hermes kanban reassign <task_id> <profile> [--reclaim]``.
    r8   N)r  r  r  zcannot reassign zS: unknown id, or still running (pass reclaim_first=true to release the claim first)r)   T)r~  r[   r   )
r6   r=   r   r  rv   r   r  r  r   r  r  s        r"   reassign_task_endpointr&  s  s     5!!EuD$'O#tw455>	
 
 
  	Sw S S S    wGO<StTT



s   AB B&z/configc            	        	 ddl m}   |             pi }n# t          $ r i }Y nw xY w|                    d          pi }|                    d          pi }|                    d          pdt	          |                    dd                    t	          |                    d	d
                    t	          |                    dd                    dS )a$  Return kanban dashboard preferences from ~/.hermes/config.yaml.

    Reads the ``dashboard.kanban`` section if present; defaults otherwise.
    Used by the UI to pre-select tenant filters, toggle markdown rendering,
    or set column-width preferences without a round-trip per page load.
    r   r   r:  kanbandefault_tenantr'   lane_by_profileTinclude_archived_by_defaultFrender_markdown)r)  r*  r+  r,  )r   r   r   r   r   )r   cfgdash_cfgk_cfgs       r"   
get_configr0    s    111111kmm!r   $$*HLL""(bE))$455;		*;T B BCC'+EII6SUZ,[,['\'\		*;T B BCC	  s    $$c                 l   	 ddl m}  n# t          $ r g cY S w xY w	  |             }n# t          $ r g cY S w xY wg }|j                                        D ]H\  }}|r|j        s|j        }|                    |j        |j        |j	        pd|j
        pdd           I|                    d            |S )a  Return every platform that has a home_channel set, fully hydrated.

    Reads the live GatewayConfig so env-var overlays (``TELEGRAM_HOME_CHANNEL``
    etc.) are honored alongside config.yaml. Returns platforms in a stable
    order and drops platforms without a home.
    r   )load_gateway_configr'   Home)platformchat_id	thread_idr   c                    | d         S )Nr4  r   r   s    r"   <lambda>z+_configured_home_channels.<locals>.<lambda>  s
    a
m ra   r  )gateway.configr2  r   	platformsr  home_channelr   valuer5  r6  r   r  )r2  gw_cfgr  r4  pcfghcs         r"   _configured_home_channelsr@    s   6666666   			$$&&   			F *0022 	 	$ 	4, 	 z+G%v	
 
 	 	 	 	 KK++K,,,Ms   	 
' 66c                 J    	 ddl m}   |             pdS # t          $ r Y dS w xY w)z@Return the current Hermes profile name for notify-sub ownership.r   get_active_profile_namedefault)hermes_cli.profilesrC  r   rB  s    r"   _active_profile_namerF    sO    ??????&&((5I5   yys    
""subdicthomec                .   |                      d          |d         k    ovt          |                      dd                    t          |d                   k    o<t          |                      d          pd          t          |d         pd          k    S )z@True if a notify_subs row corresponds to the given home channel.r4  r5  r'   r6  )r   r1   )rG  rI  s     r"   _home_sub_matchesrK    s     	
tJ// 	L	2&&''3tI+?+??	L$$*++s43D3J/K/KKra   z/home-channelsc                   t                      }t                      }| rt          |          }t          |          }	 t	          j        ||           }|                                 n# |                                 w xY w|D ]}t          |                    d          pd          t          |                    d          pd          t          |                    d          pd          f}|	                    |           g }|D ]6}	|	d         |	d         |	d         f}|
                    i |	d||v i           7d|iS )u   List every platform with a home channel, plus whether *task_id*
    (if given) is currently subscribed to that home.

    When ``task_id`` is omitted, every entry's ``subscribed`` is ``false``
    — useful for the "no task selected" state of the UI.
    r8   r4  r'   r5  r6  
subscribedhome_channels)r@  setr6   r=   r   list_notify_subsr  r1   r   addr   )
r[   r$   homessubscribed_homesr   subsrG  r  r  rI  s
             r"   get_home_channelsrU    s_    &''E25%% &u%%5!!!	-dG<<DJJLLLLDJJLLLL 	& 	&CCGGJ''-2..CGGI&&,"--CGGK((.B//C
   %%%%F G GJi${2CDEE|S4D-DEEFFFFV$$s   A) )A?z*/tasks/{task_id}/home-subscribe/{platform}r4  c           	        t                      }t          fd|D             d          }|st          ddd d          t          |          }t	          |          }	 t          j        ||           }|t          dd	|  d
          t          j        || |d         |d         pdt                                 d| |d|	                                 S # |	                                 w xY w)u   Subscribe *task_id* to notifications routed to *platform*'s home channel.

    Idempotent — re-subscribing is a no-op at the DB layer. 404 if the
    platform has no home channel configured. 404 if the task doesn't exist.
    c              3  4   K   | ]}|d          k    |V  dS r4  Nr   r   hr4  s     r"   rH  z!subscribe_home.<locals>.<genexpr>  1      ??qQz]h%>%>%>%>%>%>??ra   Nr,   (No home channel configured for platform zJ. Set one from the messenger via /sethome, or configure gateway.platforms.z.home_channel in config.yaml.r)   r8   r  r  r5  r6  )r[   r4  r5  r6  notifier_profileTr~  r[   r;  )
r@  nextr   r6   r=   r   r  add_notify_subrF  r  )r[   r4  r$   rR  rI  r   rK   s    `     r"   subscribe_homera    sI    &''E????E???FFD 
Ph P P(0P P P
 
 
 	
 5!!EuD!$00<C8S8S8S8STTTT O;'/4133	
 	
 	
 	
 wEE



s   &A%C   C6c                z   t                      }t          fd|D             d          }|st          ddd          t          |          }t	          |          }	 t          j        || |d         |d	         pd
           d| |d|                                 S # |                                 w xY w)zKRemove any notify subscription on *task_id* that matches *platform*'s home.c              3  4   K   | ]}|d          k    |V  dS rX  r   rY  s     r"   rH  z#unsubscribe_home.<locals>.<genexpr><  r[  ra   Nr,   r\  rI  r)   r8   r5  r6  )r[   r4  r5  r6  Tr^  )r@  r_  r   r6   r=   r   remove_notify_subr  )r[   r4  r$   rR  rI  r   s    `    r"   unsubscribe_homere  8  s     &''E????E???FFD 
KhKKK
 
 
 	
 5!!EuD
#O;'/4	
 	
 	
 	
 wEE



s   #,B$ $B:z/statsc                    t          |           } t          |           }	 t          j        |          |                                 S # |                                 w xY w)zPer-status + per-assignee counts + oldest-ready age.

    Designed for the dashboard HUD and for router profiles that need to
    answer "is this specialist overloaded?" without scanning the whole
    board themselves.
    r8   )r6   r=   r   board_statsr  r$   r   s     r"   	get_statsri  U  sU     5!!EuD$T**



s   A	 	Az
/assigneesc                    t          |           } t          |           }	 dt          j        |          i|                                 S # |                                 w xY w)a<  Known profiles + per-profile task counts.

    Returns the union of ``~/.hermes/profiles/*`` on disk and every
    distinct assignee currently used on the board. The dashboard uses
    this to populate its assignee dropdown so a freshly-created profile
    appears in the picker before it's been given any task.
    r8   r   )r6   r=   r   known_assigneesr  rh  s     r"   get_assigneesrl  e  sZ     5!!EuDY6t<<=



   A A!z/tasks/{task_id}/logr   i )geletailr0  c           	        t          |          }t          |          }	 t          j        ||           }|                                 n# |                                 w xY w|t          dd|  d          t          j        | ||          }t          j        | |          }|                                r|	                                j
        nd}| t          |          |du||pd	t          |o||k              d
S )u}  Return the worker's stdout/stderr log.

    ``tail`` caps the response size (bytes) so the dashboard drawer
    doesn't paginate megabytes into the browser. Returns 404 if the task
    has never spawned. The on-disk log is rotated at 2 MiB per
    ``_rotate_worker_log`` — a single ``.log.1`` is kept, no further
    generations, so disk usage per task is bounded at ~4 MiB.
    r8   Nr,   r  r  r)   )
tail_bytesr$   r   r'   )r[   ru  r]  
size_bytescontent	truncated)r6   r=   r   r  r  r   read_worker_logworker_log_pathr]  statst_sizer1   r   )r[   rp  r$   r   rK   rt  log_pathro   s           r"   get_task_logr{  z  s    5!!EuD!$00



|4OG4O4O4OPPPP'DNNNG(>>>H&.oo&7&7>8==??""QDH%=b$.4$;//  rm  z	/dispatch   max)aliasdry_runmax_nc                V   t          |          }t          |          }	 t          j        || ||          }	 t	          |          |                                 S # t          $ r( dt          |          icY |                                 S w xY w# |                                 w xY w)Nr8   )r  	max_spawnr$   r  )r6   r=   r   dispatch_oncer   r  	TypeErrorr1   )r  r  r$   r   r  s        r"   dispatchr    s     5!!EuD
('U%
 
 
	+&>> 	

  	+ 	+ 	+c&kk***

	+ 	

s(   B A B8B BB B(c                  `    e Zd ZU ded<   dZded<   dZded<   dZded<   dZded<   d	Zd
ed<   dS )CreateBoardBodyr1   slugNr%   r   r   iconcolorFr   switch)	r4  r5  r6  r7  r   r   r  r  r  r   ra   r"   r  r    sp         IIID!%K%%%%DEFra   r  c                  H    e Zd ZU dZded<   dZded<   dZded<   dZded<   dS )RenameBoardBodyNr%   r   r   r  r  )r4  r5  r6  r   r7  r   r  r  r   ra   r"   r  r    sV         D!%K%%%%DEra   r  r  dict[str, int]c                j   	 t          j        |           }|                                si S t          j        |           }	 |                    d                                          }d |D             |                                 S # |                                 w xY w# t          $ r i cY S w xY w)z<Return ``{status: count}`` for a board. Safe on an empty DB.r8   z7SELECT status, COUNT(*) AS n FROM tasks GROUP BY statusc                F    i | ]}|d          t          |d                   S )r   r   )r   r   s     r"   r   z!_board_counts.<locals>.<dictcomp>  s(    ;;;AhKQsV;;;ra   )r   kanban_db_pathr]  r<   r   r   r  r   )r  ru  r   r   s       r"   _board_countsr    s    'd333{{}} 	I t,,,	<<I hjj  <;d;;;JJLLLLDJJLLLL   			s.   *B# B# 2B
 5B# 
B  B# #B21B2z/boardsc                   t          j        |           }t          j                    }|D ]S}|d         |k    |d<   t          |d                   |d<   t	          |d                                                   |d<   T||dS )z@Return every board on disk with task counts and the active slug.)r   r  
is_currentcountsr   )boardsr  )r   list_boardsget_current_boardr  r  values)r   r  r  bs       r"   r  r    s     "4DEEEF)++G / /V9/,#AfI..(8++--..'

111ra   c                   	 t          j        | j        | j        | j        | j        | j                  }n0# t          $ r#}t          dt          |                    d}~ww xY w| j
        rL	 t          j        |d                    n0# t          $ r#}t          dt          |                    d}~ww xY w|t          j                    dS )uG   Create a new board. Idempotent — ``slug`` collision returns existing.r   r   r  r  r(   r)   Nr  )r$   r  )r   create_boardr  r   r   r  r  r0   r   r1   r  set_current_boardr  )r]   metar5   s      r"   create_board_endpointr    s    	>%L+-
 
 
  > > >CHH====>~ B	B'V5555 	B 	B 	BCCAAAA	Bi&A&C&CDDDs,   25 
A"AA"-B 
B5B00B5z/boards/{slug}c                F   	 t          j        |           }n0# t          $ r#}t          dt	          |                    d}~ww xY w|rt          j        |          st          dd| d          t          j        ||j        |j        |j	        |j
                  }d|iS )	uc   Update a board's display metadata (slug is immutable — create a new one to rename the directory).r(   r)   Nr,   r-   r.   r  r$   )r   r/   r0   r   r1   r3   write_board_metadatar   r   r  r  )r  r]   r4   r5   r  s        r"   rename_boardr    s    >066 > > >CHH====> V/77 V4TT4T4T4TUUUU)\'\m  D T?    
A?AzHard-delete instead of archivedeletec                    	 t          j        | |           }n0# t          $ r#}t          dt	          |                    d}~ww xY w|t          j                    dS )z)Archive (default) or hard-delete a board.)r  r(   r)   N)r  r  )r   remove_boardr0   r   r1   r  )r  r  resr5   s       r"   delete_boardr    so    >$Tv:>>> > > >CHH====>i&A&C&CDDDs    
AAAz/boards/{slug}/switchc                   	 t          j        |           }n0# t          $ r#}t          dt	          |                    d}~ww xY w|rt          j        |          st          dd| d          t          j        |           d|iS )u  Persist ``slug`` as the active board for subsequent CLI / slash calls.

    Dashboard users pick boards via a client-side ``localStorage`` — this
    endpoint is for ``/kanban boards switch`` parity so gateway slash
    commands and the CLI share the same current-board pointer.
    r(   r)   Nr,   r-   r.   r  )r   r/   r0   r   r1   r3   r  )r  r4   r5   s      r"   switch_boardr    s    >066 > > >CHH====> V/77 V4TT4T4T4TUUUU'''vr  g333333?c                      e Zd ZU dZded<   dS )DescribeBodyNr%   r   )r4  r5  r6  r   r7  r   ra   r"   r  r  ;  s#         !%K%%%%%%ra   r  c                      e Zd ZU dZded<   dS )DescribeAutoBodyFr   	overwriteN)r4  r5  r6  r  r7  r   ra   r"   r  r  ?  s#         Ira   r  z	/profilesc                     	 ddl m}  |                                 }n&# t          $ r}t	          dd|           d}~ww xY wdd |D             iS )	u  Return every installed profile with its description.

    Consumed by the dashboard's settings panel (orchestrator picker)
    and the profile-description editing UI. Profiles without a
    description still appear here — they're routable on name alone,
    just less precisely.
    r   profilesrX  zfailed to list profiles: r)   Nr  c                    g | ]^}|j         t          |j                  |j        pd |j        pd |j        pd t          |j                  t          |j        pd          d_S )r'   r   )r   
is_defaultmodelproviderr   description_autoskill_count)	r   r   r  r  r  r   r  r   r  r  s     r"   r   z'list_profile_roster.<locals>.<listcomp>R  s     
 
 
  "1<00BJ," }2$();$<$<"1=#5A66 
 
 
ra   )r   r  list_profilesr   r   )profiles_modr  r5   s      r"   list_profile_rosterr  C  s    W777777--// W W W4UPS4U4UVVVVW 	 
 
 
 
 
 s    
A ;A z/profiles/{profile_name}profile_namec                   	 ddl m} |                    |           }|dk    r ddlm} ddlm}  | |                      }n|                    |          }|                                st          dd|  d	          |j
        pd
                                }|                    ||d           n0# t          $ r  t          $ r}t          dd| 	          d}~ww xY wd||dS )a  Set or clear the description of a profile.

    Empty string clears the description; non-empty stores it as a
    user-authored description (``description_auto: false``) so the
    auto-describer won't overwrite it on a sweep without
    ``--overwrite``.
    r   r  rD  )get_hermes_homer   r,   	profile 'z' not foundr)   r'   F)r   r  rX  zfailed to update profile: NT)r~  rv   r   )r   r  normalize_profile_namehermes_constantsr  pathlibr   get_profile_diris_dirr   r   rL  write_profile_metar   )	r  r]   r  canonr  _Pathprofile_dirtextr5   s	            r"   update_profile_descriptionr  a  sg   X77777733LAAI888888------% 1 122KK&66u==K!!## 	_C8]L8]8]8]^^^^#)r0022''" 	( 	
 	
 	
 	

     X X X4VQT4V4VWWWWX5>>>s   B2B5 5C"	CC"z&/profiles/{profile_name}/describe-autoc                    	 ddl m} |                    | t          |j                            }n&# t
          $ r}t          dd|           d}~ww xY wt          |j                  |j        |j	        |j
        dS )	u  Generate a description for the named profile via the auxiliary
    LLM (``auxiliary.profile_describer``). Persists with
    ``description_auto: true`` so the dashboard can surface a "review"
    badge.

    Maps 1:1 to ``hermes profile describe <name> --auto``. Non-OK
    outcomes are NOT HTTP errors — the UI renders the reason inline
    (e.g. "no auxiliary client configured") so the operator can fix
    config and retry without a page reload.
    r   )profile_describer)r  rX  zdescriber crashed: r)   N)r~  rv   r  r   )r   r  describe_profiler   r  r   r   r~  r  r  r   )r  r]   r  r   r5   s        r"   auto_describe_profiler    s    Q000000#447,-- 5 
 
  Q Q Q4O#4O4OPPPPQ 7:'.*	  s   /2 
AAAc                      e Zd ZU dZded<   dS )DecomposeBodyNr%   rf   )r4  r5  r6  rf   r7  r   ra   r"   r  r    r  ra   r  z/tasks/{task_id}/decomposec                ^   t          |          }t          j        |pt          j                  5  ddlm} |                    | |j        pd          }ddd           n# 1 swxY w Y   t          |j	                  |j
        |j        t          |j                  |j        pg |j        dS )u  Fan a triage-column task out into a graph of child tasks via the
    auxiliary LLM, routed to specialist profiles by description. Maps
    1:1 to ``hermes kanban decompose <task_id>``.

    Returns the outcome shape used by the CLI: ``{ok, task_id, reason,
    fanout, child_ids, new_title}``. A non-OK outcome is NOT an HTTP
    error — the UI renders the reason inline.

    Runs in FastAPI's threadpool (sync ``def``) because the LLM call
    can take minutes on reasoning models.
    r   )kanban_decomposeNr  )r~  r[   r  fanout	child_idsr  )r6   r   r  r2   r   r  decompose_taskrf   r   r~  r[   r  r  r  r  )r[   r]   r$   r  r   s        r"   decompose_task_endpointr    s    " 5!!E
 
	'(H1H	I	I 
 
//////"11N*d 2 
 

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 7:?.w~&&&,"&  r"  c                  H    e Zd ZU dZded<   dZded<   dZded<   dZded<   dS )OrchestrationSettingsBodyNr%   orchestrator_profiledefault_assigneezOptional[bool]auto_decomposeauto_promote_children)r4  r5  r6  r  r7  r  r  r  r   ra   r"   r  r    sY         *.....&*****%)N)))),0000000ra   r  z/orchestrationc                    	 ddl m}   |             pi }n# t          $ r i }Y nw xY wt          |t                    r|                    d          pi ni }|                    d          pd                                }|                    d          pd                                }t          |                    dd                    }t          |                    d	d                    }|}|}	 dd
lm	}	 |	
                                pd}
|r|	                    |          s|
}|r|	                    |          s|
}n# t          $ r d}
|s|
}|s|
}Y nw xY w|||||||
dS )z}Return the current kanban orchestration knobs from config.yaml
    plus the resolved effective values (filling in fallbacks).r   r   r(  r  r'   r  r  Tr  r  rD  )r  r  r  r  resolved_orchestrator_profileresolved_default_assigneeactive_profile)r   r   r   
isinstancerH  r   rL  r   r   r  rC  profile_exists)r   r-  
kanban_cfgexplicit_orchexplicit_defaultr  r  resolved_orchresolved_defaultr  active_defaults              r"   get_orchestration_settingsr    s   111111kmm!r   .8d.C.CK#''(##)rJ^^$:;;ArHHJJM"'9::@bGGII*..)94@@AAN 0G!N!NOO "M'.777777%==??L9 	+L$?$?$N$N 	+*M 	.|'B'BCS'T'T 	.- . . ." 	+*M 	.-. !.,(!6)6%5(  s    $$2AE EEc                   	 ddl m}m}  |            pi }n&# t          $ r}t	          dd|           d}~ww xY w|                    di           }t          |t                    si }||d<   	 ddlm	} n# t          $ r d}Y nw xY w| j
        j| j
        pd	                                }|rH|F	 |                    |          st	          d
d| d          n# t          $ r  t          $ r Y nw xY w||d<   | j        j| j        pd	                                }|rH|F	 |                    |          st	          d
d| d          n# t          $ r  t          $ r Y nw xY w||d<   | j        t          | j                  |d<   | j        t          | j                  |d<   	  ||           n&# t          $ r}t	          dd|           d}~ww xY wt#                      S )u  Update the kanban orchestration knobs in ~/.hermes/config.yaml.

    Each field is optional — only fields explicitly passed are
    written. ``orchestrator_profile`` / ``default_assignee`` accept
    empty strings to clear the override and fall back to the default
    profile.
    r   )r   save_configrX  zfailed to load config: r)   Nr(  r  r'   r(   r  z' does not existr  r  r  r  zfailed to save config: )r   r   r  r   r   r   r  rH  r   r  r  rL  r  r  r  r   r  r  )r]   r   r  r-  r5   kanban_sectionr  r   s           r"   set_orchestration_settingsr  	  s   U>>>>>>>>kmm!r U U U4Sc4S4STTTTU ^^Hb11Nnd++ '&H7777777    #/,299;; 
	L,	#22488 '$'A4AAA   
 !      15-.+(.B5577 
	L,	#22488 '$'A4AAA   
 !      -1)*)+/0F+G+G'($026w7T2U2U./UC U U U4Sc4S4STTTTU &'''s^    
:5:0A7 7BB0*C C21C2!*E E#"E#(F4 4
G>GGz/eventsr   c                  K   t          |           s(|                     t          j                   d {V  d S |                                  d {V  	 | j                            dd          }	 t          |          }n# t          $ r d}Y nw xY w| j                            d          }	 |rt          j
        |          nd n# t          $ r d Y nw xY wdfd
}	 t          j        ||           d {V \  }}|r|                     ||d           d {V  t          j        t                     d {V  ^# t           $ r Y d S t          j        $ r Y d S t$          $ rX}t&                              d|           	 |                                  d {V  n# t$          $ r Y n
w xY wY d }~d S Y d }~d S d }~ww xY w)N)codesince0r   r$   
cursor_valr   r   tuple[int, list[dict]]c           
        t          j                  }	 |                    d| f                                          }g }| }|D ]|}	 |d         rt	          j        |d                   nd }n# t          $ r d }Y nw xY w|                    |d         |d         |d         |d         ||d         d	           |d         }}||f|                                 S # |                                 w xY w)
Nr8   zmSELECT id, task_id, run_id, kind, payload, created_at FROM task_events WHERE id > ? ORDER BY id ASC LIMIT 200r]   rZ   r[   r_   r\   r^   )rZ   r[   r_   r\   r]   r^   )	r   r<   r   r   r  loadsr   r   r  )r  r   r   r   
new_cursorrs   r]   ws_boards          r"   
_fetch_newz!stream_events.<locals>._fetch_newg	  s*   $8444D||NM  (**	 
 #%'
 ) )A'>?	l"T$*Qy\":":":PT$ ' ' '"&'JJg#$Y<"#H+ !&	#*&'o       "#4JJ!3



s0   1C 
$A/.C /A>;C =A>>A	C C2T)r  cursorzKanban event stream error: %s)r  r   r   r  )r#   r  http_statusWS_1008_POLICY_VIOLATIONacceptquery_paramsr   r   r0   r   r/   asyncio	to_thread	send_jsonsleep_EVENT_POLL_SECONDSr   CancelledErrorr   r:   r;   )r   	since_rawr  ws_board_rawr  r  r5   r  s          @r"   stream_eventsr  K	  s      ""%% hhK@hAAAAAAAAA
))++@O''55		^^FF 	 	 	FFF	 **733	HT^y6|DDDZ^HH 	 	 	HHH		 	 	 	 	 	8	5#*#4Z#H#HHHHHHHNFF Illf#G#GHHHHHHHHH- 3444444444		5
    !    	   3S999	((** 	 	 	D	 DDDDDs   D? 2B D? BD? BD? /C D? CD? CA(D? ?
F>F>	F>&F9FF9
F*'F9)F**F99F>)r   r   r   r   )r$   r%   r   r%   )N)r$   r%   )rK   rL   rJ   r%   r   rM   )rV   rW   r   rM   )rb   rc   r   rM   )ri   rj   r   rM   )rs   rt   r   rM   )r   r   r   r   r   r   )r   r   r   r   )r   r   r[   r1   r   r   )
r   r%   r   r   r$   r%   r   r%   r   r%   )r[   r1   r$   r%   r  r%   r  r%   )r]   r(  r$   r%   )r@  r1   r   r1   )r[   r1   r$   r%   )r[   r1   rR  r   r$   r%   rp   r%   )rr  r   r$   r%   )r[   r1   r]   r  r$   r%   )r   r   r[   r1   r   r8  )r   r   r[   r1   r  r1   r   r   )r[   r1   r]   r  r$   r%   )r]   r  r$   r%   )r   r1   r   r1   r$   r%   )r]   r  r$   r%   )r$   r%   r   r%   )r_   r   r$   r%   )r_   r   r]   r  r$   r%   )r[   r1   r]   r  r$   r%   )r[   r1   r]   r  r$   r%   )r[   r1   r]   r$  r$   r%   )r   r   )r   r1   )rG  rH  rI  rH  r   r   )r[   r%   r$   r%   )r[   r1   r4  r1   r$   r%   )r[   r1   rp  r0  r$   r%   )r  r   r  r   r$   r%   )r  r1   r   r  )r   r   )r]   r  )r  r1   r]   r  )r  r1   r  r   )r  r1   )r  r1   r]   r  )r  r1   r]   r  )r[   r1   r]   r  r$   r%   )r]   r  )r   r   )r  
__future__r   r  r  loggingsqlite3r  dataclassesr   r  r   typingr   r   fastapir	   r
   r   r   r   r   r   r   r   r  fastapi.responsesr   pydanticr   r   r   r   r   r   	getLoggerr4  r:   routerr#   r6   r=   rG   r7  r   rU   r`   rh   rr   r   _WARNING_EVENT_KINDSr   r   r   r   r  r  r(  postr=  r`  rN  rQ  rq  r|  r  r  r  patchr  r  r  r  r  r  r  r  r  r  r  r  psutilr   ImportErrorr  r  r  r  r  r  r  r  r!  r$  r&  r0  r@  rF  rK  rU  ra  re  ri  rl  r{  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  putr  	websocketr  r   ra   r"   <module>r     s  ! ! !F # " " " " "                                  C  C  C  C  C  C  C  C  C  C  C  C  C  C  C  C  C  C  C  C  C  C * * * * * * % % % % % % % %             / / / / / /g!!	% % % %B   ,* * * * *>      
 "  %)     *            6  %)A A A A AH$ $ $ $N6 6 6 6. H!E$4OPPP"U5\\ 53YZZZ*/%M+ + + ',eG' ' 'C C C C CT  !5;;$)E\% % % %*EP% % %7 7 7 7  7|) ) ) ) )Y ) ) )" X@Ed * * * * *f )    & *++?DuT{{     ,+ +,, tCyy 5;;!%d	G G G G -,GT *++CH5;;     ,+4 -..AFt 	 	 	 	 /.	 $ $ $ $ $Y $ $ $  !!NSeTXkk s s s s "!st !""5:U4[[ 	 	 	 	 #"	   .c c c cT( ( ( ( () ( ( (
 ())KP5QU;;     *)(    y   
 X7<uT{{ 	 	 	 	 	 xU3ZZE#JJ 5;;    $	  	  	  	  	 9 	  	  	  ]>CeDkk ] ] ] ] ]L N 53YZZZ#e@  I I I I Ib   GGG  53YZZZ9 9 9 9 9x  !53YZZZ    * $%% !53YZZZ@W @W @W @W &%@WF! ! ! ! !y ! ! ! '(( !53YZZZ, , , , )(,f! ! ! ! !) ! ! ! '(( !5;;    )(:! ! ! ! !) ! ! ! '(( !5;;( ( ( ( )((V! ! ! ! !9 ! ! ! ()) !5;;    *)L I  N   >       "U4[[ 5;;% % % % %B 9::GLuT{{     ;:D ;<<INt     =<8 H%*U4[[      L).t     ( "##  %y999 5;;        $# N [E%LLq&&& 5;;    0    i            i         $ I).u 2 2 2 2 2 YE E E E(     $   +05Dd+e+e+e E E E E ! E $%%   &%0  & & & & &9 & & &    y    K  : ())? ? ? *)?@ 566   76>! ! ! ! !I ! ! ! )** !5;;# # # # +*#V1 1 1 1 1	 1 1 1 ' ' 'T C( C( C( C(L )J J J J J Js   O O)(O)