+
    jHàjç4  ã                  óà  € R t ^ RIHt ^ RIt^ RIt^ RIHt ^ RIHt ^ RI	H
t ^ RIHt ^ RI	Ht ^ RI	Ht ^ R	IHtHtHtHtHt ^ R
IHt ]P4                  ! ]4      tRtRt]P>                  ! R]P@                  4      t!] ! R R4      4       t"R R ltRR/R R llt#R R lt$R R lt%R R lt&] ! R R4      4       t'RR/R R llt(R  R! lt)R" R# lt*R$ R% lt+R&RR'R/R( R) llt,R*R/R+ R, llt-R# )-u}  Kanban decomposer â€” fan a triage task out into a graph of child tasks.

Invoked by ``hermes kanban decompose [task_id | --all]`` and the gateway
dispatcher's auto-decompose path. Reads the profile roster (with
descriptions), asks the auxiliary LLM for a task graph in JSON, then
atomically creates the children, links them under the root, and flips the
root ``triage -> todo``. The root stays alive as parent of every leaf child so
it wakes back up when the graph completes and its assignee (the orchestrator
profile) can judge completion and add more work.

Mirrors ``kanban_specify`` (lazy aux import, lenient parse, never raises on
expected failures). ``fanout=false`` collapses to the ``specify`` behaviour
(tighten + promote, no children), making ``decompose`` a strict superset.
Unknown assignees are rewritten to ``default_assignee`` â€” a child NEVER ends
up with ``assignee=None``.
)ÚannotationsN)Ú	dataclass)ÚOptional)Ú	kanban_db)Údecompose_triage_task)Úkanban_db_connect)Úprofiles)Ú	_call_auxÚ_extract_json_blobÚ_load_triage_taskÚ_task_prompt_fieldsÚ_title_body)Ú_profile_authoruÃ  You are the Kanban decomposer for the Hermes Agent board.

A user dropped a rough idea into the Triage column. Your job is to break it
into a small graph of concrete child tasks and route each one to the best-
matching profile from the available roster.

You will be given:
  - The original task title and body
  - The list of available profiles (each with name + description)
  - The fallback "default_assignee" used when no profile fits

Output a single JSON object with this exact shape:

  {
    "fanout": true,
    "rationale": "<one sentence on why this decomposition>",
    "tasks": [
      {
        "title": "<concrete task title, imperative voice, <= 80 chars>",
        "body":  "<detailed spec for the worker on this child task>",
        "assignee": "<profile name from the roster, or null for default>",
        "parents": [<int>, ...]
      },
      ...
    ]
  }

Rules:
  - "parents" is a list of INDICES (0-based) into this same "tasks" list,
    expressing actual data dependencies. Tasks with no parents run in
    PARALLEL. Tasks with parents wait until every parent completes.
  - Prefer parallelism. If two tasks can be done independently, give
    them no parents so the dispatcher fans them out at once.
  - Use 2-6 tasks for normal work. Don't create 20 tiny tasks. Don't
    cram everything into 1 task.
  - Pick assignees from the roster by matching the task to the profile's
    DESCRIPTION (not just the name). When nothing matches well, use null
    and the system will route to the default_assignee.
  - Each child task body is what a fresh worker will read with no other
    context â€” be specific about goal, approach, and acceptance criteria.

When the task is genuinely a single unit of work (no useful decomposition),
return:

  {
    "fanout": false,
    "rationale": "<one sentence>",
    "title": "<tightened title>",
    "body":  "<concrete spec for a single worker>",
    "assignee": "<profile name from the roster, or null for default>"
  }

In that case the task stays as one work item, just with a tightened spec and
a concrete assignee. If no profile fits, use null and the system will route to
the default_assignee.

No preamble, no closing remarks, no code fences. Output only the JSON object.
z´Task id: {task_id}
Title: {title}
Body:
{body}

Available profiles (assignees you may pick from):
{roster}

Default assignee (used when no profile fits a task): {default_assignee}
z^```(?:json)?\s*|\s*```$c                  óh   € ] tR t^pt$ RtR]R&   R]R&   RtR]R&   RtR]R	&   R
tR]R&   R
t	R]R&   Rt
R
# )ÚDecomposeOutcomez+Result of decomposing a single triage task.ÚstrÚtask_idÚboolÚokÚ ÚreasonFÚfanoutNzlist[str] | NoneÚ	child_idsúOptional[str]Ú	new_title© )Ú__name__Ú
__module__Ú__qualname__Ú__firstlineno__Ú__doc__Ú__annotations__r   r   r   r   Ú__static_attributes__r   ó    Ú@/home/ubuntu/.hermes/hermes-agent/hermes_cli/kanban_decompose.pyr   r   p   s8   ‡ á5àƒLØƒHØ€FˆCÓØ€FˆDÓØ"&€IÐÓ&Ø#€Iˆ}×#r#   r   c               ó   € V ^8„  d   QhRR/# )é   Úreturnr   r   )Úformats   "r$   Ú__annotate__r)   |   s   € ÷ )ñ )˜ñ )r#   c                 ó   € \        R4      # )z0Mirror of ``hermes_cli.kanban._profile_author``.Ú
decomposer)Ú_specify_authorr   r#   r$   r   r   |   s   € ä˜<Ó(Ð(r#   Úfallbackc               ó(   € V ^8„  d   QhRRRRRRRR/# )r&   ÚcfgÚdictÚkeyr   r-   r   r'   r   )r(   s   "r$   r)   r)   �   s)   € ÷ ñ  4ð ¨cð Àð ÐY\ñ r#   c               óÊ  € \        V \        4      '       d   V P                  R/ 4      M/ pVP                  V4      ;'       g    RP                  4       pYB;'       g    RP                  4       3 F.  pV'       g   K   \        P
                  ! V4      '       d   Vu # K0  	   \        P                  ! 4       ;'       g    R#   \         d     K`  i ; i  \         d     R# i ; i)ub  ``kanban.<key>`` if it names an existing profile, else ``fallback``
(the root task's own assignee) if that does, else the active default
profile â€” so a task is never stranded for lack of an owner.
``orchestrator_profile`` owns the root after fan-out; ``default_assignee``
catches children the decomposer can't route.

The root's assignee sits before the active profile because the decomposer
runs inside whatever profile hosts the dispatcher â€” an operator's
credential-less incognito profile, say â€” and that profile must never
silently become the owner of work the card was assigned away from (#114294).
Úkanbanr   Údefault)Ú
isinstancer0   ÚgetÚstripÚprofiles_modÚprofile_existsÚ	ExceptionÚget_active_profile_name)r/   r1   r-   Ú
kanban_cfgÚexplicitÚ	candidates   &&$   r$   Ú_resolve_profile_from_cfgr?   �   sÆ   € ô +5°S¼$×*?Ò*?�—‘˜ 2Ô&ÀR€JØ—‘˜sÓ#×)Ð) r×0Ñ0Ó2€HØ§ ¨R× 6Ñ 6Ó 8Ó9ˆ	ß‰9ðÜ×.Ò.¨y×9Ò9Ø$Ò$ñ :ñ :ðÜ×3Ò3Ó5×BÐB¸ÐBøô ô Úðûô ô Úðús0   Á=CÂCÂ"C Â>C ÃCÃCÃC"Ã!C"c               ó   € V ^8„  d   QhRR/# )r&   r'   ztuple[list[dict], set[str]]r   )r(   s   "r$   r)   r)   œ   s   € ÷ 2ñ 2Ð2ñ 2r#   c                 óÈ  €  \         P                  ! 4       p . pT  Fj  pTP                  ;'       g    RP                  4       pTP                  RTP                  RT;'       g    RTP                  : R2R\        T4      /4       Kl  	  Y  Uu0 uF  q3P                  kK  	  up3#   \         d-   p\        P	                  RT4       . \        4       3u Rp?# Rp?ii ; iu upi )	zd``(roster_for_prompt, valid_assignee_names)``; entries are
``{name, description, has_description}``.z&decompose: failed to list profiles: %sNr   ÚnameÚdescriptionz(no description; profile named Ú)Úhas_description)r8   Úlist_profilesr:   ÚloggerÚwarningÚsetrC   r7   ÚappendrB   r   )Úall_profilesÚexcÚrosterÚpÚdescs        r$   Ú_build_rosterrP   œ   sÍ   € ðÜ#×1Ò1Ó3ˆð €FÛˆØ—‘×#Ð# ×*Ñ*Ó,ˆØ�‰Ø�A—F‘FØ˜4×PÐPÐ%DÀQÇVÁVÁJÈaÐ#PØœt D›zð
ö 	ñ ð  LÓ1¡L˜q—F”F¡LÑ1Ð1Ð1øô ô Ü�‰Ð?ÀÔEØ”3“5ˆyÕûðüò 2s#   ‚B% ÂCÂ%CÂ0!CÃCÃCc               ó    € V ^8„  d   QhRRRR/# )r&   rM   ú
list[dict]r'   r   r   )r(   s   "r$   r)   r)   ¯   s   € ÷ ñ ˜:ð ¨#ñ r#   c                óF   € V '       g   R # RP                  R V  4       4      # )u:     (no profiles installed â€” decomposer cannot route work)Ú
c              3  ót   "  € T F.  pR VR,           VR,          '       d   RMR RVR,           2x € K0  	  R# 5i)z  - rB   rE   r   u    âš  undescribedz: rC   Nr   )Ú.0Úentrys   & r$   Ú	<genexpr>Ú!_format_roster.<locals>.<genexpr>²   sD   é € ð áˆEð ˆu�V�}ˆo EÐ*;×$<Ô$<™bÐBTÐUÐUWÐX]Ð^kÕXlÐWmÕnÛùs   ‚8 8)Újoin)rM   s   &r$   Ú_format_rosterr[   ¯   s&   € ßÙKØ�9‰9ñ áóó ð r#   c               ó(   € V ^8„  d   QhRRRRRRRR/# )r&   ÚassigneeÚobjectÚdefault_assigneer   Úvalid_namesúset[str]r'   r   )r(   s   "r$   r)   r)   ¸   s0   € ÷ Añ A¨ð AÀcð AÐX`ð AÐehñ Ar#   c               ó’   € \        V \        4      '       d   V P                  4       '       g   V# V P                  4       pW29   d   V# T# )uW   A valid assignee, else ``default_assignee`` â€” promoted work is never
left unassigned.)r5   r   r7   )r]   r_   r`   Úchosens   &$$ r$   Ú_normalize_assignee_choicerd   ¸   s@   € ô �h¤×$Ò$¨H¯N©N×,<Ò,<ØÐØ�^‰^Ó€FØÔ*ˆ6Ð@Ð0@Ð@r#   c                  óN   € ] tR t^Át$ RtR]R&   R]R&   R]R&   R]R&   R	]R
&   RtR# )Ú_Routingz5Config-derived routing context for one decomposition.r   Úorchestratorr_   r   Úauto_promoterR   rM   ra   r`   r   N)r   r   r   r   r    r!   r"   r   r#   r$   rf   rf   Á   s#   ‡ á?àÓØÓØÓØÓØ×r#   rf   Úroot_assigneec               ó    € V ^8„  d   QhRRRR/# )r&   ri   r   r'   rf   r   )r(   s   "r$   r)   r)   Ì   s   € ÷ ñ  Mð ¸Xñ r#   c           
     ó2  € ^ RI Hp  V! 4       p\        V\        4      '       d   VP                  R/ 4      M/ p\        4       w  rE\        \        VRV R7      \        VRV R7      \        VP                  RR4      4      VVR7      #   \         d    / p L†i ; i)	é    )Úload_config_readonlyr3   Úorchestrator_profile)r-   r_   Úauto_promote_childrenT)rg   r_   rh   rM   r`   )
Úhermes_cli.configrm   r:   r5   r0   r6   rP   rf   r?   r   )ri   rm   r/   r<   rM   r`   s   $     r$   Ú_load_routingrq   Ì   s•   € Ý6ðÙ"Ó$ˆô +5°S¼$×*?Ò*?�—‘˜ 2Ô&ÀR€JÜ'›/Ñ€FÜÜ.¨sÐ4JÐUbÔcÜ2°3Ð8JÐUbÔcÜ˜*Ÿ.™.Ð)@À$ÓGÓHØØôð øô	 ô ØŠðús   ˆB ÂBÂBc          
     ó,   € V ^8„  d   QhRRRRRRRRR	R
/# )r&   Útaskzkb.TaskÚparsedr0   Úroutingrf   Úauthorr   r'   r   r   )r(   s   "r$   r)   r)   Ý   s7   € ÷ iñ i˜ð i¨ð i¸ð iÈ#ð iÐRbñ ir#   c                ó  € \        V4      w  rERpV P                  '       g2   \        VP                  R4      VP                  VP
                  R7      pVf   Vf   \        V P                  RR4      # \        P                  ! 4       ;_uu_ 4       p\        P                  ! WpP                  WEWcR7      pRRR4       X'       g   \        V P                  RR4      # \        V P                  RR	RVR
7      #   + '       g   i     LI; i)zF``fanout=false``: single-task spec promotion (same effect as specify).Nr]   ©r_   r`   Fz3decomposer returned fanout=false with no title/body)ÚtitleÚbodyr]   rv   z)task moved out of triage before promotionTzsingle task (no fanout))r   r   )r   r]   rd   r6   r_   r`   r   ÚidÚkbcÚconnect_closingÚkbÚspecify_triage_task)	rs   rt   ru   rv   Ú	title_valÚbody_valÚassignee_valÚconnr   s	   &&&&     r$   Ú_apply_singler„   Ý   sÑ   € ä% fÓ-Ñ€IØ€LØ�=�=ˆ=Ü1Ø�J‰J�zÓ"°W×5MÑ5MÐ[b×[nÑ[nô
ˆð Ò˜XÒ-Ü §¡¨Ð0eÓfÐfÜ	×	Ò	×	Ô	 $Ü×#Ò#Ø—'‘' ÀLô
ˆ÷ 
÷ Ü §¡¨Ð0[Ó\Ð\Ü˜DŸG™G TÐ+DÈUÐ^gÔhÐh÷ 
×	ús   Â$C4Ã4D	c               ó(   € V ^8„  d   QhRRRRRRRR/# )	r&   r   r   Ú	raw_tasksÚlistru   rf   r'   ztuple[list[dict], str]r   )r(   s   "r$   r)   r)   ð   s)   € ÷ ñ ˜Sð ¨Tð ¸Hð ÐI_ñ r#   c                ó  € . p\        V4       EFñ  w  rE\        V\        4      '       g   . RV R23u # VP                  R4      p\        V\        4      '       d   VP                  4       '       g   . RV R23u # VP                  R4      pVP                  R4      p\        W‚P                  VP                  R7      p	\        V\        4      '       dW   VP                  4       '       dA   VP                  4       VP                  9  d"   \        P                  RWW‚P                  4       VP                  R	4      ;'       g    . p
\        V
\        4      '       g   . p
TP                  RVP                  4       R
,          R\        V\        4      '       d   VP                  4       MRRT	R	V
 Uu. uFC  p\        V\        4      '       g   K  ^ Tu;8:  d   \        V4      8  g   K5  M K9  W´8w  g   KA  VNKE  	  up/4       EKô  	  VR3# u upi )z‘Validate/normalise the LLM's ``tasks`` list; ``(children, "")`` or ``([], reason)``.
Unknown assignees route to the default; never assignee=None.ztasks[z] is not an objectry   z].title is missing or emptyrz   r]   rx   uY   decompose: task %s child %d picked unknown assignee %r â€” routing to default_assignee %rÚparents:NéÈ   Nr   )Ú	enumerater5   r0   r6   r   r7   rd   r_   r`   rG   Úinfor‡   rJ   ÚintÚlen)r   r†   ru   ÚchildrenÚidxrW   ry   rz   r]   rc   r‰   rN   s   &&&         r$   Ú_clean_childrenr‘   ð   s³  € ð €HÜ 	×*‰
ˆÜ˜%¤×&Ò&Ø˜ ˜uÐ$6Ð7Ð7Ò7Ø—	‘	˜'Ó"ˆÜ˜%¤×%Ò%¨U¯[©[¯]ª]Ø˜ ˜uÐ$?Ð@Ð@Ò@Ø�y‰y˜Ó ˆØ—9‘9˜ZÓ(ˆÜ+Ø×'?Ñ'?ÈW×M`ÑM`ô
ˆô �h¤×$Ò$¨¯©×)9Ò)9¸h¿n¹nÓ>NÐV]×ViÑViÔ>iÜ�K‰Kð1à˜h×(@Ñ(@ôð
 —)‘)˜IÓ&×,Ð,¨"ˆÜ˜'¤4×(Ò(ØˆGØ�‰Ø�U—[‘[“] 4Õ(Ø¤J¨t´S×$9Ò$9�D—J‘J”L¸rØ˜à¡7Ój¡7˜a¬j¸¼C×.@œÀQÈ!ÖE\ÌcÐR[ËnÑE\œÑE\™ÐabÑaiŸ˜¡7Ñjð
÷ 	ñ) +ð6 �Rˆ<Ðùò ks   Æ.HÇHÇ%HÇ)HÇ1Hc          
     ó,   € V ^8„  d   QhRRRRRRRRRR	/# )
r&   r   r   rt   r0   ru   rf   rv   r'   r   r   )r(   s   "r$   r)   r)     s0   € ÷ ñ ˜3ð ¨ð °xð Èð ÐQañ r#   c                óÖ  € VP                  R 4      ;'       g    . p\        V\        4      '       d	   V'       g   \        V RR4      # \	        WV4      w  rVV'       d   \        V RV4      #  \
        P                  ! 4       ;_uu_ 4       p\        VV VP                  VVVP                  R7      pRRR4       Xf   \        T RR4      # \        T R	R
\!        T4       R2R	TR7      #   + '       g   i     L>; i  \         d   p	\        T RRT	 24      u Rp	?	# Rp	?	i\         dD   p	\        P                  RT 4       \        T RR\        T	4      P                   24      u Rp	?	# Rp	?	ii ; i)ÚtasksFz5decomposer returned fanout=true with empty tasks list)ri   r�   rv   rh   NzDB rejected graph: zdecompose: DB error on task %sz
DB error: z.task already decomposed or moved out of triageTzdecomposed into z	 children)r   r   )r6   r5   r‡   r   r‘   r|   r}   r   rg   rh   Ú
ValueErrorr:   rG   Ú	exceptionÚtyper   rŽ   )
r   rt   ru   rv   r†   r�   r   rƒ   r   rL   s
   &&&&      r$   Ú_apply_fanoutr˜     sK  € Ø—
‘
˜7Ó#×)Ð) r€IÜ�i¤×&Ò&¯iÜ ¨Ð0gÓhÐhÜ& w¸7ÓCÑ€HßÜ ¨°Ó7Ð7ðSÜ× Ò ×"Ô" dÜ-ØØØ%×2Ñ2Ø!ØØ$×1Ñ1ôˆI÷ #ð ÒÜ ¨Ð0`ÓaÐaÜØ�Ð)¬#¨i«.Ð)9¸ÐCÈDÐ\eôð ÷! #×"ûô ô MÜ ¨Ð2EÀcÀUÐ0KÓLÕLûÜô SÜ×ÑÐ9¸7ÔCÜ ¨°*¼TÀ#»Y×=OÑ=OÐ<PÐ0QÓRÕRûðSúsT   Á+C7 Â&C$Â.C7 Ã$C4	Ã/C7 Ã4C7 Ã7E(ÄDÄE(ÄE(Ä$E(Ä%8E#ÅE(Å#E(rv   Útimeoutc               ó(   € V ^8„  d   QhRRRRRRRR/# )	r&   r   r   rv   r   r™   zOptional[int]r'   r   r   )r(   s   "r$   r)   r)   /  s8   € ÷ !Añ !AØð!Að ð!Að ð	!Að
 ñ!Ar#   c               ó"  € \        V 4      w  r4Vf   \        V RV4      # \        VP                  R7      p\	        RT R\
        \        P                  ! R/ \        V4      BR\        VP                  4      RVP                  /B RT;'       g    ^´\        R7      w  rdVf   \        V RV4      # \        V\        4      pVf   \        V RR	4      # T;'       g    \        4       pVP!                  R
4      '       g   \#        W7WX4      # \%        WWX4      # )z Decompose a triage task into a graph of child tasks. Expected failures
(not in triage, no aux client, API error, malformed/empty reply) surface
as ``ok=False``.F)ri   Ú	decomposeÚkanban_decomposerrM   r_   i   )Úaux_taskÚsystemÚuserÚ
max_tokensr™   ÚlogzLLM returned malformed JSONr   r   )r   r   rq   r]   r	   Ú_SYSTEM_PROMPTÚ_USER_TEMPLATEr(   r   r[   rM   r_   rG   r
   Ú	_FENCE_REr   r6   r„   r˜   )	r   rv   r™   rs   r   ru   Úrawrt   Úaudit_authors	   &$$      r$   Údecompose_taskr¨   /  s  € ô % WÓ-�L€DØ‚|Ü ¨°Ó7Ð7ä¨$¯-©-Ô8€GÜØ�WÐ':Ä>Ü×"Ò"ñ 
Ü! $Ó'ñ
ä! '§.¡.Ó1ð
ð %×5Ñ5ò
ð
  § ¨C´Vô�K€Cð ‚{Ü ¨°Ó7Ð7ä ¤YÓ/€FØ‚~Ü ¨Ð0MÓNÐNà×.Ð.œ_Ó.€LØ�:‰:�h×ÒÜ˜T¨7ÓAÐAÜ˜¨'Ó@Ð@r#   Útenantc               ó    € V ^8„  d   QhRRRR/# )r&   r©   r   r'   z	list[str]r   )r(   s   "r$   r)   r)   S  s   € ÷ $ñ $˜}ð $¸	ñ $r#   c           	     óè   € \         P                  ! 4       ;_uu_ 4       p\        P                  ! VRV RR7      pRRR4       X Uu. uF  q3P                  NK  	  up#   + '       g   i     L+; iu upi )z/Return task ids currently in the triage column.Útriageiè  )Ústatusr©   ÚlimitN)r|   r}   r~   Ú
list_tasksr{   )r©   rƒ   ÚrowsÚrows   $   r$   Úlist_triage_idsr²   S  sN   € ä	×	Ò	×	Ô	 $Ü�}Š}˜T¨(¸6ÈÔNˆ÷ 
á"Ó#™d�s�FŒF™dÑ#Ð#÷ 
×	üâ#s   žAÁA/ÁA,	).r    Ú
__future__r   ÚloggingÚreÚdataclassesr   Útypingr   Ú
hermes_clir   r~   Úhermes_cli.kanban_db_graphr   r   r|   r   r8   Úhermes_cli.kanban_specifyr	   r
   r   r   r   r   r,   Ú	getLoggerr   rG   r£   r¤   ÚcompileÚ	MULTILINEr¥   r   r?   rP   r[   rd   rf   rq   r„   r‘   r˜   r¨   r²   r   r#   r$   Ú<module>r¾      s  ðñõ" #ã Û 	Ý !Ý å &Ý <Ý /Ý /÷õ õ Ià	×	Ò	˜8Ó	$€ð9€ðx	€ð �JŠJÐ2°B·L±LÓA€	ð ÷$ð $ó ð$õ)ð
ÐPT÷ õ62õ&õAð ÷ð ó ðð°D÷ õ"iõ&õDð:!Að !ð!Að "÷	!AðH$¨t÷ $ñ $r#   