
    (Gj                   R   U d Z ddlmZ ddlZddlZddlZddlmZm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  ej        e          Zd	Zd
ZddZddZddZddZddZddZddZdddZ e ddh          Z!dd Z"d!Z#d"a$d#e%d$<   dd%Z&dd(Z'dd*Z(d+d,dd1Z)dd3Z*dd5Z+dd6Z,dd7Z-dd8Z.dd9Z/dd:Z0dd;Z1dd<Z2dd>Z3dd?Z4dd@Z5dAZ6dBZ7ddDZ8dEdFdGdHe6dI e8            dJg dKdLZ9dMdNdGdHdOdIdHg dPdQdRdHdSdIdTdUdIdVdWdI e8            dXg dKdLZ:dYdZdGdHe6dIdHd[dIdGd\dIdHd]dId^d_dHid`dad^d_dHidbda e8            dcg dKdLZ;dddedGdHe6dIdHdfdIdHg dgdhdR e8            didjgdKdLZ<dkdldGdHe6dIdHdmdI e8            dng dKdLZ=dodpdGdHdqdIdHdrdI e8            dsddtgdKdLZ>dudvdGi dwdHdxdIdydHdzdIdtdHd{dId|d^d_dHid}dad~dHddIddVddIddHg dddRddHddIddHddIddTddIddHddIddVddIddHddgddRdd^d_dHiddaddTddIddVddId e8            dwdygdKdLZ?dddGdHddI e8            dJdgdKdLZ@dddGdHddIdHddI e8            dddgdKdLZA ejB        dEde9e,ed            ejB        dMde:e-ed            ejB        dYde;e.ed            ejB        ddde<e/ed            ejB        dkde=e0ed            ejB        dode>e1ed            ejB        dude?e2ed            ejB        dde@e4ed            ejB        ddeAe5ed           dS )uz  Kanban tools — structured tool-call surface for worker + orchestrator agents.

These tools are registered into the model's schema when the agent is
running under the dispatcher (env var ``HERMES_KANBAN_TASK`` set) or when
the active profile explicitly enables the ``kanban`` toolset for
orchestrator work. A normal ``hermes chat`` session still sees **zero**
kanban tools in its schema unless configured.

Why tools instead of just shelling out to ``hermes kanban``?

1. **Backend portability.** A worker whose terminal tool points at Docker
   / Modal / Singularity / SSH would run ``hermes kanban complete …``
   inside the container, where ``hermes`` isn't installed and the DB
   isn't mounted. Tools run in the agent's Python process, so they
   always reach ``~/.hermes/kanban.db`` regardless of terminal backend.

2. **No shell-quoting footguns.** Passing ``--metadata '{"x": [...]}'``
   through shlex+argparse is fragile. Structured tool args skip it.

3. **Better errors.** Tool-call failures return structured JSON the
   model can reason about, not stderr strings it has to parse.

Humans continue to use the CLI (``hermes kanban …``), the dashboard
(``hermes dashboard``), and the slash command (``/kanban …``) — all
three bypass the agent entirely. The tools are for dispatcher-spawned
worker handoffs and for configured orchestrator profiles that route work
through the board.
    )annotationsN)AnyOptional)redact_sensitive_text)
judge_goal)registry
tool_error)cfg_getload_config2      returnboolc                 z    	 ddl m}   |             }|                    dg           }d|v S # t          $ r Y dS w xY w)Nr   )r   toolsetskanbanF)hermes_cli.configr   get	Exception)r   cfgr   s      8/home/rurouni/.hermes/hermes-agent/tools/kanban_tools.py_profile_has_kanban_toolsetr   4   sd    111111kmm77:r**8##   uus   ), 
::c                 `    t           j                            d          rdS t                      S )a  Task-lifecycle tools are available when:

    1. ``HERMES_KANBAN_TASK`` is set (dispatcher-spawned worker), OR
    2. The current profile has ``kanban`` in its toolsets config
       (orchestrator profiles like techlead that route work via Kanban).

    Humans running ``hermes chat`` without the kanban toolset see zero
    kanban tools. Workers spawned by the kanban dispatcher (gateway-
    embedded by default) and orchestrator profiles with the kanban
    toolset enabled see the Kanban lifecycle tool surface.
    HERMES_KANBAN_TASKTosenvironr   r        r   _check_kanban_moder    A   s,     
z~~*++ t&(((r   c                 `    t           j                            d          rdS t                      S )a  Board-routing tools (kanban_list, kanban_unblock) are intentionally
    hidden from task workers.

    Dispatcher-spawned workers should close their own task via the
    lifecycle tools (complete/block/heartbeat), not enumerate or unblock
    board state. Profiles that explicitly opt into the kanban toolset
    and are NOT scoped to a single task are the orchestrator surface.
    r   Fr   r   r   r   _check_kanban_orchestrator_moder"   R   s,     
z~~*++ u&(((r   argOptional[str]c                P    | r| S t           j                            d          }|pdS )zGResolve ``task_id`` arg or fall back to the env var the dispatcher set.r   N)r   r   r   )r#   env_tids     r   _default_task_idr'   d   s-    
 
jnn122G?dr   task_idstrOptional[int]c                    t           j                            d          | k    rdS t           j                            d          }|sdS 	 t          |          S # t          $ r Y dS w xY w)zDReturn this worker's dispatcher run id when it is scoped to task_id.r   NHERMES_KANBAN_RUN_ID)r   r   r   int
ValueError)r(   raws     r   _worker_run_idr0   l   st    	z~~*++w66t
*../
0
0C t3xx   tts   
A 
A'&A'metadataOptional[dict]c                    t           j                            d          | k    r|S t           j                            d          }|s|S t          |pi           }||d<   |S )zBAdd trusted worker session id metadata for this worker's own task.r   HERMES_SESSION_IDworker_session_id)r   r   r   dict)r(   r1   
session_idstampeds       r   _stamp_worker_session_metadatar9   y   se     
z~~*++w66 344J 8>r""G#-G Nr   tidc                    t           j                            d          }|sdS | |k    rt          d| d|  d          S dS )u_  Reject worker-driven destructive calls on foreign task IDs.

    A process spawned by the dispatcher has ``HERMES_KANBAN_TASK`` set
    to its own task id. Tools like ``kanban_complete`` / ``kanban_block``
    / ``kanban_heartbeat`` mutate run-lifecycle state, so a buggy or
    prompt-injected worker that passed an explicit ``task_id`` for some
    other task could corrupt sibling or cross-tenant runs (see #19534).

    Orchestrator profiles (kanban toolset enabled but **no**
    ``HERMES_KANBAN_TASK`` in env) aren't subject to this check — their
    job is routing, and they sometimes legitimately close out child
    tasks or reopen blocked ones. Workers are narrowly scoped to their
    one task.

    Returns ``None`` when the call is allowed, or a tool-error string
    when it must be rejected. Callers should ``return`` the error
    verbatim.
    r   Nzworker is scoped to task z; refusing to mutate zf. Use kanban_comment to hand off information to other tasks, or kanban_create to spawn follow-up work.r   r   r   r	   )r:   r&   s     r   _enforce_worker_task_ownershipr=      sp    & jnn122G t
g~~@ @ @@ @ @
 
 	

 4r   boardc                >    ddl m} ||                    |           fS )u  Import + connect lazily so the module imports cleanly in non-kanban
    contexts (e.g. test rigs that import every tool module).

    When ``board`` is provided it's forwarded to :func:`kb.connect`, which
    routes the connection to that board's sqlite file. ``None`` (the
    default) preserves the legacy resolution chain
    (``HERMES_KANBAN_DB`` → ``HERMES_KANBAN_BOARD`` env → current symlink
    → ``default``). Per-tool ``board`` lets a Telegram-side agent override
    the env-pinned active board without restarting Hermes.
    r   	kanban_dbr>   )
hermes_clirA   connect)r>   kbs     r   _connectrF      s/     +*****rzzz&&&&r   
dependencyneeds_inputc                 v    	 ddl m}   | d          \  }}n# t          $ r Y dS w xY w|duot          |          S )ap  True when an auxiliary client is configured for the goal judge.

    ``judge_goal`` is fail-open at the source: when no auxiliary model can
    be reached it returns a ``"continue"`` verdict that is indistinguishable
    from a real "not done yet" judgment. The completion gate must not treat
    that as a rejection, or an unconfigured/degraded auxiliary model would
    wedge every ``goal_mode`` worker (it could never close its own task).

    So we probe availability first and only enforce the gate when a judge is
    actually reachable. This mirrors the same client lookup ``judge_goal``
    performs internally.
    r   )get_text_auxiliary_client
goal_judgeFN)agent.auxiliary_clientrJ   r   r   )rJ   clientmodels      r   _goal_judge_availablerO      sg    DDDDDD11,??   uu-$u++-s    
%%g      N@g        float_auto_heartbeat_last_attemptc                    t           j                            d          } | sdS ddl}|                                }|t
          z
  t          k     rdS |a	 t                      \  }}	 t           j                            d          }	 |                    || |           n,# t          $ r t                              dd	           Y nw xY wt           j                            d
          }	 |rt          |          nd}n# t          t          f$ r d}Y nw xY w	 |                    || d|           n,# t          $ r t                              dd	           Y nw xY w	 |                                 n:# t          $ r Y n.w xY w# 	 |                                 w # t          $ r Y w w xY wxY wdS # t          $ r  t                              dd	           Y dS w xY w)u  Best-effort: extend the kanban claim + bump board heartbeat for the
    current dispatcher-spawned worker, using identity from env vars.

    Returns True if a write was attempted (whether or not it succeeded);
    False if the call was skipped (not a kanban worker, rate-limited, or
    swallowed exception). The boolean is informational — callers should
    not branch on it.

    Identity comes from:
      * ``HERMES_KANBAN_TASK`` — task id (required; absence means no-op)
      * ``HERMES_KANBAN_RUN_ID`` — pins the run row so we don't heartbeat
        a stale run that may have already been reclaimed
      * ``HERMES_KANBAN_CLAIM_LOCK`` — claim lock for ``heartbeat_claim``;
        falls back to the default ``_claimer_id()`` for locally-driven
        workers that never went through the dispatcher path

    Rate-limited via the module-level ``_auto_heartbeat_last_attempt``
    timestamp (monotonic clock); not thread-safe in the strict sense, but
    the worst case is one extra DB write per race, which is harmless.
    r   Fr   NHERMES_KANBAN_CLAIM_LOCKclaimerz&auto-heartbeat: heartbeat_claim failedTexc_infor,   noteexpected_run_idz'auto-heartbeat: heartbeat_worker failedzauto-heartbeat: bridge failed)r   r   r   time	monotonicrQ   $_AUTO_HEARTBEAT_MIN_INTERVAL_SECONDSrF   heartbeat_claimr   loggerdebugr-   	TypeErrorr.   heartbeat_workerclose)r:   _timenowrE   conn
claim_lock
run_id_rawrun_ids           r   !heartbeat_current_worker_from_envrj      sN   , *..-
.
.C u
//

C**.RRRu#& ::D	(BCCJV""4j"AAAA V V VEPTUUUUUV(>??J,6@ZDz*   W##D#D&#QQQQ W W WFQUVVVVVW

   

   t   4tDDDuus   F1 &F B F &CF C"F +C? >F ?DF DF D3 2F 3&EF EF  E5 4F1 5
F?F1 FF1 F,FF,
F)&F,(F))F,,F1 1&GGfieldsr   c                 2    t          j        ddi|           S )NokT)jsondumps)rk   s    r   _okrp   "  s    :tT,V,---r   valuec                    | dS t          |                                           }|r|                                dv rdS |S )zANormalize CLI-compatible assignee sentinels for the tool surface.N>   -nonenull)r)   striplower)rq   texts     r   _normalize_profilery   &  sH    }tu::D 4::<<#888tKr   Fdefaultargsr6   namer{   c                   |                      |          }||d fS t          |t                    r|d fS t          |                                                                          }|dv rdS |dv rdS || dfS )N>   1yestrue)TN>   0nofalse)FNz$ must be a boolean or 'true'/'false')r   
isinstancer   r)   rv   rw   )r|   r}   r{   rq   rx   s        r   _parse_bool_argr   0  s    HHTNNE}}% d{u::##%%D###z###{tAAAAAr   	tool_namec                h    t           j                            d          rt          |  d          S dS )a  Belt-and-suspenders runtime guard for orchestrator-only handlers.

    The check_fn (`_check_kanban_orchestrator_mode`) keeps these tools
    out of the worker schema entirely, but in case a stale registration
    or test harness routes a worker to one of them anyway, return a
    structured tool_error so the model gets a clear refusal instead of
    silently mutating board state from a worker context.
    r   z is orchestrator-only; dispatcher-spawned workers must use kanban_complete, kanban_block, kanban_heartbeat, or kanban_comment for their assigned task.Nr<   )r   s    r   _require_orchestrator_toolr   >  sF     
z~~*++ 
 6 6 6
 
 	

 4r   dict[str, Any]c                   |                      ||j                  }|                     ||j                  }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|d|t#          |          t#          |          dS )z+Compact task shape for board-listing tools.idtitleassigneestatusprioritytenantworkspace_kindworkspace_path
project_id
created_by
created_at
started_atcompleted_atcurrent_run_idmodel_overrideparentschildren)parent_countchild_count)
parent_idsr   	child_idsr   r   r   r   r   r   r   r   r   r   r   r   r   r   len)rE   rf   taskr   r   s        r   _task_summary_dictr   P  sD   mmD$'**G||D$'**Hdg 	DM 	$+	
 	DM 	$+ 	$- 	$- 	do 	do 	do 	do 	) 	$- 	$-  	7!" 	H#$ G8}}'   r   c                   t          |                     d                    }|st          d          S |                     d          }	 t          |          \  }}	 |                    ||          }|'t          d| d          |                                 S |                    ||          }|                    ||          }|                    ||          }	|	                    ||          }
|
                    ||          }d }d	 t          j         ||          |
|d
 |D             d |dd         D             fd|	D             |                    ||          d          |                                 S # |                                 w xY w# t          $ r}t          d|           cY d}~S d}~wt          $ r6}t                               d           t          d|           cY d}~S d}~ww xY w)zsRead a task's full state: task row, parents, children, comments,
    runs (attempt history), and the last N events.r(   :task_id is required (or set HERMES_KANBAN_TASK in the env)r>   rB   Nztask z
 not foundc                   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 )Nr   r   bodyr   r   r   r   r   r   r   r   r   r   resultr   r   )r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   )ts    r   
_task_dictz _handle_show.<locals>._task_dict  s    !$ '28!&
,4ah ah )3AJ %a&6	
 %a&6 !!, 1=al !!, #AN ah %a&6 %a&6 r   c           
     t    | j         | j        | j        | j        | j        | j        | j        | j        | j        d	S )N	r   profiler   outcomesummaryerrorr1   r   ended_atr   )rs    r   	_run_dictz_handle_show.<locals>._run_dict  s:    $19h19 y17 !
"#,AJ  r   c                8    g | ]}|j         |j        |j        d S )authorr   r   r   .0cs     r   
<listcomp>z _handle_show.<locals>.<listcomp>  s?          !x#$<1 1  r   c                D    g | ]}|j         |j        |j        |j        d S )kindpayloadr   ri   r   )r   es     r   r   z _handle_show.<locals>.<listcomp>  sE         V	#$<18E E  r   ic                &    g | ]} |          S r   r   )r   r   r   s     r   r   z _handle_show.<locals>.<listcomp>  s!    444!1444r   )r   r   r   commentseventsrunsworker_contextzkanban_show: zkanban_show failed)r'   r   r	   rF   get_taskrc   list_commentslist_events	list_runsr   r   rn   ro   build_worker_contextr.   r   r_   	exception)r|   kwr:   r>   rE   rf   r   r   r   r   r   r   r   r   r   s                 @r   _handle_showr   o  s    488I..
/
/C 
H
 
 	
 HHWE@/%(((D8	;;tS))D|!"9#"9"9"9::j JJLLLLi ''c22H^^D#..F<<c**DmmD#..G||D#..H     :"
4(("$  &  
  $CDD\  
 5444t444
 #%"9"9$"D"D'   , JJLLLLDJJLLLL / / /-!--........ / / /-...-!--......../sU   F  *F 	F  CF 2F  FF   
H*G;HH+G?9H?Hc           
        t          d          }|r|S |                     d          }|                     d          }|                     d          }t          | d          \  }}|rt          |          S |                     d          }|t          }	 t          |          }n&# t          t          f$ r t          d          cY S w xY w|d	k     rt          d
          S |t          k    rt          dt                     S |                     d          }		 t          |	          \  	 
                              }
                    |||||d	z             }t          |          |k    }|d|         }t          j        fd|D             t          |          |||r#|t          k     rt          |dz  t                    nd|
d                                           S #                                  w xY w# t          $ r}t          d|           cY d}~S d}~wt"          $ r6}t$                              d           t          d|           cY d}~S d}~ww xY w)z:List task summaries with the same core filters as the CLI.kanban_listr   r   r   include_archivedlimitNzlimit must be an integer   zlimit must be >= 1zlimit must be <= r>   rB   )r   r   r   r   r   c                2    g | ]}t          |          S r   )r   )r   r   rf   rE   s     r   r   z _handle_list.<locals>.<listcomp>  s&    IIIa,Rq99IIIr      )taskscountr   	truncated
next_limitpromotedzkanban_list: zkanban_list failed)r   r   r   r	   KANBAN_LIST_DEFAULT_LIMITr-   ra   r.   KANBAN_LIST_MAX_LIMITrF   recompute_ready
list_tasksr   rn   ro   minrc   r   r_   r   )r|   r   guardr   r   r   r   
bool_errorr   r>   r   rowsr   r   r   rf   rE   s                  @@r   _handle_listr     s   &}55E xx
##HXXhFXXhF#249K#L#L j &*%%%HHWE})6E

z" 6 6 64555556qyy.///$$$E.CEEFFFHHWE#/%(((D	 ))$//H ==!!1ai !  D D		E)I%LE:IIIII5IIIU& !M%*-B%B%B 	#8999HL$
 
 
 
 JJLLLLDJJLLLL / / /-!--........ / / /-...-!--......../s[   B( ( C
CH /B)G- H -HH 
I*H'!I*'I*4+I%I*%I*c                   t          |                     d                    }|st          d          S t          |          }|r|S |                     d          }|                     d          }|                     d          }|rt	          t          |          d          }|rt	          t          |          d          }|et          |t                    rPt          j	        |          }t	          |d          }	 t          j
        |          }n# t          j        $ r Y nw xY w|                     d	          }|                     d
          }	|dt          |t
                    r|g}t          |t          t          f          s$t          dt          |          j                   S d |D             }|	Vt          |	t
                    r|	g}	t          |	t          t          f          s$t          dt          |	          j                   S d |	D             }	|	r|i }n9t          |t                    s$t          dt          |          j                   S |                    d
          }
t          |
t          t          f          r|g }t!                      }t          |
          |	z   D ]S}t          |                                          }|r.||vr*|                    |           |                    |           T||d
<   n|	|d
<   |s|st          d          S |9t          |t                    s$t          dt          |          j                   S t)          ||          }|                     d          }	 t+          |          \  }}	 |                    ||          }|r|j        rt1                      rd}d}	 t3          |j         d|j        pd                                 |p|pd                                          \  }}}n4# t8          $ r'}t:                              d|d           Y d}~nd}~ww xY w|dk    r*t          d| d| d          |                                 S 	 |                     ||||||tC          |                    }nV# |j"        $ rI}t          dd#                    |j$                   d          cY d}~|                                 S d}~ww xY w|s't          d | d!          |                                 S |%                    ||          }tM          ||r|j'        nd"          |                                 S # |                                 w xY w# tP          $ r}t          d#|           cY d}~S d}~wt8          $ r6}t:          )                    d$           t          d#|           cY d}~S d}~ww xY w)%z5Mark the current task done with a structured handoff.r(   r   r   r1   r   TforceNcreated_cards	artifactsz.created_cards must be a list of task ids, got c                    g | ]D}t          |                                          #t          |                                          ES r   r)   rv   r   s     r   r   z$_handle_complete.<locals>.<listcomp>  sI     
 
 
 SVV\\^^
FFLLNN
 
 
r   z,artifacts must be a list of file paths, got c                    g | ]D}t          |                                          #t          |                                          ES r   r   )r   ps     r   r   z$_handle_complete.<locals>.<listcomp>(  sI     
 
 
 Q
FFLLNN
 
 
r   z%metadata must be an object/dict, got z4provide at least one of: summary (preferred), resultr>   rB   done z

)goallast_responsez0goal judge check failed, allowing completion: %srV   z#Goal completion rejected by judge: z. To proceed, either: (1) provide explicit acceptance evidence in your summary matching the task's criteria, or (2) create continuation tasks with parents=[z] and keep this task alive.)r   r   r1   r   rZ   zfkanban_complete blocked: the following created_cards do not exist or were not created by this worker: z, z. Your task is still in-flight (no state change). Retry kanban_complete with the same summary/metadata and either drop these ids from created_cards, or pass created_cards=[] to skip the card-claim check entirely.zcould not complete z! (unknown id or already terminal))r(   ri   zkanban_complete: zkanban_complete failed)*r'   r   r	   r=   r   r)   r   r6   rn   ro   loadsJSONDecodeErrorlisttupletype__name__setrv   addappendr9   rF   r   	goal_moderO   r   r   r   r   r_   warningrc   complete_taskr0   HallucinatedCardsErrorjoinphantom
latest_runrp   r   r.   r   )r|   r   r:   ownership_errr   r1   r   	meta_jsonr   r   existingmergedseenitemsr>   rE   rf   r   verdictreason_	judge_excrm   hall_errrunr   s                              r   _handle_completer    s8   
488I..
/
/C 
H
 
 	
 3377M hhy!!Gxx
##HXXhF B'GDAAA @&s6{{$???
8T : :Jx((	))4@@@		z),,HH# 	 	 	D	HH_--M%%I mS)) 	,*OM-$77 	2&&/2 2  

 
$1
 
 
 i%% 	$"I)dE]33 	.	??+. .  
 
$-
 
 
	  	2$// !1H~~.1 1    ||K00H(T5M22 
2$&!$ NNY6 ) )DD		))A )Qd]]a((((.%%(1% 
v 
B
 
 	
 Jx$>$>MDNN4KMM
 
 	
 .c8<<HHHWEI3%(((DB	 ;;tS))D  +@+B+B  )3 $
AA	RAAGGII'.'>&'>B&E&E&G&G* * *&GVQQ !    NNJ!!% #         f$$%5f 5 5 KN5 5 5 T JJLLLLE%%#!7X"/$23$7$7	 &   ,    "Oyy!122O O O        JJLLLL7(  !P#PPP  JJLLLL --c**CsS+B366dCCCJJLLLLDJJLLLL 3 3 31a1122222222 3 3 312221a11222222223s   D D*)D*
V
 1U1 AP U1 
Q)QU1 QU1 .V
 )R. -U1 .
T8*S<"T#U1 'V
 <TU1 V
 -/U1 V
 1VV
 

W.V+%W.+W.8+W)#W.)W.c           	        t          |                     d                    }|st          d          S t          |          }|r|S |                     d          }|r!t	          |                                          st          d          S t          t	          |          d          }|                     d          }|                     d          }	 t          |	          \  }}|B||j        vr9|	                                 t          dt          |j                   d          S |                    ||          }	|	rL|	j        rE|t          vr<|	                                 t          dt          t                     d|d          S 	 |                    ||||t          |                    }
|
s't          d| d          |	                                 S |                    ||          }|                    ||          }t#          ||r|j        nd
|r|j        nd|          |	                                 S # |	                                 w xY w# t(          $ r}t          d|           cY d
}~S d
}~wt*          $ r6}t,                              d           t          d|           cY d
}~S d
}~ww xY w)z?Transition the task to blocked with a reason a human will read.r(   r   r  u2   reason is required — explain what input you needTr   r   r>   rB   Nzkind must be one of z (or omit it)z,goal_mode tasks can only block with kind in z (got u   ). If the task is actually finished or cannot proceed for another reason, call kanban_complete instead — the completion judge will evaluate it.)r  r   rZ   zcould not block z% (unknown id or not in running/ready)blocked)r(   ri   r   
block_kindzkanban_block: zkanban_block failed)r'   r   r	   r=   r)   rv   r   rF   VALID_BLOCK_KINDSrc   sortedr   r   _GOAL_MODE_BLOCK_ALLOWED_KINDS
block_taskr0   r   rp   r   r   r.   r   r_   r   )r|   r   r:   r   r  r   r>   rE   rf   r   rm   r  landedr   s                 r   _handle_blockr    s?   
488I..
/
/C 
H
 
 	
 3377M XXhF PV**,, PNOOO"3v;;d;;;F88FDHHWE;0%(((DB,@ @ @JJLLLRvb.B'C'CRRR   {{4%%		 :::JJLLL68996 6AE6 6 6  	c .s 3 3	   B  !&s & & &  JJLLLL --c**C [[s++F!$.svv$(.=v}}I	   JJLLLLDJJLLLL 0 0 0.1..//////// 0 0 0.///.1..////////0s]   AI! .A#I! ;I I! #AI 3I! II! !
K+J<KK+K :K Kc                `   t          |                     d                    }|st          d          S t          |          }|r|S |                     d          }|                     d          }	 t	          |          \  }}	 t
          j                            d          }|                    |||           |                    |||t          |                    }	|	s't          d	| d
          |
                                 S t          |          |
                                 S # |
                                 w xY w# t          $ r}
t          d|
           cY d}
~
S d}
~
wt          $ r6}
t                              d           t          d|
           cY d}
~
S d}
~
ww xY w)u  Signal that the worker is still alive during a long operation.

    Extends the claim TTL via ``heartbeat_claim`` AND records a heartbeat
    event via ``heartbeat_worker``. Without the ``heartbeat_claim`` half,
    a diligent worker that loops this tool while a single tool call
    blocks the agent for >DEFAULT_CLAIM_TTL_SECONDS still gets reclaimed
    by ``release_stale_claims`` — which is exactly the trap that
    ``heartbeat_claim``'s docstring warns against.
    r(   r   rY   r>   rB   rS   rT   rX   zcould not heartbeat z (unknown id or not running))r(   zkanban_heartbeat: Nzkanban_heartbeat failed)r'   r   r	   r=   rF   r   r   r^   rb   r0   rc   rp   r.   r   r_   r   )r|   r   r:   r   rY   r>   rE   rf   rg   rm   r   s              r   _handle_heartbeatr    s    488I..
/
/C 
H
 
 	
 3377M 88FDHHWE4%(((D	 (BCCJtS*===$$ .s 3 3	 %  B  !L3LLL 
 JJLLLL s###JJLLLLDJJLLLL 4 4 42q2233333333 4 4 423332q22333333334sU   2E	 A1D0 7E	 D0 E	 0EE	 	
F-E*$F-*F-7+F("F-(F-c                D   |                      d          }|st          d          S |                      d          }|r!t          |                                          st          d          S t	          t          |          d          }t
          j                             d          pd}|                      d	          }	 t          |
          \  }}	 |                    |||t          |                    }t          ||          |
                                 S # |
                                 w xY w# t          $ r}	t          d|	           cY d}	~	S d}	~	wt          $ r6}	t                              d           t          d|	           cY d}	~	S d}	~	ww xY w)z$Append a comment to a task's thread.r(   uo   task_id is required (use the current task id if that's what you mean — pulls from env but kept explicit here)r   zbody is requiredTr   HERMES_PROFILEworkerr>   rB   )r   r   )r(   
comment_idzkanban_comment: Nzkanban_comment failed)r   r	   r)   rv   r   r   r   rF   add_commentrp   rc   r.   r   r_   r   )
r|   r   r:   r   r   r>   rE   rf   cidr   s
             r   _handle_commentr    s   
((9

C 
B
 
 	
 88FD .s4yy(( .,--- T$777D Z^^,--9FHHWE2%(((D	..s6D		.JJCss333JJLLLLDJJLLLL 2 2 20Q0011111111 2 2 201110Q00111111112sH   D; 6D" D; "D88D; ;
FEFF)+FFFc                
   |                      d          }|r!t          |                                          st          d          S |                      d          }|st          d          S |                      d          }|                      d          pg }|                      d          pt          j                             d          }|                      d	          pt          j                             d
          }|                      d          }|                      d          }	|                      d          }
|                      d          p|                      d          }|	du o|
du }|	d}	t          | d          \  }}|rt          |          S |                      d          }|                      d          }|                      d          pd}|                      d          }t          |t                    r|g}|@t          |t          t          f          s$t          dt          |          j                   S t          | d          \  }}|rt          |          S |                      d          }t          |t                    r|g}t          |t          t          f          s$t          dt          |          j                   S |                      d          }	 t          |          \  }}	 |r^t          j                             d          }|r=|                    ||          }|%|j        r|j        }	|j        }
||j        r|j        }|                    |t          |                                          |t          |          t          |          ||t%          |          ndt          |	          |
||||t%          |          nd|||t%          |          ndt          |          t          j                             d           pd!|"          }|                    ||          }t'          ||          }t)          ||r|j        nd|#          |                                 S # |                                 w xY w# t.          $ r}t          d$|           cY d}~S d}~wt0          $ r6}t2                              d%           t          d$|           cY d}~S d}~ww xY w)&zCreate a child task. Orchestrator workers use this to fan out.

    ``parents`` can be a list of task ids; dependency-gated promotion
    works as usual.
    r   ztitle is requiredr   u   assignee is required — name the profile that should execute this task (the dispatcher will only spawn tasks with an assignee)r   r   r   HERMES_TENANTr7   r4   r   r   r   projectr   Nscratchtriageidempotency_keymax_runtime_secondsinitial_statusrunningskillsz*skills must be a list of skill names, got r   goal_max_turnsz(parents must be a list of task ids, got r>   rB   r   r   r  r  )r   r   r   r   r   r   r   r   r   r#  r$  r%  r(  r   r)  r&  r   r7   )r(   r   
subscribedzkanban_create: zkanban_create failed)r   r)   rv   r	   r   r   r   r   r   r   r   r   rF   r   r   r   r   create_taskr-   _maybe_auto_subscriberp   r   rc   r.   r   r_   r   )r|   r   r   r   r   r   r   r7   r   r   r   r   _inherit_workspacer#  r   r$  r%  r&  r(  r   goal_bool_errorr)  r>   rE   rf   	_self_tid
_self_tasknew_tidnew_taskr*  r   s                                  r   _handle_creater3  B  s5    HHWE /E

((** /-...xx
##H 
K
 
 	
 88FDhhy!!'RGXXhB2:>>/#B#BF ,''N2:>>:M+N+NJxx
##H XX.//NXX.//N)$$>(>(>J'4/JNd4J"(x88FJ &*%%%hh011O((#899XX.//<9NXXhF&# *VdE]"C"CPf9NPP
 
 	
 "1{!C!CI +/***XX.//N'3 )ge}-- 
OtG}}7MOO
 
 	
 HHWE71%(((D0	 " 
?JNN+?@@	 ?!#T9!=!=J!-*2K-)3)B)3)B &-*2G-)3)>Jnn%jj&&((Xg*2*>XA">22-% / +6 +,,,<@#+9+EC'''4">22:>>*:;;Gx%1 %  G4 {{411H.tW==J*2<x%   JJLLLLDJJLLLL 1 1 1/A//00000000 1 1 1/000/A//000000001sI   2R! E-R 3R! RR! !
T+S<TT+T :T Trf   c           	     |   	 t                      }t          |ddd          sdS n# t          $ r Y nw xY wd}d}	 ddlm}  |d	d          } |d
d          }|r|s4 |dd          pt
          j                            dd          }|sdS d}|} |dd          pd} |dd          pd} |dd          pt
          j                            d          }	ddlm	}
 |

                    | ||||||	           dS # t          $ r5}t                              d||t          |                     Y d}~dS d}~ww xY w)a+  Auto-subscribe the calling session to task completion / block events.

    Returns True if a subscription row was written, False otherwise (no
    session context, config gate disabled, or best-effort failure). The
    caller surfaces this in the ``subscribed`` field of the kanban_create
    response so an orchestrator can decide whether to fall back to an
    explicit ``kanban_notify-subscribe`` or to polling.

    Gated by ``kanban.auto_subscribe_on_create`` in config.yaml (default
    True). Disable to mirror pre-feature behaviour, e.g. when the
    originating user/chat opted out via the per-platform notification
    toggle (see ``hermes dashboard``).

    Subscription paths:

    - **Gateway** (telegram/discord/slack/etc): ``HERMES_SESSION_PLATFORM``
      and ``HERMES_SESSION_CHAT_ID`` are set in ContextVars by the
      messaging gateway before agent dispatch. The notification poller
      already keys off these, so we just register a row.

    - **TUI** (herm desktop / herm TUI): the platform/chat_id ContextVars
      are intentionally cleared (TUI is a single-channel local UI, not
      a multi-tenant chat surface), but the agent subprocess inherits
      ``HERMES_SESSION_KEY`` from the parent session. We subscribe with
      ``platform="tui"`` and ``chat_id=<key>``; the TUI notification
      poller (``tui_gateway/server.py``) reads ``kanban_notify_subs``
      for these rows and posts the completion message into the running
      session.

    - **CLI / cron / test / unattached**: no persistent delivery channel,
      no-op.

    Failure mode: any exception inside the function is logged at WARNING
    with the offending exception + diagnostic env vars and swallowed.
    We never want a notification bookkeeping failure to fail the
    kanban_create that the agent is mid-conversation about.
    r   auto_subscribe_on_createTrz   Fr   r   )get_session_envHERMES_SESSION_PLATFORMHERMES_SESSION_CHAT_IDHERMES_SESSION_KEYtuiHERMES_SESSION_THREAD_IDNHERMES_SESSION_USER_IDHERMES_SESSION_PROFILEr  r@   )r(   platformchat_id	thread_iduser_idnotifier_profilez9_maybe_auto_subscribe failed: %r (platform=%r key_set=%r))r   r
   r   gateway.session_contextr6  r   r   r   rC   rA   add_notify_subr_   r   r   )rf   r(   r   r>  r?  r6  session_keyr@  rA  rB  _kb_excs               r   r,  r,    s   LmmsH&@$OOO 	5	    	
 HG/;;;;;;"?#<bAA!/":B?? 	"w 	"   4b99 <:>>"6;;   uH!G#O$>CCKt	!/":B??G4O4b99 0z~~.// 	 	0/////'w-	 	 	
 	
 	
 t   G(DMM	
 	
 	
 uuuuus+   !& 
33AC< A-C< <
D;*D66D;c                   t          d          }|r|S |                     d          }|st          d          S t          t	          |                    }|r|S |                     d          }	 t          |          \  }}	 |                    |t	          |                    }|s't          d| d          |                                 S t          t	          |          d	          |                                 S # |                                 w xY w# t          $ r}	t          d
|	           cY d}	~	S d}	~	wt          $ r6}	t                              d           t          d
|	           cY d}	~	S d}	~	ww xY w)z(Transition a blocked task back to ready.kanban_unblockr(   ztask_id is requiredr>   rB   zcould not unblock z (not blocked or unknown)ready)r(   r   zkanban_unblock: Nzkanban_unblock failed)r   r   r	   r=   r)   rF   unblock_taskrc   rp   r.   r   r_   r   )
r|   r   r   r:   r   r>   rE   rf   rm   r   s
             r   _handle_unblockrL    s   &'788E 
((9

C 1/00023s88<<M HHWE2%(((D	s3xx00B W!"Us"U"U"UVV JJLLLL s3xx888JJLLLLDJJLLLL 2 2 20Q0011111111 2 2 201110Q00111111112sT   0D 7D ;D D -D DD 
E?%D<6E?<E?	+E:4E?:E?c                J   |                      d          }|                      d          }|r|st          d          S |                      d          }	 t          |          \  }}	 |                    |||           t	          ||          |                                 S # |                                 w xY w# t          $ r}t          d|           cY d}~S d}~wt          $ r6}t          	                    d	           t          d|           cY d}~S d}~ww xY w)
u4   Add a parent→child dependency edge after the fact.	parent_idchild_idz(both parent_id and child_id are requiredr>   rB   )rN  rO  zkanban_link: Nzkanban_link failed)
r   r	   rF   
link_tasksrp   rc   r.   r   r_   r   )r|   r   rN  rO  r>   rE   rf   r   s           r   _handle_linkrQ  8  sR   %%Ixx
##H FH FDEEEHHWE/%(((D	MM$)hMGGGX>>>JJLLLLDJJLLLL / / /-!--........ / / /-...-!--......../sH   B> ((B% B> %B;;B> >
D"CD"D",+DD"D"zrTask id. If omitted, defaults to HERMES_KANBAN_TASK from the env (the task the dispatcher spawned you to work on).uX  Kanban board slug to target. When omitted, the call resolves the active board the usual way: HERMES_KANBAN_DB env → HERMES_KANBAN_BOARD env → the 'current' symlink under the kanban home → 'default'. Pass an explicit slug only when the caller (e.g. a Telegram routing layer) needs to override the env-pinned active board for this one call.dict[str, str]c                     dt           dS )zSchema fragment for the optional ``board`` parameter.

    Centralised so a future tweak to the description / validation hint
    only has to land in one place.
    stringr   description)_DESC_BOARDr   r   r   _board_schema_proprX  a  s     [999r   kanban_showuO  Read a task's full state — title, body, assignee, parent task handoffs, your prior attempts on this task if any, comments, and recent events. Use this to (re)orient yourself before starting work, especially on retries. The response includes a pre-formatted ``worker_context`` string suitable for inclusion verbatim in your reasoning.objectrT  rU  )r(   r>   )r   
propertiesrequired)r}   rV  
parametersr   u  List Kanban task summaries so an orchestrator profile can discover work to route. Supports the same core filters as the CLI: assignee, status, tenant, include_archived, and limit. Returns compact rows with ids, title, status, assignee, priority, parent/child ids, and counts. Bounded to 50 rows by default, 200 max, with truncation metadata. Also recomputes ready tasks before listing, matching the CLI. Orchestrator-only — dispatcher-spawned task workers never see this tool.z!Optional assignee/profile filter.)r#  todorJ  r'  r  r   archivedzOptional task status filter.)r   enumrV  z)Optional tenant/project namespace filter.booleanz*Include archived tasks. Defaults to false.integerz6Optional maximum rows to return (default 50, max 200).)r   r   r   r   r   r>   kanban_completeuh  Mark your current task done with a structured handoff for downstream workers and humans. Prefer ``summary`` for a human-readable 1-3 sentence description of what you did; put machine-readable facts in ``metadata`` (changed_files, tests_run, decisions, findings, etc). At least one of ``summary`` or ``result`` is required. If you created new tasks via ``kanban_create`` during this run, list their ids in ``created_cards`` — the kernel verifies them so phantom references are caught before they leak into downstream automation. If you produced deliverable files (charts, PDFs, spreadsheets, generated images), list their absolute paths in ``artifacts`` — the gateway notifier will upload them as native attachments to the human who subscribed to the task, so the deliverable lands in their chat alongside the summary instead of being a path they have to fetch by hand.zrHuman-readable handoff, 1-3 sentences. Appears in Run History on the dashboard and in downstream workers' context.u   Free-form dict of structured facts about this attempt — {"changed_files": [...], "tests_run": 12, "findings": [...]}. Surfaced to downstream workers alongside ``summary``.zShort result log line (legacy field, maps to task.result). Use ``summary`` instead when possible; this exists for compatibility with callers that still set --result on the CLI.arrayr   u  Optional structured manifest of task ids you created via ``kanban_create`` during this run. The kernel verifies each id exists and was created by this worker's profile; any phantom id blocks the completion with an error listing what went wrong (auditable in the task's events). Only list ids you got back from a successful ``kanban_create`` call — do not invent or remember ids from prose. Omit the field if you did not create any cards.)r   itemsrV  u1  Optional list of absolute paths to deliverable files you produced during this run — generated charts, PDFs, spreadsheets, images, archives. Examples: ["/tmp/q3-revenue.png", "/tmp/report.pdf"]. The gateway notifier uploads each path as a native attachment to the subscribed chat (images embed inline, everything else uploads as a file) so the deliverable lands with the completion notification. Skip intermediate scratch files and references that are not the deliverable. The path must exist on disk when the notifier runs; missing files are silently skipped.)r(   r   r1   r   r   r   r>   kanban_blockuq  Stop work on this task and route it according to WHY you're stuck. Set ``kind`` to say which: 'dependency' (waiting on another task — goes to todo and auto-resumes when that task finishes, no human needed), 'needs_input' (you need a human decision/answer), 'capability' (a hard wall: no access, missing credentials, an action no agent can do), or 'transient' (a flaky failure that may clear). ``reason`` is shown to the human on the board. If a task keeps getting unblocked and re-blocked for the same reason, it is auto-escalated to triage. Use for genuine blockers only — don't block on things you can resolve yourself.zWhat you need answered or what stopped you, in one or two sentences. Don't paste the whole conversation; the human has the board and can ask follow-ups via comments.)rG   rH   
capability	transientzWhy you're blocked. 'dependency' waits in todo and resumes automatically; the others surface to a human. Omit only if none apply.)r(   r  r   r>   r  kanban_heartbeatu   Signal that you're still alive during a long operation (training, encoding, large crawls). Call every few minutes so humans see liveness separately from PID checks. Pure side effect — no work changes.zHOptional short note describing current progress. Shown in the event log.)r(   rY   r>   kanban_commentu   Append a comment to a task's thread. Use for durable notes that should outlive this run (questions for the next worker, partial findings, rationale). Ephemeral reasoning doesn't belong here — use your normal response instead.uW   Task id. Required (may be your own task or another's — comment threads are per-task).z Markdown-supported comment body.)r(   r   r>   r   kanban_createuc  Create a new kanban task, optionally as a child of the current one (pass the current task id in ``parents``). Used by orchestrator workers to fan out — decompose work into child tasks with specific assignees, link them into a pipeline, then complete your own task. The dispatcher picks up the new tasks on its next tick and spawns the assigned profiles.r   zShort task title (required).r   u   Profile name that should execute this task (e.g. 'researcher-a', 'reviewer', 'writer'). Required — tasks without an assignee are never dispatched.zkOpening post: full spec, acceptance criteria, links. The assigned worker reads this as part of its context.r   zParent task ids. The new task stays in 'todo' until every parent reaches 'done'; then it auto-promotes to 'ready'. Typical fan-in: list all the researcher task ids when creating a synthesizer task.r   zUOptional namespace for multi-project isolation. Defaults to HERMES_TENANT env if set.r   zZDispatcher tiebreaker. Higher = picked sooner when multiple ready tasks share an assignee.r   )r"  dirworktreezWorkspace flavor: 'scratch' (fresh tmp dir, default), 'dir' (shared directory, requires absolute workspace_path), 'worktree' (git worktree).r   zYAbsolute path for 'dir' or 'worktree' workspace. Relative paths are rejected at dispatch.r!  zOptional project id or slug to link the task to. When set, the task becomes a git worktree under the project's primary repo with a deterministic branch (project slug + task id), instead of a random branch.r#  u   If true, task lands in 'triage' instead of 'todo' — a specifier profile is expected to flesh out the body before work starts.r$  zIf a non-archived task with this key already exists, return that task's id instead of creating a duplicate. Useful for retry-safe automation.r%  zxPer-task runtime cap. When exceeded, the dispatcher SIGTERMs the worker and re-queues the task with outcome='timed_out'.r&  r'  r  zInitial card status. Use 'blocked' for tasks that require immediate human ops (R3 gate) to skip the brief running-to-blocked transition. Defaults to 'running', which preserves the usual dispatch path.r(  u:  Skill names to force-load into the dispatched worker. The kanban lifecycle is already injected automatically; use this to pin a task to a specialist context — e.g. ['translation'] for a translation task, ['github-code-review'] for a reviewer task. The names must match skills installed on the assignee's profile.r   a  Run the dispatched worker in a goal loop. When true, after each turn an auxiliary judge checks the worker's response against this card's title/body; if the work isn't done and budget remains, the worker keeps going in the same session until the judge agrees it's complete (or the goal-turn budget is exhausted, which blocks the task for human review). Use this for open-ended cards where one shot rarely finishes the work. Defaults to false (classic single-shot worker).r)  zTurn budget for goal_mode workers. Caps how many continuation turns the worker may take before the task is blocked for review. Ignored unless goal_mode is true. Defaults to the goal-engine default (20).rI  u   Move a blocked Kanban task back to ready. Orchestrator-only — only profiles with the kanban toolset can unblock routed work; dispatcher-spawned task workers never see this tool.z#Blocked task id to return to ready.kanban_linku   Add a parent→child dependency edge after both tasks already exist. The child won't promote to 'ready' until all parents are 'done'. Cycles and self-links are rejected.zParent task id.zChild task id.)rN  rO  r>   rN  rO  r   u   📋)r}   toolsetschemahandlercheck_fnemojiu   ✔u   ⏸u   💓u   💬u   ➕u   ▶u   🔗)r   r   )r#   r$   r   r$   )r(   r)   r   r*   )r(   r)   r1   r2   r   r2   )r:   r)   r   r$   )N)r>   r$   )rk   r   r   r)   )rq   r   r   r$   )r|   r6   r}   r)   r{   r   )r   r)   r   r$   )r   r   )r|   r6   r   r)   )rf   r   r(   r)   r   r   )r   rR  )C__doc__
__future__r   rn   loggingr   typingr   r   agent.redactr   hermes_cli.goalsr   tools.registryr   r	   r   r
   r   	getLoggerr   r_   r   r   r   r    r"   r'   r0   r9   r=   rF   	frozensetr  rO   r]   rQ   __annotations__rj   rp   ry   r   r   r   r   r   r  r  r  r  r3  r,  rL  rQ  _DESC_TASK_ID_DEFAULTrW  rX  KANBAN_SHOW_SCHEMAKANBAN_LIST_SCHEMAKANBAN_COMPLETE_SCHEMAKANBAN_BLOCK_SCHEMAKANBAN_HEARTBEAT_SCHEMAKANBAN_COMMENT_SCHEMAKANBAN_CREATE_SCHEMAKANBAN_UNBLOCK_SCHEMAKANBAN_LINK_SCHEMAregisterr   r   r   <module>r     sa    8 # " " " " "   				                 . . . . . . ' ' ' ' ' ' / / / / / / / / 2 2 2 2 2 2 2 2		8	$	$   
 
 
 
) ) ) )") ) ) )$   
 
 
 
      @' ' ' ' ' "+L-+H!I!I . . . .T (, $&)  ) ) ) )8 8 8 8v. . . .    ?D B B B B B B   $   >I/ I/ I/ I/X:/ :/ :/ :/za3 a3 a3 a3HK0 K0 K0 K0\04 04 04 04f"2 "2 "2 "2Jt1 t1 t1 t1n` ` ` `F2 2 2 28/ / / /68  : : : : 	&  !4  ('))
 
 
 
  0 	  !B 
 !    >  !J 
 "K! !
 "W  ('))3
 
6 ; + + \ 	>"  !4 
 !(  !5  !B    (+	0	 "   (+,	 & ('))EC
 C
H MG G'[ [ | 		4  !4 
 !O  !PPP/	  ('))-
 
0 J5 * * Z 	&  !4 
 !.  ('))
 
 !   : 	<  !C  !A  ('))
 
 '!   : 	C O
 = O

  " O
  # O
,  (+(	
 
-O
B  < CO
P !C QO
^  666K	 _O
p  ? qO
~  < O
P !3 QO
`  E   aO
p "!5$ $qO
@  "I.J		 	AO
T  (+*	 UO
n !L oO
J !F KO
\ ''))]O
` j)eS S^ ^ B 	?
  !D  ('))
 
 K
 
  * 	:
 "*;LMM"*;KLL''))
 

 !*-   .  	
     	,
     	!
     	
     	"
     	 
     	
     	 ,
     	
     r   