
    .cjT                   0   d Z ddlmZ ddlZddlZddlZddlZddlZddlmZ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 dd	lmZ  ej        e          ZdtdZ G d de          ZdudZ dZ!dZ"dZ#dZ$dZ%dvdZ&dwdZ'dwdZ(dxd Z)dyd#Z*dzd$Z+dwd%Z,dzd&Z-d{d(Z.d|d*Z/d{d+Z0d{d,Z1dzd-Z2dzd.Z3d}d1Z4d~dd3Z5dd5Z6d~dd7Z7d8Z8d9Z9dvd:Z:dd>Z;ddFZ<ddIZ=ddKZ>	 d~ddQZ?ddUZ@dd^ZAdd`ZBddaZC	 	 	 	 dddhZDddjZEddlZFddnZGdddoddsZHd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)atomic_json_writevaluer   returnOptional[str]c                T    | d S t          |                                           }|pd S N)strstrip)r   texts     //home/ice/.hermes/hermes-agent/agent/curator.py_strip_aux_credentialr   (   s-    }tu::D<4    c                  F    e Zd ZU dZded<   ded<   ded<   ded<   ded	<   d
S )_ReviewRuntimeBindingzCProvider/model for the curator review fork plus per-slot overrides.r   providermodelr   explicit_api_keyexplicit_base_urlDict[str, Any]request_overridesN)__name__
__module____qualname____doc____annotations__ r   r   r    r    /   sN         MMMMMJJJ####$$$$%%%%%%r   r    runtime_overridesslot_extra_bodyr%   c                    t          | pi           }t          |t                     r@|r>t          |                    d          pi           }|                    |           ||d<   |S )z<Merge resolver metadata with task-local request body fields.
extra_body)dict
isinstancegetupdate)r-   r.   mergedr0   s       r   _merge_request_overridesr6   9   sq    
 #)r**F/4(( *_ *&**\228b99
/***)|Mr            Z   Fr   c                 *    t                      dz  dz  S )Nskillsz.curator_stater   r,   r   r   _state_filer=   U   s    x'*:::r   c                     d d d d d dddS )NFr   )last_run_atlast_run_duration_secondslast_run_summarylast_run_summary_shown_atlast_report_pathpaused	run_countr,   r,   r   r   _default_staterF   Y   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>m   s5    YYY$!Q!t))q||TWGXGX)A)))r   z Failed to read curator state: %s)r=   existsrF   jsonloads	read_textr2   r1   r4   itemsOSErrorJSONDecodeErrorloggerdebug)pathdataerQ   s      @r   
load_stater_   e   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/r]   Nonec                    t                      }	 t          || dd           d S # t          $ r(}t                              d|d           Y d }~d S d }~ww xY w)Nr8   T)indent	sort_keysz Failed to save curator state: %sexc_info)r=   r   	ExceptionrZ   r[   )r]   r\   r^   s      r   
save_staterg   t   s|    ==DK$Q$?????? K K K7TJJJJJJJJJKs   % 
AAArD   boolc                d    t                      }t          |           |d<   t          |           d S NrD   )r_   rh   rg   )rD   states     r   
set_pausedrl   |   s.    LLE6llE(Our   c                 ^    t          t                                          d                    S rj   )rh   r_   r3   r,   r   r   	is_pausedrn      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.configrq   rf   rZ   r[   r2   r1   r3   )rq   cfgr^   curs       r   _load_configrv      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rv   rh   r3   rt   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)rv   r|   r3   DEFAULT_INTERVAL_HOURS	TypeError
ValueErrorrz   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)rv   r   r3   DEFAULT_MIN_IDLE_HOURSr   r   rz   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)rv   r|   r3   DEFAULT_STALE_AFTER_DAYSr   r   rz   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)rv   r|   r3   DEFAULT_ARCHIVE_AFTER_DAYSr   r   rz   s    r   get_archive_after_daysr      sZ    
..C*377/1KLLMMMz" * * *))))*r   c                 d    t                      } t          |                     dd                    S )ap  Whether the curator may prune (archive) bundled built-in skills too.

    ON by default. When on, built-ins become curation candidates and are
    archived after the same inactivity period as agent-created skills, with a
    suppression list keeping them archived across `hermes update` re-seeds.
    Hub-installed skills are never pruned regardless of this flag.
    prune_builtinsTry   rz   s    r   get_prune_builtinsr      s*     ..C($//000r   c                 n    t                      } t          |                     dt                              S )uQ  Whether the curator runs its LLM consolidation (umbrella-building) pass.

    OFF by default. When off, a curator run does ONLY the deterministic
    inactivity prune (mark stale / archive long-unused skills) and skips the
    forked aux-model review entirely — no consolidation, no umbrella-building,
    no aux-model cost. Set ``curator.consolidate: true`` to opt back into the
    LLM pass that merges overlapping skills into class-level umbrellas.

    The explicit ``hermes curator run --consolidate`` flag overrides this for
    a single invocation regardless of the config value.
    consolidate)rv   rh   r3   DEFAULT_CONSOLIDATErz   s    r   get_consolidater      s*     ..C':;;<<<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.
    Fr?   Nuu   deferred first run — curator seeded, will run after one interval; use `hermes curator run --dry-run` to preview nowrA   z&Failed to seed curator last_run_at: %stzinfo)hours)r{   rn   r_   r   r3   r   r   r   utc	isoformatrg   rf   rZ   r[   r   replacer   r   )r   rk   lastr^   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Set[str]c                     	 ddl m}   |             S # t          $ r5}t                              d|d           t                      cY d}~S d}~ww xY w)zSkill names referenced by any cron job (incl. paused/disabled).

    Best-effort: a cron-module import error or corrupt jobs store must never
    break the curator, so any failure yields an empty set (no protection,
    but no crash).
    r   )referenced_skill_namesz0Curator could not read cron skill references: %sTrd   N)	cron.jobsr   rf   rZ   r[   set)_refsr^   s     r   _cron_referenced_skillsr   "  sp    ======uww   GUYZZZuus    
A*AAADict[str, int]c                ^   ddl m} | t          j        t          j                  } | t          t                                z
  }| t          t                                z
  }t                      }dddddd}|
                                D ]}|dxx         dz  cc<   |d         }|                    d	          r1||v r6|                    d
d          s&|                    |           |dxx         dz  cc<   rt          |                    d                    }|p#t          |                    d                    p| }	|	j         |	                    t          j                  }	|                    d|j                  }
t#          |                    dd          pd          dk    }|r>|	|k    r8|
|j        k    r+|                    ||j                   |dxx         dz  cc<   e|	|k    r7|
|j        k    r,|                    |          \  }}|r|dxx         dz  cc<   |	|k    r8|
|j        k    r-|                    ||j                   |dxx         dz  cc<   |	|k    r6|
|j        k    r+|                    ||j                   |dxx         dz  cc<   |S )u.  Walk every curator-managed skill and move active/stale/archived based on
    the latest real activity timestamp. Pinned skills are never touched.

    Built-ins (eligible only when ``curator.prune_builtins`` is on) are seeded
    with a baseline record the first time they're seen so their inactivity
    clock starts NOW rather than at epoch — a long-unused built-in is therefore
    archived only after a fresh ``archive_after_days`` of non-use, not on the
    first pass after the flag flips on.

    Returns a counter dict describing what changed.
    r   r   N)days)marked_stalearchivedreactivatedcheckedseededr      namepinned
_persistedTr   last_activity_at
created_atr   rk   	use_countr   r   r   )toolsr   r   r   r   r   r   r   r   r   agent_created_reportr3   seed_record_if_missingr   r   r   STATE_ACTIVEr|   STATE_STALE	set_stateSTATE_ARCHIVEDarchive_skill)r   _ustale_cutoffarchive_cutoffcron_referencedcountsrowr   last_activityanchorcurrent
never_usedok_msgs                 r   apply_automatic_transitionsr   1  s    ('''''
{l8<(((<(>(>????L9*@*B*BCCCCN-//OQqQZ[\\F&&(( 5' 5'yQ6{778 	 ?"" ww|T** 	%%d+++8!"377+=#>#>?? J*SWW\-B-B"C"CJs= ^^8<^88F'''2?33 a005A66!;
 	&<//".((T2?3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 (note: long descriptions are truncated to 57 chars in the system prompt skill index — keep the trigger class in that window). 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, hub-installed, or external-dir skills (`skills.external_dirs`). The candidate list below is already filtered to local curator-managed skills only; external skills are externally owned and read-only to this background curator.
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.
3b. DO NOT archive, delete, consolidate, move, or otherwise modify any skill named in the protected built-ins list (currently: plan). These back load-bearing UX (slash-command entry points referenced in docs and tips) and are filtered out of the candidate list below — never resurrect one as an archive or absorb target.
3c. DO NOT archive or prune any skill marked `cron=yes` in the candidate list. A cron job depends on it and will fail to load it on its next run. You MAY still consolidate it into an umbrella — but only because the curator rewrites cron job skill references to follow consolidations; never simply prune it.
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. Corollary: 'use=0' is ALSO not a reason to PRUNE a skill. Never archive a never-used skill (use=0) unless it is at least 30 days old (check last_activity / created date) AND its content is genuinely obsolete or fully absorbed elsewhere — a recently-created skill simply may not have had its trigger come up yet.
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/).

Package integrity — not optional:
Before demoting or archiving a skill, inspect it as a COMPLETE directory package, not just SKILL.md. A skill root may include `references/`, `templates/`, `scripts/`, and `assets/`; `skill_view` discovers those relative to the skill root. A reference markdown file inside another skill is NOT a new skill root and does not get its own linked-file discovery.
If the source skill has support files OR SKILL.md contains relative links such as `references/...`, `templates/...`, `scripts/...`, or `assets/...`, DO NOT flatten only SKILL.md into `<umbrella>/references/<old>.md`. Choose one safe path instead:
   • keep it as a standalone skill, OR
   • fully merge it by re-homing every needed support file into the umbrella's canonical `references/`, `templates/`, `scripts/`, or `assets/` directories AND rewrite the destination instructions to the new paths, OR
   • archive the entire original skill package unchanged.
Never leave archived/demoted instructions pointing at files that were left behind under the old skill directory.
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                       — move LOCAL candidate content into a support subfile when package integrity requires it; never mv, cp, rm, patch, or rewrite bundled, hub-installed, or external-dir skills

'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.
    logsrr   Tparentsexist_okz%Curator reports dir create failed: %sN)r   mkdirrX   rZ   r[   )rootr^   s     r   _reports_rootr   A  s     v%	1DA

4$
//// A A A<a@@@@@@@@AKs   . 
AAAneedler   r\   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".
    -rL   \/.r   r   TF)r   splitrsplit)r   r\   norm_needlepartstems        r   _needle_in_path_componentr   W  s     ..c**KT3''--c22   	),t{{3""1%%$<<S!![0044 15r   removed	List[str]addedafter_names
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   rL   )	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)r2   r1   r3   r   rT   rU   rf   appendr   r   r   rh   researchescape)r   r   r   r   r   r   parsed_callstcrawargsdestinationsr   r   r   needlestarget	haystackskeyrP   hithayr   matcheds                          r   _classify_removed_skillsr  i  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  )r2   r   r   r  DOTALL
IGNORECASEgroupyaml	safe_loadrf   r1   r3   listr   r   )r  emptyr   matchbodyr  r]   outcons_rawprun_rawentryfrmr   r  r   s                  r   _parse_structured_summaryr%    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)r2   r1   r3   r   rT   rU   rf   r   )r   r   r  r  r  r   r	  s          r   #_extract_absorbed_into_declarationsr+  4  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,   rN   r^   s     r   rR   z-_reconcile_classification.<locals>.<dictcomp>  s    III!6AIIIr   r   c                    h | ]
}|d          S r2  r,   r3  s     r   	<setcomp>z,_reconcile_classification.<locals>.<setcomp>  s    BBB1V9BBBr   r   c                     i | ]}|d          |S )r  r,   r3  s     r   rR   z-_reconcile_classification.<locals>.<dictcomp>  s    NNN1!F)QNNNr   r  c                     i | ]}|d          |S r2  r,   r3  s     r   rR   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   r8  r  r"   z+auditz.tool-call audit (model named missing umbrella))r   r   r8  r  r   model_claimed_intoz>fallback (model named missing umbrella, no tool-call evidence)z5tool-call audit (model omitted from structured block))r   r   r8  r  r   zno-evidence fallbackr   )r3   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_classificationrC  j  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   before_namesafter_reportmodel_finalc                   d |D             }t          |                                          }t          | |z
            }t          || z
            }|sdS t          ||||          }t	          |          }	t          |          t          |          z  }
t          |          }t          |||	|
|          }|d         }|d         }d}g }t          |          t          |          z   }|                    d| d	           d
}|D ]V}||k    r nM|	                    dd          }|	                    dd          }|                    d| d|            |dz  }W|D ]b}||k    r nYt          |t                    r|	                    dd          nt          |          }|                    d| d           |dz  }c||k    r|                    d||z
   d           |                    d           |r;t          d |D                       }|r |d
         }|                    d|            d                    |          S )u  Format the user-visible rename map for a curator run.

    Renders the "where did my skills go?" lines that get appended to the
    `final_summary` string fed to gateway/CLI receivers. Empty string when
    nothing was archived this run — most ticks are no-op and shouldn't add
    extra log noise.

    Format::

        archived 4 skill(s):
          • pdf-extraction → document-tools
          • docx-extraction → document-tools
          • flaky-thing — pruned (stale)
          • old-utility → spreadsheet-ops
        full report: hermes curator status
        keep an umbrella stable: hermes curator pin document-tools

    Cap is 10 entries so a 50-skill consolidation doesn't blow up
    agent.log; the full list is always in REPORT.md. The pin hint only
    appears when at least one consolidation produced an umbrella worth
    pinning (pruned-only runs skip it).
    c                d    i | ]-}t          |t                    |                    d           |.S r2  r2   r1   r3   rN   rs     r   rR   z)_build_rename_summary.<locals>.<dictcomp>
  3    SSS!z!T?R?RSQUU6]]ASSSr   r   r   r   r   r   r   r,  r-  r  r.  r   r   
   z	archived z
 skill(s):r   r   r   r   u     •     → r   u    — pruned (stale)u
     … and z morez"full report: hermes curator statusc                b    h | ],}|                     d           |                     d           -S )r   )r3   r3  s     r   r5  z(_build_rename_summary.<locals>.<setcomp>>  s1    QQQa155==QAEE&MMQQQr   z,keep an umbrella stable: hermes curator pin 
)r   keyssortedr  r%  r+  rC  lenr   r3   r2   r1   r   join)rD  rE  r   rF  after_by_namer   r   r   r,  r-  r  r.  classificationr   r   SHOWlinestotalshownr#  r   r   	umbrellasexamples                           r   _build_rename_summaryr_    s   : TS|SSSMm((**++K\K/00G;-..E r(	  I ,K88K{##c%jj0L?
KK.!3  N ".1LH%FDEF+E	LL.U...///E  D==Eyy%%yy%%/d////000
  D==E)3E4)@)@Puyy%%%c%jj7d777888
t||5%$,555666	LL5666
  QQ<QQQRR	 	lGLLHwHH   99Ur   
started_atr   elapsed_secondsauto_countsauto_summarybefore_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).
    Tr   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 r2  rI  rJ  s     r   rR   z%_write_run_report.<locals>.<dictcomp>l  rL  r   c                d    i | ]-}t          |t                    |                    d           |.S r2  rI  rJ  s     r   rR   z%_write_run_report.<locals>.<dictcomp>p  s3    UUU1AtATATUaeeFmmQUUUr   rk   )r   r  tor   r   unknownr   rM  finalr   rN  r   r   )rewritesjobs_updatedjobs_scannedc                    i | ]P}t          |t                    |                    d           ,|                    d          A|d          |d         QS )r   r   rI  r3  s     r   rR   z%_write_run_report.<locals>.<dictcomp>  sj     
 
 
!T""
 ()uuV}}
 :;v
fIqy
 
 
r   c                r    g | ]4}t          |t                    |                    d           ,|d          5S r2  rI  r3  s     r   
<listcomp>z%_write_run_report.<locals>.<listcomp>  sN     
 
 
!T""
'(uuV}}
fI
 
 
r   )rewrite_skill_refsr   z%Curator cron skill rewrite failed: %srd   )rm  rn  ro  errorr`  duration_secondsr8   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 r2  r,   )rN   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)rb   ensure_asciirR  rH   rI   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   r   rf   rZ   r[   strftimerS   r   rS  rT  r3   r   r  r%  r+  rC  r   rs  r   r   roundrU  r|   sumvalues
write_textrT   dumps_render_report_markdown)%r`  ra  rb  rc  rd  rD  rE  re  r   r^   stamprun_dirsuffixrW  r   r   r   before_by_nametransitionsr   s_befores_after	tc_countsr  r,  r-  r  r.  rX  r   r   r  consolidated_mapr  _rewrite_cron_refspayloadmds%                                        r   _write_run_reportr  G  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r  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 — rR  r"   z(not resolved)r!   r   zModel: `z` via `u   `  ·  Duration: u     ·  Agent-created skills: rw  rP  rx  z (ry  z+dz)
r  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   r  z## LLM consolidation pass
z- tool calls: **r  z** (by name: , c              3  *   K   | ]\  }}| d | V  dS )=Nr,   )rN   rO   rP   s      r   	<genexpr>z*_render_report_markdown.<locals>.<genexpr>*  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  r8  z- `u   ` → merged into ``u    — ztool-call auditz  _(detected via z)_r9  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  r  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
r  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.)r3   divmodr|   r   rV  rT  rW   rU  r   rM   r2   r1   )%r  rZ  starteddurationminssecs	dur_labelr"   provr   rt  autor  r   rY  r#  r   r   r  r8  liner   r   ntranstcron_rwcron_rewrites_listr  rw  rx  r  r  oldnewrl  llm_sums%                                        r   r  r    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                 0   t          j                    } | sdS t                      }dt          |            dg}| D ]}|                    d|d          d|d          d|                    d	          rd
nd d|d         |v 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=rk   z	  pinned=r   yesnoz  cron=z  activity=activity_countr   z  use=r   z  view=
view_countz
  patches=patch_countz  last_activity=r   neverrR  )r   r   r   rU  r   r3   rV  )rowsr   rZ  rK  s       r   _render_candidate_listr    s   +--D 433-//O5c$ii5556E 
 
D6 D DwZD D uuX8eeDD D vY/99EEt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   
on_summaryOptional[Callable[[str], None]]synchronousdry_runr   Optional[bool]c                    t                      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 consolidation is enabled AND 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.

    *consolidate* gates the LLM umbrella-building pass. ``None`` (the default)
    reads ``curator.consolidate`` from config (OFF by default). Passing
    ``True``/``False`` overrides the config for this invocation — used by the
    ``hermes curator run --consolidate`` flag. When consolidation is off, only
    the deterministic inactivity prune runs and the forked aux-model review is
    skipped entirely (no aux-model cost).

    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. A dry-run also honors
    *consolidate*: when consolidation is off, the preview only reports the
    deterministic prune candidates.
    Nr   )r   r   r   r   )curator_backupzpre-curator-run)r  zcurator: snapshot created (r  z#Curator pre-run snapshot failed: %sTrd   )r   r   z marked staler   z	 archivedr   z reactivatedr  z
no changesr?   rE   r   zdry-run auto: zauto: rA   c            
        	 t          j                    } n# t          $ r g } Y nw xY wd | D             }s  d}ddddg d d}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|            n# t          $ r Y nw xY wd S i }	 t                      }	d|	v r  d}ddddg d d}ned}
t                      rd}
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)          |t          j                    |                    dg           pg |                    dd          pd          }|r| d| }n4# t          $ r'}t                              d|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 r2  rI  rJ  s     r   r5  z8run_curator_review.<locals>._llm_pass.<locals>.<setcomp>8  s1    TTT!
1d@S@STfTTTr   z"; llm: skipped (consolidation off)r   zskipped (consolidation off)rl  r  r"   r!   r   rt  r@   rA   )r`  ra  rb  rc  rd  rD  rE  re  rC   zCurator report write failed: %sTrd   z	curator: zNo agent-created skillsz; llm: skipped (no candidates)zskipped (no candidates)a  

PRUNE-BUILTINS MODE IS ON: bundled built-in skills ARE included in the candidate list below and MAY be archived for staleness/irrelevance, overriding hard rule #1 for bundled skills ONLY. Hub-installed skills remain strictly off-limits. Treat a stale built-in the same as a stale agent-created skill: archive it (never delete). It will be restored on `hermes update` only if the user explicitly restores it.z

z; llm: r  	no changezCurator LLM pass failed: %sz; llm: error (r  zerror (r   rl  )rD  rE  r   rF  rR  z'Curator rename summary build failed: %s)r   r   rf   r   r   r   r   total_secondsr_   r  r   rZ   r[   rg   r  r   CURATOR_DRY_RUN_BANNERCURATOR_REVIEW_PROMPT_run_llm_reviewr3   r_  )rd  rD  final_summaryre  elapsedstate2rE  report_pathr^   candidate_listbuiltins_notepromptrename_linesrc  r   r   r  r  prefixstarts                r   	_llm_passz%run_curator_review.<locals>._llm_pass1  s   	'<>>MM 	 	 	MMM	TT}TTT  )	K<KKK  8  H  |HL11E9HHJJG\\F29F./)6F%&"*?AA " " "!"R/$$+ &!-"/!-!-%	 	 	 *14[1A1AF-. R R R>DQQQQQQQQRv J:=::;;;;    DF#%3	355N(N::#) W< W W W8 ""$!  !#%'' 
; "  [1 , ,0,2?, ,), , F !6Z}ZZ.ZZF*622Z|ZZHLLK4X4XZZ   
	 
	 
	LL6DLIII%G|GG1GGGM)Q>>> Q HHHHHH
	 
	V0)(=??#<<b99?R$LL"55;	  L  C#0 B BL B B 	V 	V 	VLLBAPTLUUUUUUUU	V <--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   	 	s    &&B( (B76B7;+C' '
D1DD-D< <
E	E	BG 
H/$AH**H/3AJ 
J>J99J>L! !L0/L04+M   
N*NN&N6 6
OOzcurator-review)r	  daemonr   )r`  rv  summary_so_far)r   r   r   r   r   r   r   rU  rf   agentr  snapshot_skillsr   rZ   r[   r   r   rV  r_   r   r|   r3   rg   	threadingThreadr  )r  r  r  r   reportr  snapr^   auto_summary_partsrk   r  r  rc  r   r  r  s   ` ``        @@@@r   run_curator_reviewr    sS   D %''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` ` ` ` ` ` ` ` ` ` `D  	IdAQRRR				 oo''"&  sM   'A! !A54A59 C  B/ .C  /
B<9C  ;B<<C   
C1
C,,C1rt   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}|r||dk    rv|rtt	          ||t          |                    d	                    t          |                    d
                    t          i |                    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
                    t          i |	                    d                              S t	          ||ddi           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   	auxiliaryrr   Napi_keybase_urlr0   u|   curator: using deprecated curator.auxiliary.{provider,model} config — please migrate to auxiliary.curator.{provider,model})
r2   r3   r1   r   r    r   r6   rZ   infor   )rt   _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$!)--	":":;;!)--
";";<<$R|)D)DEE
 
 	
 &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::$R\)B)BCC
 
 	
 !dD"MMMr   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"   )rt   bs     r   _resolve_review_modelr    s      	 $$A:qwr   r  c                t
   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}	i }
d}d}d}d}	 dd	lm} dd
lm}  |            }t          |          }|j	        |j
        }} ||||j        |j                  }|                    d          }|                    d          }|                    d          }|                    d          p|}|                    d          }	t          |                    d          |j                            d                    }
|                    d          }|                    d          }t!          |                    d          pg           }t#          |                    d          t$                    r4|d                                         r|d                                         }n4# t          $ r'}t(                              d|d           Y d}~nd}~ww xY w||d<   |pd|d<   d}	 i }t#          |t,                    r||d<   t#          |t$                    r|r||d<   |pg |d<    |d3||||||	|
d|dddddd }d|_        d|_        d!|_        t5          t6          j        d"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}t#          |t@                    r6t%          |                    d&          pd                                          }||d'<   tC          |          d(k    r|dd(         d)z   n|pd*|d<   g }tE          |d+g           pg D ]}t#          |t@                    s|                    d,          pg }|D ]}t#          |t@                    s|                    d-          pi }|                    d.          pd}|                    d/          pd} t#          | t$                    r tC          |           d0k    r| dd0         d)z   } |#                    || d1           ||d,<   n*# t          $ r}d2| |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 )4a/  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  r  rp   )resolve_runtime_provider)	requestedtarget_modelr#   r$   r  r  api_moder!   credential_poolr&   r0   max_output_tokenscommandr  r"   z&Curator provider resolution failed: %sTrd   
max_tokensacp_commandacp_args)r"   r!   r  r  r  r  r&   i'  rr   )max_iterations
quiet_modeplatformskip_context_filesskip_memorybackground_reviewwrH   rI   )user_messagefinal_responserl     u   …r  _session_messagesr   functionr   r   i  )r   r   zerror: r,   )%
contextlib	run_agentr  rf   rs   rq   hermes_cli.runtime_providerr  r  r!   r"   r#   r$   r3   r6   r&   r  r2   r   r   rZ   r[   r|   _memory_nudge_interval_skill_nudge_interval_memory_write_originopenosdevnullredirect_stdoutredirect_stderrrun_conversationr1   rU  getattrr   close)!r  r  result_metar  r^   _api_key	_base_url	_api_mode_resolved_provider_credential_pool_request_overrides_max_tokens_acp_command	_acp_args_model_namerq   r  _cfg_binding	_provider_rpreview_agent_agent_kwargs_devnullconv_resultrl  _callsmsgtcsr  fnr   args_raws!                                    r   r  r  #  s    # #K%%%%%%%   <<<G!,W!5I  HII)+KL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77#4555GG'((&**<88
 
 gg122wwy)).B//	cggg&&,, 	/W1C1C1E1E 	/g,,,..K Q Q Q=q4PPPPPPPPQ 'K06BK
LR(*k3'' 	6*5M,'lC(( 	8\ 	8+7M-((1RM*%w 
',0
 
 
  #%
 
 
 
* /0+-.* -@) "*cG444 	M''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   
?:??FG4 4
H%>H  H%7BR8 ?L<L%+LL%LL%LL%L<%L)	)L<,L)	-L<0R8 <M  R8 M E3R8 7T 8
SST ST %S: :
TTT5T$#T5$
T1.T50T11T5)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: %sTrd   )r   r   r  rf   rZ   r[   )r@  r  
min_idle_sr^   s       r   maybe_run_curatorrE    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.   r   r   r%   )r   r   )r   r%   )r]   r%   r   r`   )rD   rh   r   r`   )r   rh   )r   r|   )r   r   )r   r   r   r   r   )r   r   r   rh   )r   r   )r   r   r   r   )r   r   r\   r   r   rh   )
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   )
rD  r   rE  r   r   r   rF  r   r   r   )r`  r   ra  r   rb  r   rc  r   rd  r   rD  r   rE  r   re  r%   r   rf  )r  r%   r   r   )r   r   )NFFN)
r  r  r  rh   r  rh   r   r  r   r%   )rt   r%   r   r    )rt   r%   r   r  )r  r   r   r%   )r@  rA  r  r  r   rB  )Ir*   
__future__r   rT   loggingr!  r   r  r   r   r   pathlibr   typingr   r	   r
   r   r   r   r   hermes_constantsr   r   r   utilsr   	getLoggerr'   rZ   r   r    r6   r   r   r   r   r   r=   rF   r_   rg   rl   rn   rv   r{   r   r   r   r   r   r   r   r   r   r   r  r  r   r   r  r%  r+  rC  r_  r  r  r  r  r  r  r  rE  r,   r   r   <module>rM     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   , , , ,    * * * *& & & && & & &( ( ( (* * * *	1 	1 	1 	1= = = =(   2$ 2$ 2$ 2$ 2$r   N N N N NlD 8WM @   ,   $w< w< w< w<tN N N Nb3 3 3 3v BF@< @< @< @< @<FW W W Wt} } } }@t t t tv   . 37"&	E E E E EP-N -N -N -N`   &f f f f^ )-26       r   