
    FjtR             	         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Zddl	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 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$Z+d%Z,	 ddd+Z-dd/Z.dd3Z/e!0                    d4           edd56           ed7           edd86           edd96           edd:6          fdd?            Z1e!0                    d@           ed           eddA6           eddB6          fddE            Z2 G dF dGe          Z3e!4                    dH           ed          fddJ            Z5 G dK dLe          Z6e!7                    d@           ed          fddM            Z8e!9                    d@           ed          fddN            Z:ddPZ;ddRZ< G dS dTe          Z=e!4                    dU           ed          fddV            Z> G dW dXe          Z?e!4                    dY           ed          fddZ            Z@e!9                    dY           ed[           ed[           ed          fdd^            ZA G d_ d`e          ZBe!4                    da           ed          fddb            ZCe!0                    dc           edd86           eddd6          fddf            ZD	 ddlEZFn# eG$ r dZFY nw xY we!0                    dg           edd86          fddh            ZHe!0                    di           edd86          fddl            ZIe!0                    dm           edd86          fddn            ZJ G do dpe          ZKe!4                    dq           edd86          fddr            ZL G ds dte          ZMe!4                    du           ed          fddv            ZN G dw dxe          ZOe!4                    dy           ed          fddz            ZP G d{ d|e          ZQe!4                    d}           ed          fdd~            ZRe!0                    d          d             ZSddZTddZUddZVe!0                    d           ed           ed          fdd            ZWe!4                    d           ed          fdd            ZXe!9                    d           ed          fdd            ZYe!0                    d           ed          fdd            ZZe!0                    d           ed          fdd            Z[e!0                    d           eddd           ed          fdd            Z\e!4                    d           ed7           edd           ed          fdd            Z] G d de          Z^ G d de          Z_ddZ`e!0                    d           ed7          fdd            Zae!4                    d          dd            Zbe!7                    d          dd            Zce!9                    d           ed7d6          fdd            Zde!4                    d          dd            ZedZf G d de          Zg G d de          Zhe!0                    d          d             Zie!7                    d          dd            Zje!4                    d          dd            Zk G d de          Zle!4                    d           ed          fdd            Zm G d de          Zne!0                    d¦          dÄ             Zoe!p                    d¦          ddĄ            Zqe!r                    dŦ          ddȄ            Zsd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)AnyOptional)	APIRouterHTTPExceptionQuery	WebSocketWebSocketDisconnectstatus)	BaseModelField)	kanban_dbkanban_diagnosticsprovidedOptional[str]returnboolc                    | sdS 	 ddl m} n# t          $ r Y dS w xY wt          |dd          }|sdS t	          j        t          |           t          |                    S )zConstant-time compare against the dashboard session token.

    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).
    Fr   )
web_serverT_SESSION_TOKENN)
hermes_clir   	Exceptiongetattrhmaccompare_digeststr)r   _wsexpecteds      B/usr/local/lib/hermes-agent/plugins/kanban/dashboard/plugin_api.py_check_ws_tokenr"   @   s      u0000000    tt	
 s,d33H ts8}}c(mm<<<s    
boardc                   | | 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   r   DEFAULT_BOARDboard_exists)r#   normedexcs      r!   _resolve_boardr3   V   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#   r2   s     r!   _connr:   l   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.TaskrG   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_secondsrG   )r   r   task_ager   )rH   rG   ds      r!   
_task_dictrR      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_idrV   )rS   s    r!   _event_dictr]      s0    h=
=&,      ckanban_db.Commentc                D    | j         | j        | j        | j        | j        dS )NrW   rX   authorbodyr[   rb   )r_   s    r!   _comment_dictre      s*    d9(l  r^   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.rW   rX   profilestep_keyr   
claim_lockclaim_expires
worker_pidmax_runtime_secondslast_heartbeat_at
started_atended_atoutcomesummarymetadataerror)rW   rX   ri   rj   r   rk   rl   rm   rn   ro   rp   rq   rr   rs   rt   ru   rf   s    r!   	_run_dictrw      s    ad19 	19 	AJ	
 	!( 	al 	 	al 	q4 	Q0 	al 	AJ 	19 	19 	AJ  	! r^   ) 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 rW    .0rf   s     r!   
<listcomp>z-_compute_task_diagnostics.<locals>.<listcomp>  s    %%%1qw%%%r^   c                    i | ]}|g S r   r   r   tids     r!   
<dictcomp>z-_compute_task_diagnostics.<locals>.<dictcomp>  s    &B&B&B3sB&B&B&Br^   z,SELECT * FROM task_events WHERE task_id IN (z) ORDER BY idrX   c                    i | ]}|g S r   r   r   s     r!   r   z-_compute_task_diagnostics.<locals>.<dictcomp>  s    $@$@$@S"$@$@$@r^   z*SELECT * FROM task_runs WHERE task_id IN (rW   )configc                6    g | ]}|                                 S r   )to_dictr   rQ   s     r!   r   z-_compute_task_diagnostics.<locals>.<listcomp>  s     333		333r^   )r   r   hermes_cli.configr   config_from_runtime_configjoinlenexecutetuplefetchall
setdefaultappendcompute_task_diagnosticsget)rz   r|   kdr   diag_configplaceholdersrowsrow_idsevents_by_taskev_rowruns_by_taskrun_rowoutrf   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Jr^   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_ORDERrY   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   rQ   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[  !!'	  r^   rX   r   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>K  s,        	
+  r^   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>R  s,        	
*  r^   ESELECT child_id FROM task_links WHERE parent_id = ? ORDER BY child_idparentschildren)r   )rz   rX   r   r   s       r!   
_links_forr   I  sz     TJ
 
  G SJ
 
  H H555r^   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``).
    r5   )r   r   r   r   z*SELECT parent_id, child_id FROM task_linksr   r   r   r   r   r   r   c                ,    i | ]}|d          |d         S )rX   nr   r   s     r!   r   zget_board.<locals>.<dictcomp>  s2     *
 *
 *
 iL!C&*
 *
 *
r^   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)rB   totalr   cstatusrB   Nr|   z1SELECT COALESCE(MAX(id), 0) AS m FROM task_eventsmc                    i | ]}|g S r   r   r   r_   s     r!   r   zget_board.<locals>.<dictcomp>  s    )G)G)GA!R)G)G)Gr^   archivedc                    g | ]	}|j         
S r   r   )r   ts     r!   r   zget_board.<locals>.<listcomp>  s    7L7L7L7L7L7Lr^   rF   link_countscomment_countprogressr   warningsr<   c                    g | ]
}|d          S )r   r   r   s     r!   r   zget_board.<locals>.<listcomp>  s,     
 
 
 hK
 
 
r^   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
 
 
r^   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  r^   )r   tenants	assigneeslatest_event_idnow)r3   r:   r   
list_tasksr   r   r   r   fetchonerD   latest_summariesr   rW   _CARD_SUMMARY_PREVIEW_CHARSrR   r   r   r   keysinttimeclose)r   r   r#   r   r   rz   r   r   rowcomment_countsr   pdiagnostics_per_taskr   summary_mapr   fullpreviewrQ   r   colr   r   r   s                          @r!   	get_boardr   `  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                   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             t          ||           d t          j        || ||          D             d|                                 S # |                                 w xY w)Nr5   r&   zDrun_state_type and run_state_name must be passed together or omittedr'   )r   rr   z,run_state_type must be 'status' or 'outcome'r*   task 
 not foundrF   r   r   r   c                ,    g | ]}t          |          S r   )re   r   s     r!   r   zget_task.<locals>.<listcomp>  s     ZZZaq))ZZZr^   c                ,    g | ]}t          |          S r   )r]   )r   es     r!   r   zget_task.<locals>.<listcomp>  s    TTT!{1~~TTTr^   c                ,    g | ]}t          |          S r   )rw   r   s     r!   r   zget_task.<locals>.<listcomp>  s.        !  r^   )
state_type
state_name)rH   commentseventslinksruns)r3   r:   r   r   get_taskrG   rR   r   r   r   list_commentslist_eventsr   	list_runsr   )
rX   r#   r   r   rz   rH   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g.. ",--	    
 
  	



s   D4E* *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S )CreateTaskBodyr   titleNr   rd   r   r   r   r   priorityscratchworkspace_kindworkspace_path)default_factoryrC   r   Fr   r;   idempotency_keyOptional[int]rn   r}   skills)__name__
__module____qualname____annotations__rd   r   r   r  r  r  r   listr   r;   r  rn   r  r   r^   r!   r  r  )  s         JJJD"H"""" F    H#N####$(N((((t444G4444F%)O)))))-----"&F&&&&&&r^   r  z/tasksrZ   c                   t          |          }t          |          }	 t          j        || j        | j        | j        d| j        | j        | j	        | j
        | j        | j        | j        | j        | j                  }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 # t.          $ r#}	t1          d	t3          |	          
          d }	~	ww xY w# |                                 w xY w)Nr5   	dashboard)r  rd   r   
created_byr  r  r   r  r   r;   r  rn   r  rH   r>   r   )_check_dispatcher_presencer8   r&   r'   )r3   r:   r   create_taskr  rd   r   r  r  r   r  r   r;   r  rn   r  r  rR   r   hermes_cli.kanbanr(  r   r   r.   r   r   )
rZ   r#   rz   rX   rH   rd   r(  r?   messager  s
             r!   r)  r)  8  s   5!!EuD%'-%""1"1>%O>#3 ' ;>
 
 
  !$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 
C(%D 'C((D 
D.D))D..D1 1E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   r  r  r  rd   resultblock_reasonrs   r   rt   )r   r!  r"  r   r#  r   r  r  rd   r.  r/  rs   rt   r   r^   r!   r-  r-  h  s          F    "H"""""H""""ED F    "&L&&&& "G!!!!#H######r^   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)%Nr5   r*   r  r  r'     ztask not foundTrB   r.  rs   rt   r@   reasonr=   r>   r@   r=   r   r?   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  z (rW   z	, status=r   r   Nr   r   r   s     r!   	<genexpr>zupdate_task.<locals>.<genexpr>  s[       * * !  !zOOqwOO8OOO* * * * * *r^   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, ?)rH   )#r3   r:   r   r  r   r   assign_taskRuntimeErrorr   r   complete_taskr.  rs   rt   
block_taskr/  schedule_taskunblock_task_set_status_directarchive_task_parents_blocking_readyr   r  	write_txnr   r   jsondumpsr   r  rd   stripr   rR   r   )rX   rZ   r#   rz   rH   okr  scurrentblockersnamessetsvalsupdateds                 r!   update_taskrQ  w  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)Nr5   r*   r  r  r'   T)deletedrX   )r3   r:   r   delete_taskr   r   )rX   r#   rz   rI  s       r!   rT  rT    s    5!!EuD"411 	UC8S8S8S8STTTTG44



s   0A& &A<r$  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 )rW   r  r   )rW   r  r   r   r   s     r!   r   z+_parents_blocking_ready.<locals>.<listcomp>  s>        w7q{CC  r^   )r   r   )rz   rX   r   s      r!   rD  rD    sR     <<	6 

	 
 hjj 	    r^   
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 = ?NFr>   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   rB   Nr   r8  s     r!   r9  z%_set_status_direct.<locals>.<genexpr>6  s<       + +*+(v%+ + + + + +r^   r   r?   >   rB   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))rr   r   rs   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   r4  parent>   rB   r>   T)r   rE  r   r   r   allrowcount_end_runrF  rG  r   r   recompute_ready)rz   rX   rW  prevparent_statuseswas_runningreopening_satisfied_parentcurr\   r   r   demoteds               r!   rB  rB    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 )CommentBodyr   rd   r&  r   rc   N)r   r!  r"  r#  rc   r   r^   r!   ri  ri    s,         III'F''''''r^   ri  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'   r5   r*   r  r  r&  )rc   rd   rI  T)
rd   rH  r   r3   r:   r   r  add_commentrc   r   )rX   rZ   r#   rz   s       r!   rk  rk    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 )LinkBodyr   r   r   N)r   r!  r"  r#  r   r^   r!   rm  rm    s"         NNNMMMMMr^   rm  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)Nr5   rI  Tr&   r'   )
r3   r:   r   
link_tasksr   r   r   r.   r   r   )rZ   r#   rz   r  s       r!   add_linkrp    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)Nr5   rI  )r3   r:   r   unlink_tasksr   r   )r   r   r#   rz   rI  s        r!   delete_linkrs    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 )BulkTaskBodyrC   idsNr   r   r   r  r  Fr   archiver.  rs   r   rt   reclaim_first)r   r!  r"  r#  r   r   r  rw  r.  rs   rt   rx  r   r^   r!   ru  ru    s         NNN F    "H"""""H""""G F    !G!!!!#H####Mr^   ru  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
/
/
/r^   r&   zids is requiredr'   r5   T)rW   rI  NFz	not found)rI  ru   zarchive refusedrB   r2  r@   r>   r5  r?   r6  r=   >   r<   r;   zunknown status ztransition to z refused)rx  zassign refusedr:  r;  r  results)"rv  r   r3   r:   r   r  updater   rw  rC  r   r>  r.  rs   rt   r?  rA  rB  r@  r   rx  reassign_taskr<  r=  r   r  rE  r   r   rF  rG  r   r   r   )rZ   r#   rv  r|  rz   r   entryrH   rJ  rI  rf  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.
    r5   Nr   r   )r   r   c                d    g | ],}t          j        |                    d                     *|-S )r   )r   severity_at_or_abover   )r   rQ   r   s     r!   r   z$list_diagnostics.<locals>.<listcomp>O  s8    ^^^a)@zARART\)])]^^^^r^   r   r   c                     i | ]}|d          |S r   r   r   s     r!   r   z$list_diagnostics.<locals>.<dictcomp>Z  s.     
 
 
 dGQ
 
 
r^   z;SELECT id, title, status, assignee FROM tasks WHERE id IN (r   r  r   r   )rX   
task_titletask_statustask_assigneer   r   c                    i | ]\  }}||	S r   r   )r   r{  rJ  s      r!   r   z$list_diagnostics.<locals>.<dictcomp>n  s    >>>DAq1a>>>r^   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_keyo  sS    m$Q'CSWWZ00"555''.)).Q/ r^   keyc              3  @   K   | ]}t          |d                    V  dS )r   N)r   r   s     r!   r9  z#list_diagnostics.<locals>.<genexpr>y  s/      <<1Q}-..<<<<<<r^   )r3   r:   r   r   itemsr$  r   r   r   r   r   r   r   r   r   r   	enumeratesortsum)r#   r   rz   diags_by_taskfilteredr   dlkeeprv  r   r   r   rf   r   r  r  s    `             @r!   list_diagnosticsr  2  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.
    r5   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\   rX   r  r  r  ri   rm   rp   rk   rl   ro   rn   )r\   rX   r  r  r  ri   rm   rp   rk   rl   ro   rn   r   )r   r   s     r!   r   z'list_active_workers.<locals>.<listcomp>  s     
 
 
  h-y>!,/"=1!$_!5y>!,/!,/!,/!$_!5%()<%='*+@'A 
 
 
r^   )workersr   
checked_at)r3   r:   r   r   r   r   r   r   )r#   rz   r   r  s       r!   list_active_workersr    s     5!!EuD+||
 
, (**- 	.
 
 
 
 
" #S\\TY[[IYIYZZ



s   A#B B/z/runs/{run_id}r\   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.
    r5   Nr*   run r  r'   run)r3   r:   r   get_runr   rw   r   )r\   r#   rz   rf   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"``.
    r5   Nr*   r  r  r'   Fzrun already ended)r\   aliver4  zno worker_pid recordedzpsutil not available)r\   r  r   r4  )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   ru   )r3   r:   r   r  r   r   rq   rm   _psutilProcessas_dictr  AttributeErrorr   rssvmsNoSuchProcessAccessDenied)	r\   r#   rz   rf   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   r4  r   r!  r"  r4  r#  r   r^   r!   r  r  !  #          F      r^   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.
    r5   Nr*   r  r  r'   r1  z already endedr3  zcannot terminate run z: task z$ is no longer in a reclaimable stateT)rI  r\   rX   )
r3   r:   r   r  r   rq   reclaim_taskrX   r4  r   )r\   rZ   r#   rz   rf   rI  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   r4  r  r   r^   r!   r  r  Y  r  r^   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 ...``.
    r5   r3  r1  zcannot reclaim z7: not in a claimable state (not running, or unknown id)r'   T)rI  rX   )r3   r:   r   r  r4  r   r   rX   rZ   r#   rz   rI  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   rc   )r   r!  r"  __doc__rc   r#  r   r^   r!   r  r  {  s/         ( ( !F      r^   r  z/tasks/{task_id}/specifyc                   t          |          }t          j                            d          }	 |pt          j        t          j        d<   ddlm} |                    | |j	        pd          }|!t          j        
                    dd           nG|t          j        d<   n7# |!t          j        
                    dd           n|t          j        d<   w xY w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``.
    HERMES_KANBAN_BOARDr   )kanban_specifyNrc   )rI  rX   r4  	new_title)r3   osenvironr   r   r/   r   r  specify_taskrc   popr   rI  rX   r4  r  )rX   rZ   r#   prev_envr  rr   s         r!   specify_task_endpointr    s   & 5!!E z~~344H9,1,LY5L
() 	.----- --N*d . 
 

 JNN0$777708BJ,-- JNN0$777708BJ,-8888 7:?.&	     ?B" "4Cc                  :    e Zd ZU dZded<   dZded<   dZded<   dS )ReassignBodyNr   ri   Fr   rx  r4  )r   r!  r"  ri   r#  rx  r4  r   r^   r!   r  r    sE         !G!!!!M F      r^   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]``.
    r5   N)rx  r4  r1  zcannot reassign zS: unknown id, or still running (pass reclaim_first=true to release the claim first)r'   T)rI  rX   r   )
r3   r:   r   r~  ri   r   rx  r4  r   r   r  s        r!   reassign_task_endpointr    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_configr    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 )Nr  r   rv   s    r!   <lambda>z+_configured_home_channels.<locals>.<lambda>"  s
    a
m r^   r  )gateway.configr  r   	platformsr  home_channelr   valuer  r  r   r  )r  gw_cfgr.  r  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.profilesr  r   r  s    r!   _active_profile_namer  &  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.r  r  r%   r  )r   r   )r  r  s     r!   _home_sub_matchesr  /  s     	
tJ// 	L	2&&''3tI+?+??	L$$*++s43D3J/K/KKr^   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.
    r5   r  r%   r  r  
subscribedhome_channels)r  setr3   r:   r   list_notify_subsr   r   r   addr   )
rX   r#   homessubscribed_homesrz   subsr  r  r.  r  s
             r!   get_home_channelsr  8  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}r  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 r  Nr   r   hr  s     r!   r9  z!subscribe_home.<locals>.<genexpr>b  1      ??qQz]h%>%>%>%>%>%>??r^   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'   r5   r  r  r  r  )rX   r  r  r  notifier_profileTrI  rX   r  )
r  nextr   r3   r:   r   r  add_notify_subr  r   )rX   r  r#   r  r  rz   rH   s    `     r!   subscribe_homer  Z  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 r
  r   r  s     r!   r9  z#unsubscribe_home.<locals>.<genexpr>  r  r^   Nr*   r  .r'   r5   r  r  )rX   r  r  r  Tr  )r  r  r   r3   r:   r   remove_notify_subr   )rX   r  r#   r  r  rz   s    `    r!   unsubscribe_homer  }  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.
    r5   )r3   r:   r   board_statsr   r#   rz   s     r!   	get_statsr    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.
    r5   r   )r3   r:   r   known_assigneesr   r  s     r!   get_assigneesr    sZ     5!!EuDY6t<<=



   A A!z/tasks/{task_id}/logr   i )geletailr  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.
    r5   Nr*   r  r  r'   )
tail_bytesr#   r   r%   )rX   pathexists
size_bytescontent	truncated)r3   r:   r   r  r   r   read_worker_logworker_log_pathr'  statst_sizer   r   )rX   r#  r#   rz   rH   r)  log_pathsizes           r!   get_task_logr1    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$;//  r   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)Nr5   )r5  	max_spawnr#   r.  )r3   r:   r   dispatch_oncer   r   	TypeErrorr   )r5  r6  r#   rz   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 )CreateBoardBodyr   slugNr   r   r   iconcolorFr   switch)	r   r!  r"  r#  r   r   r?  r@  rA  r   r^   r!   r=  r=     sp         IIID!%K%%%%DEFr^   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@  )r   r!  r"  r   r#  r   r?  r@  r   r^   r!   rC  rC  	  sV         D!%K%%%%DEr^   rC  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.r5   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;;;r^   )r   kanban_db_pathr'  r9   r   r   r   r   )r>  r&  rz   r   s       r!   _board_countsrH    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   )boardsrK  )r   list_boardsget_current_boardrH  r  values)r   rL  rK  bs       r!   rM  rM  "  s     "4DEEEF)++G / /V9/,#AfI..(8++--..'

111r^   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#   rK  )r   create_boardr>  r   r   r?  r@  r.   r   r   rA  set_current_boardrN  )rZ   metar2   s      r!   create_board_endpointrV  .  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,   rR  r#   )r   r-   r.   r   r   r0   write_board_metadatar   r   r?  r@  )r>  rZ   r1   r2   rU  s        r!   rename_boardrY  C  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.)rw  r&   r'   N)r.  rK  )r   remove_boardr.   r   r   rN  )r>  r[  resr2   s       r!   delete_boardr_  V  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,   rK  )r   r-   r.   r   r   r0   rT  )r>  r1   r2   s      r!   switch_boardra  `  s    >066 > > >CHH====> V/77 V4TT4T4T4TUUUU'''vrZ  g333333?c                      e Zd ZU dZded<   dS )DescribeBodyNr   r   )r   r!  r"  r   r#  r   r^   r!   rc  rc    s#         !%K%%%%%%r^   rc  c                      e Zd ZU dZded<   dS )DescribeAutoBodyFr   	overwriteN)r   r!  r"  rf  r#  r   r^   r!   re  re    s#         Ir^   re  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   profiles  zfailed to list profiles: r'   Nri  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   rl  rm  rn  r   ro  r   rp  r8  s     r!   r   z'list_profile_roster.<locals>.<listcomp>  s     
 
 
  "1<00BJ," }2$();$<$<"1=#5A66 
 
 
r^   )r   ri  list_profilesr   r   )profiles_modri  r2   s      r!   list_profile_rosterrs    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   rh  r  )get_hermes_home)Pathr*   	profile 'z' not foundr'   r%   F)r   ro  rj  zfailed to update profile: NT)rI  ri   r   )r   ri  normalize_profile_namehermes_constantsrv  pathlibrw  get_profile_diris_dirr   r   rH  write_profile_metar   )	rt  rZ   rr  canonrv  _Pathprofile_dirtextr2   s	            r!   update_profile_descriptionr    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)rf  rj  zdescriber crashed: r'   N)rI  ri   r4  r   )r   r  describe_profiler   rf  r   r   rI  rt  r4  r   )rt  rZ   r  rr   r2   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   rc   )r   r!  r"  rc   r#  r   r^   r!   r  r    r  r^   r  z/tasks/{task_id}/decomposec                8   t          |          }t          j                            d          }	 |pt          j        t          j        d<   ddlm} |                    | |j	        pd          }|!t          j        
                    dd           nG|t          j        d<   n7# |!t          j        
                    dd           n|t          j        d<   w xY w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  r   )kanban_decomposeNr  )rI  rX   r4  fanout	child_idsr  )r3   r  r  r   r   r/   r   r  decompose_taskrc   r  r   rI  rX   r4  r  r  r  )rX   rZ   r#   r  r  rr   s         r!   decompose_task_endpointr    s,   " 5!!Ez~~344H9,1,LY5L
()//////"11N*d 2 
 

 JNN0$777708BJ,-- JNN0$777708BJ,-8888 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)r   r!  r"  r  r#  r  r  r  r   r^   r!   r  r    sY         *.....&*****%)N)))),0000000r^   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  rh  r  )r  r  r  r  resolved_orchestrator_profileresolved_default_assigneeactive_profile)r   r   r   
isinstancer  r   rH  r   r   ri  r  profile_exists)r   r  
kanban_cfgexplicit_orchexplicit_defaultr  r  resolved_orchresolved_defaultrr  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_configrj  zfailed to load config: r'   Nr  rh  r%   r&   rx  z' does not existr  r  r  r  zfailed to save config: )r   r   r  r   r   r   r  r  r   ri  r  rH  r  r  r  r   r  r  )rZ   r   r  r  r2   kanban_sectionrr  r   s           r!   set_orchestration_settingsr  L  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/eventswsr
   c                  K   | j                             d          }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token)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)
Nr5   zmSELECT id, task_id, run_id, kind, payload, created_at FROM task_events WHERE id > ? ORDER BY id ASC LIMIT 200rZ   rW   rX   r\   rY   r[   )rW   rX   r\   rY   rZ   r[   )	r   r9   r   r   rF  loadsr   r   r   )r  rz   r   r   
new_cursorrf   rZ   ws_boards          r!   
_fetch_newz!stream_events.<locals>._fetch_new  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  )query_paramsr   r"   r   http_statusWS_1008_POLICY_VIOLATIONacceptr   r.   r   r-   asyncio	to_thread	send_jsonsleep_EVENT_POLL_SECONDSr   CancelledErrorr   r7   r8   )	r  r  	since_rawr  ws_board_rawr  r  r2   r  s	           @r!   stream_eventsr    s     
 O((E5!! 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   0E B E B+(E *B++E 	C" !E "C1.E 0C11A(E 
G&G7	G GF76G7
GGGGG)r   r   r   r   )r#   r   r   r   )N)r#   r   )rH   rI   rG   r   r   rJ   )rS   rT   r   rJ   )r_   r`   r   rJ   )rf   rg   r   rJ   )rz   r{   r|   r}   r   r~   )r   r   r   r   )rz   r{   rX   r   r   r   )
r   r   r   r   r#   r   r   r   r   r   )rX   r   r#   r   r   r   r   r   )rZ   r  r#   r   )rX   r   rZ   r-  r#   r   )rX   r   r#   r   )rz   r{   rX   r   r   r$  )rz   r{   rX   r   rW  r   r   r   )rX   r   rZ   ri  r#   r   )rZ   rm  r#   r   )r   r   r   r   r#   r   )rZ   ru  r#   r   )r#   r   r   r   )r\   r   r#   r   )r\   r   rZ   r  r#   r   )rX   r   rZ   r  r#   r   )rX   r   rZ   r  r#   r   )rX   r   rZ   r  r#   r   )r   r   )r   r   )r  r  r  r  r   r   )rX   r   r#   r   )rX   r   r  r   r#   r   )rX   r   r#  r  r#   r   )r5  r   r6  r   r#   r   )r>  r   r   rD  )r   r   )rZ   r=  )r>  r   rZ   rC  )r>  r   r[  r   )r>  r   )rt  r   rZ   rc  )rt  r   rZ   re  )rX   r   rZ   r  r#   r   )rZ   r  )r  r
   )tr  
__future__r   r  r   rF  loggingr  sqlite3r   dataclassesr   typingr   r   fastapir   r   r	   r
   r   r   r  pydanticr   r   r   r   r   r   	getLoggerr   r7   routerr"   r3   r:   rD   r#  r   rR   r]   re   rw   _WARNING_EVENT_KINDSr   r   r   r   r   r  r  postr)  r-  patchrQ  r[  rT  rD  rB  ri  rk  rm  rp  rs  ru  r  r  psutilr  ImportErrorr  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r  r1  r;  r=  rC  rH  rM  rV  rY  r_  ra  r  rc  re  rs  r  r  r  r  r  r  putr  	websocketr  r   r^   r!   <module>r     s;  ! ! !F # " " " " "     				                         j j j j j j j j j j j j j j j j % % % % % % % %             / / / / / /g!!	= = = =,   ,* * * * *>      
 "  %)     *         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% % %6 6 6 6  6z' ' ' ' 'Y ' ' ' X@Ed ( ( ( ( (^$ $ $ $ $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;;+ + + + )(+\! ! ! ! !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;;& & & & +*&\1 1 1 1 1	 1 1 1 ' ' 'T C( C( C( C(L )I I I I I Is   
L LL