
    i                       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	m	Z	m
Z
mZ ddlmZ ddlmZmZmZmZmZmZmZ ddlmZ ddlmZ  ej        e          ZdidZ G d de          ZdZdZ dZ!dZ"djdZ#dkdZ$dkdZ%dldZ&dmdZ'dndZ(dkd Z)dnd!Z*dod#Z+dpd%Z,dod&Z-dod'Z.dqd*Z/drdsd,Z0drdtd.Z1d/Z2d0Z3djd1Z4dud5Z5dvd>Z6dwdAZ7dxdCZ8	 drdydIZ9dzdTZ:d{dVZ;d|dWZ<	 	 	 d}d~d]Z=dd_Z>ddaZ?ddcZ@ddddddhZAdS )u  Curator — background skill maintenance orchestrator.

The curator is an auxiliary-model task that periodically reviews agent-created
skills and maintains the collection. It runs inactivity-triggered (no cron
daemon): when the agent is idle and the last curator run was longer than
``interval_hours`` ago, ``maybe_run_curator()`` spawns a forked AIAgent to do
the review.

Responsibilities:
  - Auto-transition lifecycle states based on derived skill activity timestamps
  - Spawn a background review agent that can pin / archive / consolidate /
    patch agent-created skills via skill_manage
  - Persist curator state (last_run_at, paused, etc.) in .curator_state

Strict invariants:
  - Only touches agent-created skills (see tools/skill_usage.is_agent_created)
  - Never auto-deletes — only archives. Archive is recoverable.
  - Pinned skills bypass all auto-transitions
  - Uses the auxiliary client; never touches the main session's prompt cache
    )annotationsN)datetime	timedeltatimezone)Path)AnyCallableDictList
NamedTupleOptionalSetget_hermes_homeskill_usagevaluer   returnOptional[str]c                T    | d S t          |                                           }|pd S N)strstrip)r   texts     2/home/piyush/.hermes/hermes-agent/agent/curator.py_strip_aux_credentialr   (   s-    }tu::D<4    c                  <    e Zd ZU dZded<   ded<   ded<   ded<   dS )	_ReviewRuntimeBindingzLProvider/model for the curator review fork plus optional per-slot overrides.r   providermodelr   explicit_api_keyexplicit_base_urlN)__name__
__module____qualname____doc____annotations__ r   r   r   r   /   sB         VVMMMJJJ####$$$$$$r   r            Z   r   c                 *    t                      dz  dz  S )Nskillsz.curator_stater   r)   r   r   _state_filer0   B   s    x'*:::r   Dict[str, Any]c                     d d d d dddS )NFr   )last_run_atlast_run_duration_secondslast_run_summarylast_report_pathpaused	run_countr)   r)   r   r   _default_stater9   F   s#    %)    r   c                     t                      } |                                 st                      S 	 t          j        |                     d                    }t          |t                    rCt                                          fd|	                                D                        S n># t          t          j        f$ r%}t                              d|           Y d }~nd }~ww xY wt                      S )Nutf-8encodingc                N    i | ]!\  }}|v s|                     d           ||"S )_)
startswith).0kvbases      r   
<dictcomp>zload_state.<locals>.<dictcomp>Y   s5    YYY$!Q!t))q||TWGXGX)A)))r   z Failed to read curator state: %s)r0   existsr9   jsonloads	read_text
isinstancedictupdateitemsOSErrorJSONDecodeErrorloggerdebug)pathdataerD   s      @r   
load_staterU   Q   s    ==D;;==  <z$..'.::;;dD!! 	!##DKKYYYY$**,,YYYZZZK	 T)* < < <7;;;;;;;;<s   A?B4 4C/
C**C/rS   Nonec                   t                      }	 |j                            dd           t          j        t          |j                  dd          \  }}	 t          j        |dd          5 }t          j	        | |d	dd
           |
                                 t          j        |                                           d d d            n# 1 swxY w Y   t          j        ||           d S # t          $ r( 	 t          j        |           n# t           $ r Y nw xY w w xY w# t"          $ r(}t$                              d|d           Y d }~d S d }~ww xY w)NTparentsexist_okz.curator_state_z.tmp)dirprefixsuffixwr;   r<   r+   F)indent	sort_keysensure_asciiz Failed to save curator state: %sexc_info)r0   parentmkdirtempfilemkstempr   osfdopenrG   dumpflushfsyncfilenoreplaceBaseExceptionunlinkrN   	ExceptionrP   rQ   )rS   rR   fdtmpfrT   s         r   
save_stateru   `   s   ==DK$666"s4;'7'7@QZ`aaaC	2sW555 %	$!t%PPPP			$$$% % % % % % % % % % % % % % % JsD!!!!! 	 	 		#   	  K K K7TJJJJJJJJJKss   AD' C2 0ACC2 CC2 CC2 2
D$=DD$
DD$DD$$D' '
E1EEr7   boolc                d    t                      }t          |           |d<   t          |           d S Nr7   )rU   rv   ru   )r7   states     r   
set_pausedrz   u   s.    LLE6llE(Our   c                 ^    t          t                                          d                    S rx   )rv   rU   getr)   r   r   	is_pausedr}   {   s"    
  **+++r   c                    	 ddl m}   |             }n4# t          $ r'}t                              d|           i cY d}~S d}~ww xY wt          |t                    si S |                    d          pi }t          |t                    si S |S )zIRead curator.* config from ~/.hermes/config.yaml. Tolerates missing file.r   load_configz%Failed to load config for curator: %sNcurator)hermes_cli.configr   rq   rP   rQ   rJ   rK   r|   )r   cfgrT   curs       r   _load_configr      s    111111kmm   <a@@@						 c4   	
'')


"Cc4   	Js    
A?AAc                 d    t                      } t          |                     dd                    S )z)Default ON when no config says otherwise.enabledT)r   rv   r|   r   s    r   
is_enabledr      s'    
..C	4(()))r   intc                     t                      } 	 t          |                     dt                              S # t          t
          f$ r
 t          cY S w xY w)Ninterval_hours)r   r   r|   DEFAULT_INTERVAL_HOURS	TypeError
ValueErrorr   s    r   get_interval_hoursr      sZ    
..C&377+-CDDEEEz" & & &%%%%&   '8 AAfloatc                     t                      } 	 t          |                     dt                              S # t          t
          f$ r
 t          cY S w xY w)Nmin_idle_hours)r   r   r|   DEFAULT_MIN_IDLE_HOURSr   r   r   s    r   get_min_idle_hoursr      sZ    
..C&SWW-/EFFGGGz" & & &%%%%&r   c                     t                      } 	 t          |                     dt                              S # t          t
          f$ r
 t          cY S w xY w)Nstale_after_days)r   r   r|   DEFAULT_STALE_AFTER_DAYSr   r   r   s    r   get_stale_after_daysr      sZ    
..C(377-/GHHIIIz" ( ( (''''(r   c                     t                      } 	 t          |                     dt                              S # t          t
          f$ r
 t          cY S w xY w)Narchive_after_days)r   r   r|   DEFAULT_ARCHIVE_AFTER_DAYSr   r   r   s    r   get_archive_after_daysr      sZ    
..C*377/1KLLMMMz" * * *))))*r   tsOptional[datetime]c                d    | sd S 	 t          j        |           S # t          t          f$ r Y d S w xY wr   )r   fromisoformatr   r   )r   s    r   
_parse_isor      sL     t%b)))z"   tts    //nowc                   t                      sdS t                      rdS t                      }t          |                    d                    }|| t          j        t          j                  } 	 | 	                                |d<   d|d<   t          |           n2# t          $ r%}t                              d|           Y d}~nd}~ww xY wdS | t          j        t          j                  } |j         |                    t          j                  }t!          t#                                }| |z
  |k    S )	u  Return True if the curator should run immediately.

    Gates:
      - curator.enabled == True
      - not paused
      - last_run_at present AND older than interval_hours

    First-run behavior: when there is no ``last_run_at`` (fresh install, or
    install that predates the curator), we DO NOT run immediately. The
    curator is designed to run after at least ``interval_hours`` (7 days by
    default) of skill activity, not on the first background tick after
    ``hermes update``. On first observation we seed ``last_run_at`` to "now"
    and defer the first real pass by one full interval. Users who want to
    run it sooner can always invoke ``hermes curator run`` (with or without
    ``--dry-run``) explicitly — that path bypasses this gate.

    The idle check (min_idle_hours) is applied at the call site where we know
    whether an agent is actively running — here we only enforce the static
    gates.
    Fr3   Nuu   deferred first run — curator seeded, will run after one interval; use `hermes curator run --dry-run` to preview nowr5   z&Failed to seed curator last_run_at: %stzinfo)hours)r   r}   rU   r   r|   r   r   r   utc	isoformatru   rq   rP   rQ   r   rn   r   r   )r   ry   lastrT   intervals        r   should_run_nowr      sF   * << u{{ uLLEeii..//D| ;,x|,,C	F#&==??E- N $% u 	F 	F 	FLLA1EEEEEEEE	Fu
{l8<(({||8<|00133444H$J8##s   4+B   
C*C

CDict[str, int]c                   ddl m} | t          j        t          j                  } | t          t                                z
  }| t          t                                z
  }ddddd}|	                                D ]q}|dxx         dz  cc<   |d         }|
                    d	          r1t          |
                    d
                    }|p#t          |
                    d                    p| }|j         |                    t          j                  }|
                    d|j                  }	||k    r6|	|j        k    r+|                    |          \  }
}|
r|dxx         dz  cc<   ||k    r8|	|j        k    r-|                    ||j                   |dxx         dz  cc<   5||k    r6|	|j        k    r+|                    ||j                   |dxx         dz  cc<   s|S )zWalk every agent-created skill and move active/stale/archived based on
    the latest real activity timestamp. Pinned skills are never touched.
    Returns a counter dict describing what changed.r   r   N)days)marked_stalearchivedreactivatedcheckedr      namepinnedlast_activity_at
created_atr   ry   r   r   r   )toolsr   r   r   r   r   r   r   r   agent_created_reportr|   r   r   rn   STATE_ACTIVESTATE_ARCHIVEDarchive_skill	set_stateSTATE_STALE)r   _ustale_cutoffarchive_cutoffcountsrowr   last_activityanchorcurrentok_msgs               r   apply_automatic_transitionsr      s,    ('''''
{l8<(((<(>(>????L9*@*B*BCCCCNQqQOOF&&(( ' 'yQ6{778 	"377+=#>#>?? J*SWW\-B-B"C"CJs= ^^8<^88F'''2?33^##23D(D(D''--HB (z"""a'"""|##2?(B(BLLr~...>"""a'""""l""w".'@'@LLr///=!!!Q&!!!Mr   u  ═══════════════════════════════════════════════════════════════
DRY-RUN — REPORT ONLY. DO NOT MUTATE THE SKILL LIBRARY.
═══════════════════════════════════════════════════════════════

This is a PREVIEW pass. Follow every instruction below EXCEPT:

  • DO NOT call skill_manage with action=patch, create, delete, write_file, or remove_file.
  • DO NOT call terminal to mv skill directories into .archive/.
  • DO NOT call terminal to mv, cp, rm, or rewrite any file under ~/.hermes/skills/.
  • skills_list and skill_view are FINE — read as much as you need.

Your output IS the deliverable. Produce the exact same human-readable summary and structured YAML block you would produce on a live run — but describe the actions you WOULD take, not actions you took. A downstream reviewer will read the report and decide whether to approve a live run with `hermes curator run` (no flag).

If you accidentally take a mutating action, say so explicitly in the summary so the reviewer can revert it.
═══════════════════════════════════════════════════════════════u  You are running as Hermes' background skill CURATOR. This is an UMBRELLA-BUILDING consolidation pass, not a passive audit and not a duplicate-finder.

The goal of the skill collection is a LIBRARY OF CLASS-LEVEL INSTRUCTIONS AND EXPERIENTIAL KNOWLEDGE. A collection of hundreds of narrow skills where each one captures one session's specific bug is a FAILURE of the library — not a feature. An agent searching skills matches on descriptions, not on exact names; one broad umbrella skill with labeled subsections beats five narrow siblings for discoverability, not the other way around.

The right target shape is CLASS-LEVEL skills with rich SKILL.md bodies + `references/`, `templates/`, and `scripts/` subfiles for session-specific detail — not one-session-one-skill micro-entries.

Hard rules — do not violate:
1. DO NOT touch bundled or hub-installed skills. The candidate list below is already filtered to agent-created skills only.
2. DO NOT delete any skill. Archiving (moving the skill's directory into ~/.hermes/skills/.archive/) is the maximum destructive action. Archives are recoverable; deletion is not.
3. DO NOT touch skills shown as pinned=yes. Skip them entirely.
4. DO NOT use usage counters as a reason to skip consolidation. The counters are new and often mostly zero. Judge overlap on CONTENT, not on use_count. 'use=0' is not evidence a skill is valuable; it's absence of evidence either way.
5. DO NOT reject consolidation on the grounds that 'each skill has a distinct trigger'. Pairwise distinctness is the wrong bar. The right bar is: 'would a human maintainer write this as N separate skills, or as one skill with N labeled subsections?' When the answer is the latter, merge.

How to work — not optional:
1. Scan the full candidate list. Identify PREFIX CLUSTERS (skills sharing a first word or domain keyword). Examples you are likely to find: hermes-config-*, hermes-dashboard-*, gateway-*, codex-*, ollama-*, anthropic-*, gemini-*, mcp-*, salvage-*, pr-*, competitor-*, python-*, security-*, etc. Expect 10-25 clusters.
2. For each cluster with 2+ members, do NOT ask 'are these pairs overlapping?' — ask 'what is the UMBRELLA CLASS these skills all serve? Would a maintainer name that class and write one skill for it?' If yes, pick (or create) the umbrella and absorb the siblings into it.
3. Three ways to consolidate — use the right one per cluster:
   a. MERGE INTO EXISTING UMBRELLA — one skill in the cluster is already broad enough to be the umbrella (example: `pr-triage-salvage` for the PR review cluster). Patch it to add a labeled section for each sibling's unique insight, then archive the siblings.
   b. CREATE A NEW UMBRELLA SKILL.md — no existing member is broad enough. Use skill_manage action=create to write a new class-level skill whose SKILL.md covers the shared workflow and has short labeled subsections. Archive the now-absorbed narrow siblings.
   c. DEMOTE TO REFERENCES/TEMPLATES/SCRIPTS — a sibling has narrow-but-valuable session-specific content. Move it into the umbrella's appropriate support directory:
      • `references/<topic>.md` for session-specific detail OR condensed knowledge banks (quoted research, API docs excerpts, domain notes, provider quirks, reproduction recipes)
      • `templates/<name>.<ext>` for starter files meant to be copied and modified
      • `scripts/<name>.<ext>` for statically re-runnable actions (verification scripts, fixture generators, probes)
      Then archive the old sibling. Use `terminal` with `mkdir -p ~/.hermes/skills/<umbrella>/references/ && mv ... <umbrella>/references/<topic>.md` (or templates/ / scripts/).
4. Also flag skills whose NAME is too narrow (contains a PR number, a feature codename, a specific error string, an 'audit' / 'diagnosis' / 'salvage' session artifact). These almost always belong as a subsection or support file under a class-level umbrella.
5. Iterate. After one consolidation round, scan the remaining set and look for the NEXT umbrella opportunity. Don't stop after 3 merges.

Your toolset:
  - skills_list, skill_view        — read the current landscape
  - skill_manage action=patch      — add sections to the umbrella
  - skill_manage action=create     — create a new umbrella SKILL.md
  - skill_manage action=write_file — add a references/, templates/, or scripts/ file under an existing skill (the skill must already exist)
  - skill_manage action=delete     — archive a skill. MUST pass `absorbed_into=<umbrella>` when you've merged its content into another skill, or `absorbed_into=""` when you're truly pruning with no forwarding target. This drives cron-job skill-reference migration — guessing from your YAML summary after the fact is fragile.
  - terminal                       — mv a sibling into the archive OR move its content into a support subfile

'keep' is a legitimate decision ONLY when the skill is already a class-level umbrella and none of the proposed merges would improve discoverability. 'This is narrow but distinct from its siblings' is NOT a reason to keep — it's a reason to move it under an umbrella as a subsection or support file.

Expected output: real umbrella-ification. Process every obvious cluster. If you end the pass with fewer than 10 archives, you stopped too early — go back and look at the clusters you left alone.

When done, write a human summary AND a structured machine-readable block so downstream tooling can distinguish consolidation from pruning. Format EXACTLY:

## Structured summary (required)
```yaml
consolidations:
  - from: <old-skill-name>
    into: <umbrella-skill-name>
    reason: <one short sentence — why merged, not just 'similar'>
prunings:
  - name: <skill-name>
    reason: <one short sentence — why archived with no merge target>
```

Every skill you moved to .archive/ MUST appear in exactly one of the two lists. If you consolidated X into umbrella Y (patched Y, wrote a references file to Y, or created Y with X's content absorbed), X goes under `consolidations` with `into: Y`. If you archived X with no absorption — truly stale, irrelevant, or obsolete — X goes under `prunings`. Leave a list empty (`consolidations: []`) if none. Do not omit the block. The block comes AFTER your human-readable summary of clusters processed, patches made, and decisions left alone.c                     t                      dz  dz  } 	 |                     dd           n2# t          $ r%}t                              d|           Y d}~nd}~ww xY w| S )ux  Directory where curator run reports are written.

    Lives under the profile-aware logs dir (``~/.hermes/logs/curator/``)
    alongside ``agent.log`` and ``gateway.log`` so it's found by anyone
    looking for operational telemetry, not mixed in with the user's
    authored skill data in ``~/.hermes/skills/``.

    ``ensure_hermes_home()`` pre-creates this dir on every CLI launch and
    the v22→v23 migration backfills it for existing profiles, but we
    still mkdir here as a belt-and-suspenders so the curator works even
    from an odd entry path (e.g. gateway-only install, bare library use)
    that bypasses both.
    logsr   TrX   z%Curator reports dir create failed: %sN)r   re   rN   rP   rQ   )rootrT   s     r   _reports_rootr     s     v%	1DA

4$
//// A A A<a@@@@@@@@AKs   . 
AAAneedler   rR   c                   |                      dd          }|                     dd                              d          D ]D}|sd|v r|                    dd          d         n|}|                     dd          |k    r dS Ed	S )
af  Check if *needle* is a complete filename stem or directory name in *path*.

    Unlike simple substring matching, this avoids false positives where short
    skill names are embedded in longer filenames (e.g. "api" matching
    "references/api-design.md").  Hyphens and underscores are normalised so
    "open-webui-setup" matches "open_webui_setup.md".
    -r?   \/.r   r   TF)rn   splitrsplit)r   rR   norm_needlepartstems        r   _needle_in_path_componentr     s     ..c**KT3''--c22   	),t{{3""1%%$<<S!![0044 15r   removed	List[str]addedafter_namesSet[str]
tool_callsList[Dict[str, Any]]Dict[str, List[Dict[str, Any]]]c                "   g }g }g }|pg D ]}t          |t                    s|                    d          dk    r2|                    d          pd}i }	t          |t                    r|}	n?t          |t                    r*	 t	          j        |          }	n# t          $ r d|i}	Y nw xY wt          |	t                    s|                    |	           t          |          t          |pg           z  }
| D ]}|sd}d}||	                    dd          |	                    dd          h}|D ]}	|	                    d          }t          |t                    r|s0||k    r7||
vr<g }d	D ]C}|	                    |          }t          |t                    r|                    ||f           Dd
}|D ]\  }}|D ]}|s|dk    rt          ||          }n8t          t          j        dt          j        |           d|                    }|r.d}d|	                    dd           d| d| d|dd          } n|r n|r|} n!|r|                    |||d           w|                    d|i           ||dS )u  Split ``removed`` into consolidated vs pruned.

    A removed skill is "consolidated" when the curator absorbed its content
    into another skill (an umbrella) during this run — the content still
    lives, just under a different name. A removed skill is "pruned" when the
    curator archived it for staleness/irrelevance without preserving its
    content elsewhere.

    Heuristic: scan this run's ``skill_manage`` tool calls and look for
    ``write_file``/``patch``/``create``/``edit`` actions whose target skill
    (the ``name`` argument) is NOT the removed skill and whose
    ``file_path`` / ``file_content`` / ``content`` arguments reference the
    removed skill's name. That's the textbook "absorbed into umbrella"
    signal. Ties are broken by first-match (earliest tool call wins).

    Returns ``{"consolidated": [{"name", "into", "evidence"}, ...],
               "pruned":       [{"name"}, ...]}``.
    r   skill_manage	arguments _rawNr   r?   )	file_pathfile_contentcontent
new_stringr   Fr   z\bTzskill_manage action=action?z on 'z' referenced 'z' in P   )r   intoevidenceconsolidatedpruned)rJ   rK   r|   r   rG   rH   rq   appendsetrn   r   rv   researchescape)r   r   r   r   r   r   parsed_callstcrawargsdestinationsr   r   r   needlestarget	haystackskeyrC   hithayr   matcheds                          r   _classify_removed_skillsr    s   0 *,L#%F *,LB " ""d## 	66&>>^++ff[!!'R!c4   	%DDS!! 	%%z# % % % }% $%% 	D!!!!
 {##c%+2&6&66L >* >* 	""& c3//c31G1GH  0	 0	DXXf%%Ffc** &  ~~ \)) 02IU / /HHSMMa%% /$$c1X...C%  S%  F! ! k))";FC"H"H"&I&ABIf,=,=&A&A&A3GG# #  "-488Hc3J3J - -#)- -9=- -"%crc(- - !
   E   	*t R RSSSSMM64.))))(F;;;s   BB*)B*	llm_finalDict[str, List[Dict[str, str]]]c           	        g g d}| rt          | t                    s|S ddl} |j        d| |j        |j        z            }|s|S |                    d          }	 ddl}|                    |          }n# t          $ r |cY S w xY wt          |t                    s|S g g d}|                    d          pg }|                    d          pg }	t          |t                    r|D ]}
t          |
t                    s|
                    d          }|
                    d	          }t          |t                    r=|                                r)t          |t                    r|                                s|
                    d
          }|d                             |                                |                                t          |t                    r|pd                                ndd           t          |	t                    r|	D ]}
t          |
t                    s|
                    d          }t          |t                    r|                                sW|
                    d
          }|d                             |                                t          |t                    r|pd                                ndd           |S )ui  Extract the structured YAML block from the curator's final response.

    The curator prompt requires a fenced ```yaml block under
    ``## Structured summary (required)`` with ``consolidations:`` and
    ``prunings:`` lists. This parses it tolerantly:

    - Missing block → returns empty lists (we'll fall back to heuristic).
    - Malformed YAML → returns empty lists and we rely on heuristic.
    - Partial block (e.g. only consolidations) → returns what we could parse.

    Returns ``{"consolidations": [{"from", "into", "reason"}, ...],
               "prunings":       [{"name", "reason"}, ...]}``.
    )consolidationspruningsr   Nz```ya?ml\s*\n(.*?)\n```r   r
  r  fromr   reasonr   )r  r   r  r   )r   r  )rJ   r   r   r   DOTALL
IGNORECASEgroupyaml	safe_loadrq   rK   r|   listr   r   )r  emptyr   matchbodyr  rS   outcons_rawprun_rawentryfrmr   r  r   s                  r   _parse_structured_summaryr  e  s      "r22E Jy#66 
 IIIBI"
	BM! E
  ;;q>>D~~d##    dD!! >@b+Q+QCxx())/RHxx
##)rH(D!!  	 	EeT** ))F##C99V$$DsC(( SYY[[ "4--26**,,YYx((F !((		

4>vs4K4KS6<R..000QS* *     (D!!  
	 
	EeT** 99V$$DtS)) djjll YYx((F
O""

4>vs4K4KS6<R..000QS$ $    
 Js   A6 6BBDict[str, Dict[str, Any]]c                   i }| pg D ]q}t          |t                    s|                    d          dk    r3|                    d          pd}i }t          |t                    r|}n;t          |t                    r&	 t	          j        |          }n# t          $ r Y w xY wt          |t                    s|                    d          dk    r|                    d          }t          |t                    r|                                sd|vr|                    d          }|.t          |t                    sE|                                d	d
||                                <   s|S )u  Walk this run's tool calls and extract model-declared absorption targets.

    The curator prompt requires every ``skill_manage(action='delete')`` call
    to pass ``absorbed_into=<umbrella>`` when consolidating, or
    ``absorbed_into=""`` when truly pruning. This is the single authoritative
    signal for classification — the model's own declaration at the moment of
    deletion, which beats both post-hoc YAML summary parsing and substring
    heuristics on other tool calls.

    Returns ``{skill_name: {"into": "<umbrella>" | "", "declared": True}}``.
    Entries with ``into == ""`` are explicit prunings.
    Skills without a ``skill_manage(delete)`` call, or with one that omitted
    ``absorbed_into``, are not in the returned dict — caller falls back to
    the existing heuristic/YAML logic for those (backward compat with older
    curator runs and any callers that don't populate the arg).
    r   r   r   r   r   deleteabsorbed_intoNT)r   declared)rJ   rK   r|   r   rG   rH   rq   r   )r   r  r   r   r   r   r   s          r   #_extract_absorbed_into_declarationsr"    s   & &(CB G G"d## 	66&>>^++ff[!!'R!c4   	DDS!! 	z#   $%% 	88H))xx$$$ 	DJJLL 	 $&&/**>&#&& 	%+\\^^FFDJJLLJs   B
B#"B#	heuristicmodel_blockr   absorbed_declarations#Optional[Dict[str, Dict[str, Any]]]c                   d |                     dg           D             }d |                     dg           D             }d |                     dg           D             }d |                     dg           D             }|pi }	g }
g }| D ]e}|                     |          }|                     |          }|                     |          }|	                     |          }||                     d
d          }|r\||v rX||d|r|                     d          pdndd}|r |                     d          r|d         |d<   |
                    |           |dk    r4|                    |d|r|                     d          pdndd           |ry|                     d
          |v rb||d
         d|rdndz   |                     d          pdd}|r |                     d          r|d         |d<   |
                    |           |rq|                     d
          |vrZ|r=|
                    ||d
         dd|                     dd          |d
         d           n|                    |ddd           |r7|
                    ||d
         dd|                     dd          d           .|r|                     dd          nd}|                    ||rdnd|d           g|
|dS )uL  Merge heuristic (tool-call evidence) with the model's structured block.

    Rules (evaluated in order; first match wins):
    - **Model-declared `absorbed_into` at delete time is authoritative.** Any
      entry in ``absorbed_declarations`` beats every other signal. This is
      the model telling us directly, at the moment of deletion, what it did.
      ``into != ""`` and target exists → consolidated. ``into == ""`` →
      pruned. ``into != ""`` but target doesn't exist → hallucination; fall
      through to the usual signals.
    - Model-declared consolidation wins when its ``into`` target exists
      in ``destinations`` (survived or newly-created). This gives the
      model authority over intent + rationale.
    - Model-declared consolidation whose ``into`` target does NOT exist is
      downgraded: the model hallucinated an umbrella. We prefer the
      heuristic's finding for that skill, or fall back to pruned.
    - Heuristic-only finding (model didn't mention it, tool calls confirm)
      is preserved as a consolidation, marked ``source="tool-call audit"``.
    - Model-declared pruning is accepted unless the heuristic has
      tool-call evidence that contradicts it (rare — the heuristic would
      have flagged consolidation). In that case we log both.

    Every removed skill is placed in exactly one bucket.
    c                     i | ]}|d          |S r   r)   rA   rT   s     r   rE   z-_reconcile_classification.<locals>.<dictcomp>
  s    III!6AIIIr   r   c                    h | ]
}|d          S r)  r)   r*  s     r   	<setcomp>z,_reconcile_classification.<locals>.<setcomp>  s    BBB1V9BBBr   r   c                     i | ]}|d          |S )r  r)   r*  s     r   rE   z-_reconcile_classification.<locals>.<dictcomp>  s    NNN1!F)QNNNr   r
  c                     i | ]}|d          |S r)  r)   r*  s     r   rE   z-_reconcile_classification.<locals>.<dictcomp>  s    JJJQAfIqJJJr   r  Nr   r   z(absorbed_into (model-declared at delete)r  )r   r   sourcer  r   z'absorbed_into="" (model-declared prune))r   r/  r  r!   z+auditz.tool-call audit (model named missing umbrella))r   r   r/  r  r   model_claimed_intoz>fallback (model named missing umbrella, no tool-call evidence)z5tool-call audit (model omitted from structured block))r   r   r/  r  r   zno-evidence fallbackr   )r|   r   )r   r#  r$  r   r%  	heur_consheur_pruned
model_consmodel_prunedr!  r   r   r   mcmphcdec
into_claimr  r  s                       r   _reconcile_classificationr:    s   < JIy}}^R'H'HIIIIBBimmHb&A&ABBBKNN8H"(M(MNNNJJJ+//*b*I*IJJJL$*H)+L#%F U U^^D!!d##]]4  ll4   ?,,J 
jL88 &H:<Drvvh//52"	) )  7"&&,, 7(*:E*%##E***R I:<Drvvh//52"    
   
	"&&..L006
!%;XX<&&**0b	% %E  3bffZ(( 3$&zNj!&&&  	"&&..44 ## vJN  "z2 6 6*,V*% %      ^     
   	6
QFF:r22! !     *,3"%%%!#?gg)?
 
 	 	 	 	 )F;;;r   
started_atr   elapsed_secondsauto_countsauto_summarybefore_reportbefore_namesafter_reportllm_metaOptional[Path]c                   t                      }	 |                    dd           n3# t          $ r&}	t                              d|	           Y d}	~	dS d}	~	ww xY w|                     d          }
||
z  }d}|                                r#|dz  }||
 d| z  }|                                #	 |                    dd           n3# t          $ r&}	t                              d	|	           Y d}	~	dS d}	~	ww xY wd
 |D             }t          |                                          }t          ||z
            }t          ||z
            }d |D             }g }t          ||z            D ]y}|
                    |          pi 
                    d          }|
                    |          pi 
                    d          }|r!|r||k    r|                    |||d           zi }|
                    dg           pg D ]4}|
                    dd          }|
                    |d          dz   ||<   5t          ||||
                    dg           pg           }t          |
                    dd          pd          }t          |          t          |pg           z  }t          |
                    dg           pg           }t          |||||          }|d         }|d         }g ddd}	 d |D             } d |D             }!| s|!rddlm}"  |"| |!          }nH# t          $ r;}	t                              d|	d           g ddt%          |	          d}Y d}	~	nd}	~	ww xY wi d |                                 d!t)          |d"          d#|
                    d#d          d$|
                    d$d          d%|d&t+          |          t+          |          t+          |          t+          |          z
  t+          |          t+          |          t+          |          t+          |          t+          |          t-          |
                    d'd                    t/          |                                          d(
d)|d*|d|d|d+d, |D             d-|d.|d/|d0|
                    dd          d1|
                    d2d          d3|
                    d4          d|
                    dg           i}#	 |d5z                      t5          j        |#d"d6          d7z   d89           n2# t          $ r%}	t                              d:|	           Y d}	~	nd}	~	ww xY w	 t9          |#          }$|d;z                      |$d89           n2# t          $ r%}	t                              d<|	           Y d}	~	nd}	~	ww xY w	 t-          |
                    d'd                    dk    r2|d=z                      t5          j        |d"d6          d7z   d89           n2# t          $ r%}	t                              d>|	           Y d}	~	nd}	~	ww xY w|S )?u   Write run.json + REPORT.md under logs/curator/{YYYYMMDD-HHMMSS}/.

    Returns the report directory path on success, None if the write
    couldn't happen (caller logs and continues — reporting is best-effort).
    TrX   z$Curator report dir create failed: %sNz%Y%m%d-%H%M%Sr   r   Fz!Curator run dir create failed: %sc                d    i | ]-}t          |t                    |                    d           |.S r)  rJ   rK   r|   rA   rs     r   rE   z%_write_run_report.<locals>.<dictcomp>  s3    SSS!z!T?R?RSQUU6]]ASSSr   c                d    i | ]-}t          |t                    |                    d           |.S r)  rF  rG  s     r   rE   z%_write_run_report.<locals>.<dictcomp>  s3    UUU1AtATATUaeeFmmQUUUr   ry   )r   r  tor   r   unknownr   )r   r   r   r   finalr   )r   r#  r$  r   r%  r   r   )rewritesjobs_updatedjobs_scannedc                    i | ]P}t          |t                    |                    d           ,|                    d          A|d          |d         QS )r   r   rF  r*  s     r   rE   z%_write_run_report.<locals>.<dictcomp>  sj     
 
 
!T""
 ()uuV}}
 :;v
fIqy
 
 
r   c                r    g | ]4}t          |t                    |                    d           ,|d          5S r)  rF  r*  s     r   
<listcomp>z%_write_run_report.<locals>.<listcomp>  sN     
 
 
!T""
'(uuV}}
fI
 
 
r   )rewrite_skill_refsr   z%Curator cron skill rewrite failed: %srb   )rM  rN  rO  errorr;  duration_secondsr+   r!   r    auto_transitionsr   rN  )
beforeafterdeltaarchived_this_runadded_this_runconsolidated_this_runpruned_this_runstate_transitionscron_jobs_rewrittentool_calls_totaltool_call_countsr   pruned_namesc                    g | ]
}|d          S r)  r)   )rA   ps     r   rR  z%_write_run_report.<locals>.<listcomp>  s    333q6333r   r   r^  cron_rewritesr  llm_summarysummary	llm_errorrT  zrun.json)r_   ra   
r;   r<   z!Curator run.json write failed: %sz	REPORT.mdz"Curator REPORT.md write failed: %szcron_rewrites.jsonz+Curator cron_rewrites.json write failed: %s)r   re   rq   rP   rQ   strftimerF   r   keyssortedr|   r   r  r  r"  r:  	cron.jobsrS  r   r   roundlenr   sumvalues
write_textrG   dumps_render_report_markdown)%r;  r<  r=  r>  r?  r@  rA  rB  r   rT   stamprun_dirr]   after_by_namer   r   r   before_by_nametransitionsr   s_befores_after	tc_countsr   r#  r$  r   r%  classificationr   r   re  consolidated_maprb  _rewrite_cron_refspayloadmds%                                        r   _write_run_reportr  o  s     ??D

4$
////   ;Q???ttttt 00EUlGF
..

 -!E,,F,,, ..

 -dU3333   8!<<<ttttt
 TS|SSSMm((**++K\K/00G;-..EUUUUUN )+K{\122 P P"&&t,,277@@ $$T**0b55g>> 	P 	PH$7$7hgNNOOO !#Ill<,,2 5 5vvfi((#--a0014	$  )<<b117R	  I ,HLL",E,E,KLLK{##c%+2&6&66L @\2&&,"  /!3  N ".1LH%F 24QXY$Z$ZM

 
!
 
 


 
%
 
 
  	| 	JJJJJJ..-#  M  
 
 
<a$OOOVV	
 

j**,,E/155 	gr** 	HLLR00	
 	K 	,''%%%%L(9(99!$W!%jj%(%6%6"6{{!$[!1!1#&}'8'8'K'K#L#L #I$4$4$6$6 7 7
 
$ 	I%& 	G'( 	)* 	&+, 	33F333-. 	/0 	[12 	34 	X\\'2..56 	x||Ir2278 	X\\'**9: 	hll<44; GB=	:	))Jwqu===D 	* 	
 	
 	
 	
  = = =8!<<<<<<<<=>$W--	;	**2*@@@@ > > >91========>
G}  3344q88++77
=GGG$N  8     G G GBAFFFFFFFFG Ns   ( 
AAA/C 
C7C22C73/L# #
M(-1M##M(	2T< <
U+U&&U+/)V 
W#WWAX& &
Y0YYrd  c                   g }|                      dd          }|                      dd          pd}t          t          |          d          \  }}|r| d| dn| d}|                    d| d	           |                      d
          pd}|                      d          pd}|                      d          pi }	|                    d| d| d| d|	                     dd           d|	                     dd           d|	                     dd          dd           |                      d          }
|
r|                    d|
 d           |                      d          pi }|                    d           |                    d|                     dd                      |                    d |                     d!d                      |                    d"|                     d#d                      |                    d$|                     d%d                      |                    d           |                      d&          pi }|                    d'           |                    d(|	                     d)d           d*d+                    d, t          |                                          D                       pd- d.           |                    d/|	                     d0d           d1           |                    d2|	                     d3d           d1           |                    d4|	                     d5d           d1           |                    d6|	                     d7d           d1           |                    d           |                      d8          pg }|r|                    d9t          |           d           |                    d:           d;}|d<|         D ]}|                     d=d>          }|                     d?d>          }|                     d@          pd                                }|                     dAd          }dB| dC| dD}|r|dE| z  }|r|	                    dF          r	|dG| dHz  }|                    |           |                     dI          r|                    dJ|dI          dK           t          |          |k    r)|                    dLt          |          |z
   dM           |                    d           |                      dN          pg }|r.|                    dOt          |           d           |                    dP           d;}|d<|         D ]}t          |t                    re|                     d=d>          }|                     d@          pd                                }dB| dD}|r|dE| z  }|                    |           ||                    dB| dD           t          |          |k    r)|                    dLt          |          |z
   dM           |                    d           |                      dQ          pg }|rn|                    dRt          |           d           |                    dS           |D ]}|                    dB| dD           |                    d           |                      d7          pg }|r|                    dTt          |           d           |D ]Y}|                    dB|                     d=           dU|                     dV           d|                     dW                      Z|                    d           |                      dX          pi }|                     dY          pg }|r|                    dZt          |           d           |                    d[           d\}|d<|         D ]&}|                     d]          p|                     d^          pd>}|                     d          pg }|                     d          pg }|                     d_          pi }|                     d`          pg } |                    dB| dad+                    |           dbd+                    |          pdc dD           |                                D ]!\  }!}"|                    dd|! db|" de           "| D ]}|                    dd| df           (t          |          |k    r)|                    dLt          |          |z
   dg           |                    d           |                      dh          pd                                }#|#r@|                    di           |                    |#           |                    d           nZ|
sX|                      dj          pd}$|$r?|                    dk           |                    |$           |                    d           |                    dl           |                    dm           |                    dn           |                    do           |                    d           d	                    |          S )pz!Render the human-readable report.r;  r   rU  r   <   zm su   # Curator run — ri  r!   z(not resolved)r    r   zModel: `z` via `u   `  ·  Duration: u     ·  Agent-created skills: rW  u    → rX  z (rY  z+dz)
rh  u   > ⚠ LLM pass error: `z`
rV  z### Auto-transitions (pure, no LLM)
z- checked: r   z- marked stale: r   z0- archived (no LLM, pure time-based staleness): r   z- reactivated: r   ra  z## LLM consolidation pass
z- tool calls: **r`  z** (by name: , c              3  *   K   | ]\  }}| d | V  dS )=Nr)   )rA   rB   rC   s      r   	<genexpr>z*_render_report_markdown.<locals>.<genexpr>R  s0      'Y'Ytq!1

q

'Y'Y'Y'Y'Y'Yr   none)z!- consolidated into umbrellas: **r\  z**z%- pruned (archived for staleness): **r]  z- new skills this run: **r[  u7   - state transitions (active ↔ stale ↔ archived): **r^  r   z'### Consolidated into umbrella skills (u$  _These skills were **absorbed into another skill** during this run — their content still lives, just under a different name. The original directory was moved to `~/.hermes/skills/.archive/` for safety and can be restored via `hermes curator restore <name>` if the consolidation was wrong._
2   Nr   r   r   r  r/  z- `u   ` → merged into ``u    — ztool-call auditz  _(detected via z)_r0  u#     ⚠ The curator's summary named `zg` as the umbrella but that skill doesn't exist post-run; showing the tool-call audit's finding instead.u
   - … and z more (see `run.json`)r   u'   ### Pruned — archived for staleness (z_These skills were archived without being merged into an umbrella (e.g. stale, unused, or judged irrelevant). Directories live under `~/.hermes/skills/.archive/`. Restore any via `hermes curator restore <name>`._
r   z### New skills this run (zX_Usually these are new class-level umbrellas created via `skill_manage action=create`._
z### State transitions (z`: r  rJ  re  rM  z)### Cron job skill references rewritten (z_Cron jobs that referenced a consolidated or pruned skill were updated in-place so they keep loading the right instructions on their next run. See `cron_rewrites.json` for the full record._
   job_namejob_idmappeddroppedz`: `u   ` → `z(none)z    - `z` (consolidated)z` dropped (pruned)z  more (see `cron_rewrites.json`)r  z## LLM final summary
rf  z## LLM summary
z## Recovery
z<- Restore an archived skill: `hermes curator restore <name>`zR- All archives live under `~/.hermes/skills/.archive/` and are recoverable by `mv`zH- See `run.json` in this directory for the full machine-readable record.)r|   divmodr   r   joinrl  rM   ro  r   r@   rJ   rK   )%rd  linesstarteddurationminssecs	dur_labelr!   provr   rT  autor|  r   SHOWr  r   r   r  r/  liner   r   ntranstcron_rwcron_rewrites_listr  rW  rX  r  r  oldnewrL  llm_sums%                                        r   rt  rt  /  s4   EeeL"%%Guu'++0qHHr**JD$&*:4""4""""4


I	LL1g111222EE'NN..E550 0DUU8__"F	LL	+5 	+ 	+ 	+ 	+	 	+ 	+!'Ha!8!8	+ 	+?Ezz'ST?U?U	+ 	+JJw""*	+ 	+ 	+   EE+E ;9u999::: 55#$$*D	LL7888	LL7txx	15577888	LLADHH^Q$?$?AABBB	LL]DHHZYZD[D[]]^^^	LL?488M1#=#=??@@@	LL ())/RI	LL.///	LL gFJJ/A1$E$E g g"ii'Y'YviooFWFW?X?X'Y'Y'YYYc]cg g g h h h	LL_VZZ@WYZ5[5[___```	LL]DUWX9Y9Y]]]^^^	LLPVZZ8H!-L-LPPPQQQ	LL =jj!4a88= = = > > >	LL 55((.BL Us<?P?PUUUVVV*	
 	
 	
 !%4%( 	 	E99VS))D99VS))Dii))/R6688FYYx,,F999$999D )(((( 7&++,=>> 7 6F6666LLyy-.. E%@T:U E E E  
 |t##LLVc,&7&7$&>VVVWWWR UU8__"F Os6{{OOOPPPB	
 	
 	
 ETE] 	- 	-E %&& -yy--))H--3::<<$T}}} -,F,,,DT""""^5^^^,,,,v;;LLPc&kkD&8PPPQQQR EE'NN bE @U@@@AAApqqq 	% 	%ALLq$$$$R EE%&&,"E >s5zz>>>??? 	T 	TALLRquuV}}RRvRRQUU4[[RRSSSSR
 eeO$$*G Z006B ]EWAXAX]]]^^^R	
 	
 	

 '. 	A 	AEyy,,J		(0C0CJsHYYx((.BFIIg&&,"EYYx((.BFii	**0bGLL]h]]DIIf$5$5]]dii>N>N>ZRZ]]]   #LLNN J JSHsHH3HHHIIII A A?t???@@@@A!""T))LL-S!344t; - - -   	R UU;%2,,..E 	-...UR %%&&," 	LL+,,,LL!!!LL 
LL!!!	LLOPPP	LLefff	LL[\\\	LL99Ur   c                    t          j                    } | sdS dt          |            dg}| D ]}|                    d|d          d|d          d|                    d	          rd
nd d|                    dd           d|                    dd           d|                    dd           d|                    dd           d|                    d          pd            d                    |          S )zCHuman/agent-readable list of agent-created skills with usage stats.z"No agent-created skills to review.zAgent-created skills (z):
z- r   z  state=ry   z	  pinned=r   yesnoz  activity=activity_countr   z  use=	use_countz  view=
view_countz
  patches=patch_countz  last_activity=r   neverri  )r   r   ro  r   r|   r  )rowsr  rH  s      r   _render_candidate_listr    sb   +--D 4335c$ii5556E 

 

D6 D DwZD D uuX8eeDD D .22D D 55a((	D D
 EE,**D D uu]A..D D UU#566A'D D		
 		
 		
 		
 99Ur   F
on_summaryOptional[Callable[[str], None]]synchronousdry_runc                    t          j        t          j                  r@	 t	          j                    }t          |          ddddn# t          $ r
 dddddY nw xY w	 ddlm	} |
                    d          }|( r&	   d|j         d           n# t          $ r Y nw xY wn4# t          $ r'}t                              d	|d
           Y d}~nd}~ww xY wt                    g }d         r|                    d          d           d         r|                    d          d           d         r|                    d          d           |rd                    |          ndt#                      }s@                                |d<   t'          |                    dd                    dz   |d<   rdnd  |d<   t+          |            fd}	|r |	             n+t-          j        |	d
d          }
|
                                                                 dS )u}  Execute a single curator review pass.

    Steps:
      1. Apply automatic state transitions (pure, no LLM).
      2. If there are agent-created skills, spawn a forked AIAgent that runs
         the LLM review prompt against the current candidate list.
      3. Update .curator_state with last_run_at and a one-line summary.
      4. Invoke *on_summary* with a user-visible description.

    If *synchronous* is True, the LLM review runs in the calling thread; the
    default is to spawn a daemon thread so the caller returns immediately.

    If *dry_run* is True, the automatic stale/archive transitions are SKIPPED
    and the LLM review pass is instructed to produce a report only — no
    skill_manage mutations, no terminal archive moves. The REPORT.md still
    gets written and ``state.last_report_path`` still records it so users
    can read what the curator WOULD have done.
    r   )r   r   r   r   )curator_backupzpre-curator-run)r  Nzcurator: snapshot created (r  z#Curator pre-run snapshot failed: %sTrb   )r   r   z marked staler   z	 archivedr   z reactivatedr  z
no changesr3   r8   r   zdry-run auto: zauto: r5   c            
        	 t          j                    } n# t          $ r g } Y nw xY wd | D             }i }	 t                      }d|v r  d}ddddg d d}nOrt           dt
           d| }nt
           d| }t          |          }  d|                    d	d
           }nX# t          $ rK}t          	                    d|d             d| d}dd| dddg t          |          d}Y d }~nd }~ww xY wt          j        t          j                  z
                                  }t!                      }||d<   ||d<   	 t          j                    }	n# t          $ r g }	Y nw xY w	 t#          || ||	|          }
|
t          |
          |d<   n4# t          $ r'}t          	                    d|d           Y d }~nd }~ww xY wt%          |           r"	  d|            d S # t          $ r Y d S w xY wd S )Nc                b    h | ],}t          |t                    |                    d           -S r)  rF  rG  s     r   r,  z8run_curator_review.<locals>._llm_pass.<locals>.<setcomp>Q  s1    TTT!
1d@S@STfTTTr   zNo agent-created skillsz; llm: skipped (no candidates)r   zskipped (no candidates)rL  rg  r!   r    r   rT  z

z; llm: rg  	no changezCurator LLM pass failed: %sTrb   z; llm: error (r  zerror (r4   r5   )r;  r<  r=  r>  r?  r@  rA  rB  r6   zCurator report write failed: %sz	curator: )r   r   rq   r  CURATOR_DRY_RUN_BANNERCURATOR_REVIEW_PROMPT_run_llm_reviewr|   rP   rQ   r   r   r   r   r   total_secondsrU   r  ru   )r?  r@  rB  candidate_listfinal_summarypromptrT   elapsedstate2rA  report_pathr>  r   r  r  r\   starts              r   	_llm_passz%run_curator_review.<locals>._llm_passJ  s/   	'<>>MM 	 	 	MMM	TT}TTT#%#	355N(N::#) W< W W W8 ""$!   L1 , ,0, ,), , F !6KK>KKF*622Z|ZZHLLK4X4XZZ   
	 
	 
	LL6DLIII%G|GG1GGGM)Q>>> Q HHHHHH
	 <--5DDFF.5*+%2!"
	&;==LL 	 	 	LLL		N+  '")+))!	 	 	K &-0-=-=)* 	N 	N 	NLL:ALMMMMMMMM	N 	6 	
6}6677777   	 	sa    &&A2B+ +
D 5AC;;D E# #E21E26+F" "
G,GG(G8 8
HHzcurator-review)r   daemonr   )r;  rV  summary_so_far)r   r   r   r   r   r   ro  rq   agentr  snapshot_skillsr   rP   rQ   r   r   r  rU   r   r   r|   ru   	threadingThreadr  )r  r  r  reportr  snaprT   auto_summary_partsry   r  r  r>  r   r\   r  s   ` `        @@@@r   run_curator_reviewr    s:   . L&&E 8		X 577Fv;; ! 	 FF  	X 	X 	X!"A1UVWWFFF	X		R,,,,,,!119J1KKDJJITYIIIJJJJ    D 	R 	R 	RLL>DLQQQQQQQQ	R,777n L!!VN%;"J"J"JKKKj D!!VJ%7"B"B"BCCCm J!!VM%:"H"H"HIII4FX499/000LL LLE @$00m ;!:!:;;a?k!(6hF#) 9< 9 9E
uQ Q Q Q Q Q Q Q Q Qf  	IdAQRRR				 oo''"&  sM   'A A$#A$( B/ 	B B/ 
B+(B/ *B++B/ /
C 9CC r   c           
         t          |                     d          t                    r|                     di           ni }|                    d          pd}|                    d          p|                    d          pd}t          |                     d          t                    r|                     di           ni }t          |                    d          t                    r|                    di           ni }|                    d          pd                                pd}|                    d          pd                                pd}|rZ|dk    rT|rRt	          ||t          |                    d	                    t          |                    d
                              S t          |                     d          t                    r|                     di           ni }t          |                    d          t                    r|                    di           ni }	|	                    d          pd}
|	                    d          pd}|
r|rt                              d           t	          t          |
          t          |          t          |	                    d	                    t          |	                    d
                              S t	          ||dd          S )ab  Resolve provider/model and per-slot credentials for the curator review fork.

    Same precedence as `_resolve_review_model()`. Non-empty ``api_key`` /
    ``base_url`` from the active slot are returned as explicit overrides so
    ``resolve_runtime_provider`` does not silently reuse the main chat
    credential chain for a routed auxiliary model.
    r!   r    r  defaultr   	auxiliaryr   Napi_keybase_urlu|   curator: using deprecated curator.auxiliary.{provider,model} config — please migrate to auxiliary.curator.{provider,model})	rJ   r|   rK   r   r   r   rP   infor   )r   _main_main_provider_main_model_aux	_cur_task_task_provider_task_model_cur_legacy_legacy_provider_legacy_models               r   _resolve_review_runtimer    s    %/swww/?/?$F$FNCGGGR   BEYYz**4fN))I&&B%))G*<*<BK (2#''+2F2F'M'MU377;###SUD+5dhhy6I6I4+P+PXB'''VXImmJ//52<<>>F$N==))/R6688@DK 
.F22{2$!)--	":":;;!)--
";";<<	
 
 	
 &0	0B0BD%I%IQ3779b!!!rD+5dhh{6K6KT+R+RZdhh{B'''XZG{{:..6$KK((0DM 

M 

N	
 	
 	
 % !!!'++i"8"899!'++j"9"9::	
 
 	
 !dDIIIr   tuple[str, str]c                <    t          |           }|j        |j        fS )u*  Pick (provider, model) for the curator review fork.

    Curator is a regular auxiliary task slot — ``auxiliary.curator.{provider,model}``
    — so it participates in the canonical aux-model plumbing (``hermes model`` →
    auxiliary picker, the dashboard Models tab, ``auxiliary.curator.{timeout,
    base_url,api_key,extra_body}``). ``provider: "auto"`` with an empty model
    means "use the main chat model" — same default as every other aux task.

    Legacy fallback: users who configured ``curator.auxiliary.{provider,model}``
    under the previous one-off schema still work. Precedence:
      1. ``auxiliary.curator.{provider,model}`` when both are set non-auto
      2. Legacy ``curator.auxiliary.{provider,model}`` when both are set
      3. Main ``model.{provider,default/model}`` pair
    )r  r    r!   )r   bs     r   _resolve_review_modelr    s      	 $$A:qwr   r  c                   ddl }ddddg dd}	 ddlm} n,# t          $ r}d| |d<   |d         |d<   |cY d}~S d}~ww xY wd}d}d}d}d}		 dd	lm}
 dd
lm}  |
            }t          |          }|j	        |j
        }	} |||	|j        |j                  }|                    d          }|                    d          }|                    d          }|                    d          p|}n4# t          $ r'}t                              d|d           Y d}~nd}~ww xY w|	|d<   |pd|d<   d}	  ||	||||ddddd
  
        }d|_        d|_        t%          t&          j        d          5 }|                    |          5  |                    |          5  |                    |           }ddd           n# 1 swxY w Y   ddd           n# 1 swxY w Y   ddd           n# 1 swxY w Y   d}t1          |t2                    r6t5          |                    d          pd                                          }||d<   t9          |          dk    r|dd         dz   n|pd|d<   g }t;          |dg           pg D ]}t1          |t2                    s|                    d          pg }|D ]}t1          |t2                    s|                    d           pi }|                    d!          pd}|                    d"          pd}t1          |t4                    r t9          |          d#k    r|dd#         dz   }|                    ||d$           ||d<   n*# t          $ r}d%| |d<   |d         |d<   Y d}~nd}~ww xY w|&	 |                                 n># t          $ r Y n2w xY wn-# |&	 |                                 w # t          $ r Y w w xY ww xY w|S )&a/  Spawn an AIAgent fork to run the curator review prompt.

    Returns a dict with:
      - final: full (untruncated) final response from the reviewer
      - summary: short summary suitable for state file (240-char cap)
      - model, provider: what the fork actually ran on
      - tool_calls: list of {name, arguments} for every tool call made during
        the pass (arguments may be truncated for readability)
      - error: set if the pass failed mid-run; final/summary may still be empty

    Never raises; callers get a structured failure instead.
    r   Nr   r  )AIAgentzAIAgent import failed: rT  rg  r   )resolve_runtime_provider)	requestedtarget_modelr"   r#   r  r  api_moder    z&Curator provider resolution failed: %sTrb   r!   i'  r   )
r!   r    r  r  r  max_iterations
quiet_modeplatformskip_context_filesskip_memoryr^   )user_messagefinal_responserL     u   …r  _session_messagesr   functionr   r   i  )r   r   zerror: ) 
contextlib	run_agentr  rq   r   r   hermes_cli.runtime_providerr  r  r    r!   r"   r#   r|   rP   rQ   _memory_nudge_interval_skill_nudge_intervalopenrh   devnullredirect_stdoutredirect_stderrrun_conversationrJ   rK   r   r   ro  getattrr   close)r  r  result_metar  rT   _api_key	_base_url	_api_mode_resolved_provider_model_namer   r  _cfg_binding	_provider_rpreview_agent_devnullconv_resultrL  _callsmsgtcsr   fnr   args_raws                              r   r  r    sC    # #K%%%%%%%   <<<G!,W!5I  HIIKQ111111HHHHHH{}}*400!)!2HN;	&&$%6&8	
 
 
 779%%GGJ''	GGJ''	 WWZ00=I Q Q Q=q4PPPPPPPPQ 'K06BK
LAw'  #
 
 
$ /0+-.* "*c"" 	Mh''11	M 	M''11	M 	M '77V7LLK	M 	M 	M 	M 	M 	M 	M 	M 	M 	M 	M 	M 	M 	M 	M 	M 	M 	M 	M 	M 	M 	M 	M 	M 	M 	M 	M 	M 	M 	M 	M 	M 	M 	M 	M 	M 	M 	M 	M 	M 	M 	M 	M 	M 	M
 k4(( 	I(899?R@@FFHHE$G:=e**s:J:J%+"5"5QVQeZeI (*<)<bAAGR 	E 	ECc4(( '',''-2C E E!"d++ VVJ''-2vvf~~+66+..4"h,, 6X1D1D'~5Ht(CCDDDDE %+L!! 6 6 6,}}G!,W!5I6 #""$$$$    $<#""$$$$    $
 s    
?:??B"C0 0
D!:DD!3=M) 0G-GF?3G?GGGG
G-G	G-G	G-!M) -G11M) 4G15E3M) (N< )
N3NN< NN< N+ +
N87N8<O& OO&
O"O&!O""O&)idle_for_secondsr  r  Optional[float]Optional[Dict[str, Any]]c                    	 t                      sdS | t                      dz  }| |k     rdS t          |          S # t          $ r(}t                              d|d           Y d}~dS d}~ww xY w)z~Best-effort: run a curator pass if all gates pass. Returns the result
    dict if a pass was started, else None. Never raises.Ng      @)r  zmaybe_run_curator failed: %sTrb   )r   r   r  rq   rP   rQ   )r  r  
min_idle_srT   s       r   maybe_run_curatorr  x  s     	4'+--6J*,,t!Z8888   3QFFFttttts   = = = 
A/A**A/)r   r   r   r   )r   r   )r   r1   )rS   r1   r   rV   )r7   rv   r   rV   )r   rv   )r   r   )r   r   )r   r   r   r   r   )r   r   r   rv   )r   r   r   r   )r   r   rR   r   r   rv   )
r   r   r   r   r   r   r   r   r   r   )r  r   r   r  )r   r   r   r  )r   r   r#  r   r$  r  r   r   r%  r&  r   r   )r;  r   r<  r   r=  r   r>  r   r?  r   r@  r   rA  r   rB  r1   r   rC  )rd  r1   r   r   )r   r   )NFF)r  r  r  rv   r  rv   r   r1   )r   r1   r   r   )r   r1   r   r  )r  r   r   r1   )r  r  r  r  r   r  )Br'   
__future__r   rG   loggingrh   r   rf   r  r   r   r   pathlibr   typingr   r	   r
   r   r   r   r   hermes_constantsr   r   r   	getLoggerr$   rP   r   r   r   r   r   r   r0   r9   rU   ru   rz   r}   r   r   r   r   r   r   r   r   r   r  r  r   r   r  r  r"  r:  r  rt  r  r  r  r  r  r  r)   r   r   <module>r!     s   * # " " " " "   				 				      2 2 2 2 2 2 2 2 2 2       G G G G G G G G G G G G G G G G G G , , , , , ,      		8	$	$   % % % % %J % % %      ; ; ; ;      K K K K*   , , , ,    * * * *& & & && & & &( ( ( (* * * *   2$ 2$ 2$ 2$ 2$r( ( ( ( (`D 8qM t   ,   $w< w< w< w<tN N N Nb3 3 3 3v BF@< @< @< @< @<F} } } }@t t t tv   * 37i i i i iX+J +J +J +J\   &F F F F^ )-26       r   