
    `gjS                      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j6                  e      Zd7d
Z G d de      Z	 	 	 	 	 	 d8dZ dZ!dZ"dZ#dZ$dZ%d9dZ&d:dZ'd:dZ(d;dZ)d<dZ*d=dZ+d:dZ,d=dZ-d>dZ.d?dZ/d>dZ0d>dZ1d=dZ2d=d Z3d@d!Z4dAdBd"Z5dCd#Z6dAdDd$Z7d%Z8d&Z9d9d'Z:dEd(Z;	 	 	 	 	 	 	 	 	 	 dFd)Z<	 	 	 	 dGd*Z=	 	 	 	 dHd+Z>	 dA	 	 	 	 	 	 	 	 	 	 	 dId,Z?	 	 	 	 	 	 	 	 	 	 dJd-Z@	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 dKd.ZAdLd/ZBdMd0ZC	 	 	 	 dN	 	 	 	 	 	 	 	 	 dOd1ZDdPd2ZEdQd3ZFdRd4ZGddd5	 	 	 	 	 dSd6ZHy)Tu  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_writec                F    | y t        |       j                         }|xs d S N)strstrip)valuetexts     @/root/.hermes/venv/lib/python3.12/site-packages/agent/curator.py_strip_aux_credentialr   (   s&    }u:D<4    c                  D    e Zd ZU dZded<   ded<   ded<   ded<   ded	<   y
)_ReviewRuntimeBindingzCProvider/model for the curator review fork plus per-slot overrides.r   providermodelOptional[str]explicit_api_keyexplicit_base_urlDict[str, Any]request_overridesN)__name__
__module____qualname____doc____annotations__ r   r   r   r   /   s!    MMJ##$$%%r   r   c                    t        | xs i       }t        |t               r6|r4t        |j                  d      xs i       }|j                  |       ||d<   |S )z<Merge resolver metadata with task-local request body fields.
extra_body)dict
isinstancegetupdate)runtime_overridesslot_extra_bodymergedr-   s       r   _merge_request_overridesr5   9   sU    
 #)r*F/4(_&**\28b9
/*)|Mr            Z   Fc                 "    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_staterE   Y   s!    %) %)  r   c            	        t               } | j                         s
t               S 	 t        j                  | j                  d            }t        |t              rUt               }|j                  |j                         D ci c]  \  }}||v s|j                  d      s||  c}}       |S 	 t               S c c}}w # t        t        j                  f$ r)}t        j                  d|       Y d }~t               S d }~ww xY w)Nutf-8encoding_z Failed to read curator state: %s)r<   existsrE   jsonloads	read_textr/   r.   r1   items
startswithOSErrorJSONDecodeErrorloggerdebug)pathdatabasekves         r   
load_stater[   e   s    =D;;=<zz$..'.:;dD!!#DKK$**,Y$!Q!t)q||TWGXAYZK " 	 ZT))* <7;;<s0   AC B;
!B;
&	C ;C DC>>Dc                    t               }	 t        || dd       y # t        $ r"}t        j	                  d|d       Y d }~y d }~ww xY w)Nr7   T)indent	sort_keysz Failed to save curator state: %sexc_info)r<   r   	ExceptionrS   rT   )rV   rU   rZ   s      r   
save_staterb   t   sE    =DK$Q$? K7TJJKs    	AAAc                J    t               }t        |       |d<   t        |       y NrC   )r[   boolrb   )rC   states     r   
set_pausedrg   |   s    LE6lE(Our   c                 F    t        t               j                  d            S rd   )re   r[   r0   r+   r   r   	is_pausedri      s    
  *++r   c                     	 ddl m}   |        }t        |t              si S |j                  d      xs i }t        |t              si S |S # t        $ r"}t        j	                  d|       i cY d}~S d}~ww xY w)zIRead curator.* config from ~/.hermes/config.yaml. Tolerates missing file.r   load_configz%Failed to load config for curator: %sNcurator)hermes_cli.configrl   ra   rS   rT   r/   r.   r0   )rl   cfgrZ   curs       r   _load_configrq      so    1m c4 	
'')

"Cc4 	J  <a@	s   A
 
	A5A0*A50A5c                 L    t               } t        | j                  dd            S )z)Default ON when no config says otherwise.enabledTrq   re   r0   ro   s    r   
is_enabledrv      s    
.C	4())r   c                     t               } 	 t        | j                  dt                    S # t        t
        f$ r	 t        cY S w xY w)Ninterval_hours)rq   intr0   DEFAULT_INTERVAL_HOURS	TypeError
ValueErrorru   s    r   get_interval_hoursr}      s@    
.C&377+-CDEEz" &%%&   + AAc                     t               } 	 t        | j                  dt                    S # t        t
        f$ r	 t        cY S w xY w)Nmin_idle_hours)rq   floatr0   DEFAULT_MIN_IDLE_HOURSr{   r|   ru   s    r   get_min_idle_hoursr      s@    
.C&SWW-/EFGGz" &%%&r~   c                     t               } 	 t        | j                  dt                    S # t        t
        f$ r	 t        cY S w xY w)Nstale_after_days)rq   ry   r0   DEFAULT_STALE_AFTER_DAYSr{   r|   ru   s    r   get_stale_after_daysr      s@    
.C(377-/GHIIz" (''(r~   c                     t               } 	 t        | j                  dt                    S # t        t
        f$ r	 t        cY S w xY w)Narchive_after_days)rq   ry   r0   DEFAULT_ARCHIVE_AFTER_DAYSr{   r|   ru   s    r   get_archive_after_daysr      s@    
.C*377/1KLMMz" *))*r~   c                 L    t               } t        | j                  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_builtinsTrt   ru   s    r   get_prune_builtinsr      s"     .C($/00r   c                 T    t               } t        | j                  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)rq   re   r0   DEFAULT_CONSOLIDATEru   s    r   get_consolidater      s"     .C':;<<r   c                ^    | sy 	 t        j                  |       S # t        t        f$ r Y y w xY wr   )r   fromisoformatr{   r|   )tss    r   
_parse_isor      s5    %%b))z" s    ,,c                @   t               syt               ryt               }t        |j	                  d            }|J| #t        j                  t        j                        } 	 | j                         |d<   d|d<   t        |       y| #t        j                  t        j                        } |j                   |j                  t        j                        }t!        t#                     }| |z
  |k\  S # t        $ r }t        j                  d|       Y d}~yd}~ww xY w)	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 nowr@   z&Failed to seed curator last_run_at: %stzinfo)hours)rv   ri   r[   r   r0   r   nowr   utc	isoformatrb   ra   rS   rT   r   replacer   r}   )r   rf   lastrZ   intervals        r   should_run_nowr      s    * <{LEeii./D| ;,,x||,C	F#&==?E- N $% u 
{ll8<<({{||8<<|0134H$J8##  	FLLA1EE	Fs   ##C4 4	D=DDc                     	 ddl m}   |        S # t        $ r,}t        j	                  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: %sTr_   N)	cron.jobsr   ra   rS   rT   set)_refsrZ   s     r   _cron_referenced_skillsr   "  s<    =w GUYZus    	A!?AAc                   ddl m} | #t        j                  t        j
                        } | t        t                     z
  }| t        t                     z
  }t               }dddddd}|j                         D ]  }|dxx   dz  cc<   |d   }|j                  d      r(||v r-|j                  d	d
      s|j                  |       |dxx   dz  cc<   ^t        |j                  d            }|xs t        |j                  d            xs | }	|	j                   |	j                  t        j
                        }	|j                  d|j                         }
t#        |j                  dd      xs d      dk(  }|r?|	|kD  r:|
|j$                  k(  r)|j'                  ||j                          |dxx   dz  cc<   E|	|k  r6|
|j(                  k7  r'|j+                  |      \  }}|sq|dxx   dz  cc<   |	|k  r:|
|j                   k(  r+|j'                  ||j$                         |dxx   dz  cc<   |	|kD  s|
|j$                  k(  s|j'                  ||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   )days)marked_stalearchivedreactivatedcheckedseededr      namepinned
_persistedTr   last_activity_at
created_atr   rf   	use_countr   r   r   )toolsr   r   r   r   r   r   r   r   r   agent_created_reportr0   seed_record_if_missingr   r   r   STATE_ACTIVEry   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  sK    (
{ll8<<((<(>??L9*@*BCCN-/OQqQZ[\F&&( 5'yQ6{778 ?" ww|T*%%d+8!"377+=#>? J*SWW\-B"CJs== ^^8<<^8F'''2??3 a05A6!;
&</"..(T2??3}%*%^#23D3D(D''-HBz"a'"|#2??(BLLr~~.>"a'"l"w"..'@LLr/=!Q&!k5'n 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, 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  } 	 | j                  dd       | S # t        $ r!}t        j	                  d|       Y d}~| S d}~ww xY w)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.
    logsrm   Tparentsexist_okz%Curator reports dir create failed: %sN)r   mkdirrQ   rS   rT   )rootrZ   s     r   _reports_rootr   ?  s^     v%	1DA

4$
/ K  A<a@@KAs   ' 	AAAc                    | j                  dd      }|j                  dd      j                  d      D ]7  }|sd|v r|j                  dd      d   n|}|j                  dd      |k(  s7 y y	)
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".
    -rJ   \/.r   r   TF)r   splitrsplit)needlerU   norm_needlepartstems        r   _needle_in_path_componentr   U  sw     ..c*KT3'--c2 ),t{{3"1%$<<S![0 r   c                   g }g }g }|xs g D ]  }t        |t              s|j                  d      dk7  r)|j                  d      xs d}i }	t        |t              r|}	n&t        |t              r	 t	        j
                  |      }	t        |	t              s|j                  |	        t        |      t        |xs g       z  }
| D ]^  }|sd}d}||j                  dd      |j                  dd      h}|D ]  }	|	j                  d      }t        |t              r|s'||k(  r-||
vr2g }d	D ]7  }|	j                  |      }t        |t              s%|j                  ||f       9 d
}|D ]  \  }}|D ]t  }|s|dk(  rt        ||      }n6t        t        j                  dt        j                  |       d|            }|sQd}d|	j                  dd       d| d| d|dd  } n |s n |s|} n |r|j                  |||d       L|j                  d|i       a ||dS # t        $ r d|i}	Y w xY w)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   rJ   )	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)r/   r.   r0   r   rL   rM   ra   appendr   r   r   re   researchescape)removedaddedafter_names
tool_callsr   r   parsed_callstcrawargsdestinationsr   r   r   needlestarget	haystackskeyrY   hithayr   matcheds                          r   _classify_removed_skillsr  g  s   0 *,L#%F *,LB ""d#66&>^+ff[!'R!c4 DS!%zz#
 $%D!'"0 {#c%+2&66L >*""& c3/c31GH  0	DXXf%Ffc*& ~ \) 02IU /HHSMa%$$c1X./ C% S% F! k)";FC"H"&IIBIIf,=+>b&A3G# "2488Hc3J2K L##)(. ?""%cr(- !
 !" '( a0	d t RSMM64.)}>*@ )F;;[  % }%s   1H..H?>H?c           	        g g d}| rt        | t              s|S ddl} |j                  d| |j                  |j
                  z        }|s|S |j                  d      }	 ddl}|j                  |      }t        |t              s|S g g d}|j                  d      xs g }|j                  d      xs g }	t        |t              r|D ]  }
t        |
t              s|
j                  d      }|
j                  d	      }t        |t              r0|j                         r t        |t              r|j                         sw|
j                  d
      }|d   j                  |j                         |j                         t        |t              r|xs dj                         ndd        t        |	t              r|	D ]  }
t        |
t              s|
j                  d      }t        |t              r|j                         sF|
j                  d
      }|d   j                  |j                         t        |t              r|xs dj                         ndd        |S # t        $ r |cY S w xY w)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	  )r/   r   r   r   DOTALL
IGNORECASEgroupyaml	safe_loadra   r.   r0   listr   r   )	llm_finalemptyr   matchbodyr  rV   outcons_rawprun_rawentryfrmr   r	  r   s                  r   _parse_structured_summaryr    s*      "r2EJy#6
 BII"
		BMM!E
 ;;q>D~~d# dD!>@b+QCxx()/RHxx
#)rH(D! 	EeT*))F#C99V$DsC(SYY["4-$**,YYx(F !((		

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

4>vs4K6<R..0QS$ 
	 JO  s   I I-,I-c                |   i }| xs g D ]  }t        |t              s|j                  d      dk7  r*|j                  d      xs d}i }t        |t              r|}n&t        |t              r	 t	        j
                  |      }t        |t              s|j                  d      dk7  r|j                  d      }t        |t              r|j                         sd|vr|j                  d      }|t        |t              s|j                         dd	||j                         <   " |S # t        $ r Y 1w xY w)
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_intoT)r   declared)r/   r.   r0   r   rL   rM   ra   r   )r   r  r   r   r   r   r   s          r   #_extract_absorbed_into_declarationsr  2  s.   & &(CB G"d#66&>^+ff[!'R!c4 DS!zz# $%88H)xx$$DJJL $&/*>&#&%+\\^FDJJL=G> J'  s   .D..	D;:D;c                h   |j                  dg       D ci c]  }|d   |
 }}|j                  dg       D ch c]  }|d   	 }}|j                  dg       D ci c]  }|d   |
 }}|j                  dg       D ci c]  }|d   |
 }	}|xs i }
g }g }| D ]  }|j                  |      }|	j                  |      }|j                  |      }|
j                  |      }||j                  dd      }|rO||v rK||d	|r|j                  d
      xs dndd}|r|j                  d      r|d   |d<   |j                  |       |dk(  r-|j                  |d|r|j                  d
      xs dndd       |re|j                  d      |v rR||d   d|rdndz   |j                  d
      xs dd}|r|j                  d      r|d   |d<   |j                  |       F|r[|j                  d      |vrH|r/|j                  ||d   dd|j                  dd      |d   d       n|j                  |ddd       |r,|j                  ||d   dd|j                  dd      d       |r|j                  d
d      nd}|j                  ||rdnd|d        ||dS c c}w c c}w c c}w c c}w )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.
    r   r   r   r  r  r  r   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   )r0   r   )r   	heuristicmodel_blockr   absorbed_declarationsrZ   	heur_consheur_pruned
model_consmodel_prunedr  r   r   r   mcmphcdec
into_claimr  r	  s                        r   _reconcile_classificationr.  h  s   < (1}}^R'HI!6AIII&/mmHb&AB1V9BKB(38H"(MN1!F)Q,NJN*5//*b*IJQAfIqLJLJ$*H)+L#%F U^^D!d#]]4 ll4  ?,JjL8 &H:<rvvh/52"	) "&&,(*:E*%##E*R I:<rvvh/52" 
  "&&.L06
!X<&&*0b	%E bffZ($&zNj!& "&&.4## vJN  "z2 6*,V*%   ^  
  6
QFF:r2!   *,"%!#g)?
 	cUn )F;;E JBNJs   J J%J*=J/c                h   |D ci c]%  }t        |t              s|j                  d      |' }}t        |j	                               }t        | |z
        }t        || z
        }|syt        ||||      }	t        |      }
t        |      t        |      z  }t        |      }t        ||	|
||      }|d   }|d   }d}g }t        |      t        |      z   }|j                  d| d	       d
}|D ]I  }||k\  r nB|j                  dd      }|j                  dd      }|j                  d| d|        |dz  }K |D ]P  }||k\  r nIt        |t              r|j                  dd      n
t        |      }|j                  d| d       |dz  }R ||kD  r|j                  d||z
   d       |j                  d       |rUt        |D ch c]%  }|j                  d      s|j                  d      ' c}      }|r|d
   }|j                  d|        dj                  |      S c c}w c c}w )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).
    r   r   r   r   r   r   r   r"  r#  r   r$  r   r   
   z	archived z
 skill(s):r   r   r   u     •     → r   u    — pruned (stale)u
     … and z morez"full report: hermes curator statusz,keep an umbrella stable: hermes curator pin 
)r/   r.   r0   r   keyssortedr  r  r  r.  lenr   r   join)before_namesafter_reportr   model_finalrafter_by_namer   r   r   r"  r#  r   r$  classificationr   r   SHOWlinestotalshownr  r   r   rZ   	umbrellasexamples                             r   _build_rename_summaryrE    sZ   : 0<S!z!T?RQUU6]A%SMSm((*+K\K/0G;-.E(	I ,K8K{#c%j0L?
K.!3N ".1LH%FDEF+E	LL9UG:./E D=yy%yy%vdV5/0
  D=)3E4)@uyy%c%jvdV#678
 t|z%$,u56	LL56
 <Qa155=AEE&MQR	lGLL>wiH 99Uu Th Rs   H*H*H/$H/c                T   t               }	 |j                  dd       | j                  d      }
||
z  }d}|j                         r |dz  }||
 d| z  }|j                         r 	 |j                  dd       |D ci c]%  }t        |t              s|j                  d
      |' }}t        |j                               }t        ||z
        }t        ||z
        }|D ci c]%  }t        |t              s|j                  d
      |' }}g }t        ||z        D ]k  }|j                  |      xs i j                  d      }|j                  |      xs i j                  d      }|sN|sQ||k7  sW|j                  |||d       m i }|j                  dg       xs g D ],  }|j                  d
d      }|j                  |d      dz   ||<   . t        ||||j                  dg       xs g       }t        |j                  dd      xs d      }t        |      t        |xs g       z  }t!        |j                  dg       xs g       }t#        |||||      }|d   }|d   }g ddd} 	 |D 	ci c]=  }	t        |	t              r+|	j                  d
      r|	j                  d      r	|	d
   |	d   ? }!}	|D 	cg c](  }	t        |	t              r|	j                  d
      r|	d
   * }"}	|!s|"rddlm}#  |#|!|"      } i d| j+                         dt-        |d      d |j                  d d      d!|j                  d!d      d"|d#t/        |      t/        |      t/        |      t/        |      z
  t/        |      t/        |      t/        |      t/        |      t/        |      t1        | j                  d$d            t3        |j5                               d%
d&|d'|d|d|d(|D $cg c]  }$|$d
   	 c}$d)|d*|d+| d,|j                  dd      d-|j                  d.d      d/|j                  d0      d|j                  dg       i}%	 |d1z  j7                  t9        j:                  |%dd2      d3z   d45       	 t=        |%      }&|d7z  j7                  |&d45       	 t1        | j                  d$d            dkD  r/|d9z  j7                  t9        j:                  | dd2      d3z   d45       |S # t        $ r }	t        j	                  d|	       Y d}	~	yd}	~	ww xY w# t        $ r }	t        j	                  d	|	       Y d}	~	yd}	~	ww xY wc c}w c c}w c c}	w c c}	w # t        $ r3}	t        j	                  d|	d       g ddt)        |	      d} Y d}	~	d}	~	ww xY wc c}$w # t        $ r!}	t        j	                  d6|	       Y d}	~	Gd}	~	ww xY w# t        $ r!}	t        j	                  d8|	       Y d}	~	Rd}	~	ww xY w# t        $ r!}	t        j	                  d:|	       Y d}	~	|S d}	~	ww xY w);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: %sr   rf   )r   r  tor   unknownr   r0  finalr   r1  r   r   )rewritesjobs_updatedjobs_scannedr   )rewrite_skill_refsr   z%Curator cron skill rewrite failed: %sr_   )rJ  rK  rL  error
started_atduration_secondsr7   r    r   auto_transitionsr   rK  )
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_namesr   rY  cron_rewritesr  llm_summarysummary	llm_errorrN  zrun.json)r]   ensure_asciir4  rG   rH   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   ra   rS   rT   strftimerK   r/   r.   r0   r   r5  r6  r   r  r  r  r.  r   rM  r   r   roundr7  ry   sumvalues
write_textrL   dumps_render_report_markdown)'rO  elapsed_secondsauto_countsauto_summarybefore_reportr9  r:  llm_metar   rZ   stamprun_dirsuffixr<  r=  r   r   r   before_by_nametransitionsr   s_befores_after	tc_countsr   r"  r#  r   r$  r>  r   r   r^  consolidated_mapr]  _rewrite_cron_refsppayloadmds'                                          r   _write_run_reportr|  E  s     ?D

4$
/
 0EUlGF
..
!E7!F8,, ..
dU3 0<S!z!T?RQUU6]A%SMSm((*+K\K/0G;-.E0=U1AtATaeeFmQ&UNU )+K{\12 P"&&t,277@ $$T*0b55g>H$7hgNO	P !#Ill<,2 5vvfi(#--a014	$5$ )<<b17R	I ,HLL",E,KLK{#c%+2&66L @\2&," /!3N ".1LH%F 24QXY$ZM
 "
!T"quuV}v fIqy 
 
  &
!T"quuV} fI
 
 |J.-#Mj**,E/15 	gr* 	HLLR0	
 	K 	,'%%L(99!$W!%j%(%6"6{!$[!1#&}'8'8'K#L #I$4$4$6 7
$ 	I%& 	G'( 	)* 	&+, 	F3q63-. 	/0 	[12 	34 	X\\'2.56 	x||Ir278 	X\\'*9: 	hll<4;GB=	:	))JJwqu=D 	* 	
>$W-	;	**2*@G}  34q8++77

=G$N  8  NU  ;Q?  8!<
 T VB


  
<a$OV	

> 4   =8!<<=  >91==>  GBAFFNGs   S3 (T ?UU$U:U2U 6AU8U >-U+U ,V/V# !W $AW= 3	T<TT	U(UU
U 	V((VV#	W,WW	W:W55W:=	X'X""X'c                f   g }| j                  dd      }| j                  dd      xs d}t        t        |      d      \  }}|r| d| dn| d}|j                  d| d	       | j                  d
      xs d}| j                  d      xs d}| j                  d      xs i }	|j                  d| d| d| d|	j                  dd       d|	j                  dd       d|	j                  dd      dd       | j                  d      }
|
r|j                  d|
 d       | j                  d      xs i }|j                  d       |j                  d|j                  dd              |j                  d |j                  d!d              |j                  d"|j                  d#d              |j                  d$|j                  d%d              |j                  d       | j                  d&      xs i }|j                  d'       |j                  d(|	j                  d)d       d*d+j	                  d, t        |j                               D              xs d- d.       |j                  d/|	j                  d0d       d1       |j                  d2|	j                  d3d       d1       |j                  d4|	j                  d5d       d1       |j                  d6|	j                  d7d       d1       |j                  d       | j                  d8      xs g }|r>|j                  d9t        |       d       |j                  d:       d;}|d<| D ]  }|j                  d=d>      }|j                  d?d>      }|j                  d@      xs dj                         }|j                  dAd      }dB| dC| dD}|r|dE| z  }|r|j                  dF      r	|dG| dHz  }|j                  |       |j                  dI      s|j                  dJ|dI    dK        t        |      |kD  r!|j                  dLt        |      |z
   dM       |j                  d       | j                  dN      xs g }|r|j                  dOt        |       d       |j                  dP       d;}|d<| D ]~  }t        |t              rW|j                  d=d>      }|j                  d@      xs dj                         }dB| dD}|r|dE| z  }|j                  |       j|j                  dB| dD        t        |      |kD  r!|j                  dLt        |      |z
   dM       |j                  d       | j                  dQ      xs g }|r\|j                  dRt        |       d       |j                  dS       |D ]  }|j                  dB| dD        |j                  d       | j                  d7      xs g }|r}|j                  dTt        |       d       |D ]I  }|j                  dB|j                  d=       dU|j                  dV       d|j                  dW              K |j                  d       | j                  dX      xs i }|j                  dY      xs g }|r|j                  dZt        |       d       |j                  d[       d\}|d<| D ]  }|j                  d]      xs |j                  d^      xs d>}|j                  d      xs g }|j                  d      xs g }|j                  d_      xs i }|j                  d`      xs g } |j                  dB| dad+j	                  |       dbd+j	                  |      xs dc dD       |j                         D ]  \  }!}"|j                  dd|! db|" de        | D ]  }|j                  dd| df         t        |      |kD  r!|j                  dLt        |      |z
   dg       |j                  d       | j                  dh      xs dj                         }#|#r4|j                  di       |j                  |#       |j                  d       nL|
sJ| j                  dj      xs d}$|$r3|j                  dk       |j                  |$       |j                  d       |j                  dl       |j                  dm       |j                  dn       |j                  do       |j                  d       d	j	                  |      S )pz!Render the human-readable report.rO  r   rP  r   <   zm su   # Curator run — r4  r    z(not resolved)r   r   zModel: `z` via `u   `  ·  Duration: u     ·  Agent-created skills: rR  r3  rS  z (rT  z+dz)
ra  u   > ⚠ LLM pass error: `z`
rQ  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  0   K   | ]  \  }}| d |   yw)=Nr+   ).0rX   rY   s      r   	<genexpr>z*_render_report_markdown.<locals>.<genexpr>(  s     'Ytq!1#Qqc
'Ys   none)z!- consolidated into umbrellas: **rW  z**z%- pruned (archived for staleness): **rX  z- new skills this run: **rV  u7   - state transitions (active ↔ stale ↔ archived): **rY  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)_r!  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  rG  r^  rJ  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.)r0   divmodry   r   r8  r6  rO   r7  r   rP   r/   r.   )%ry  r@  starteddurationminssecs	dur_labelr    provr   rN  autorv  r   r?  r  r   r   r	  r   liner   r   ntranstcron_rwcron_rewrites_listr  rR  rS  r  r  oldnewrI  llm_sums%                                        r   ri  ri    s-	   EeeL"%Guu'+0qHHr*JD$&*4&4&"4&
I	LL%gYb12EE'N..E550 0DUU8_"F	LL
5'&7	{ C!!'Ha!8 9vzz'ST?U>V WJJw"2&c	+ EE+E.ugS9: 55#$*D	LL78	LL;txx	15678	LL#DHH^Q$?#@AB	LLCDHHZYZD[C\]^	LL?488M1#=">?@	LL ()/RI	LL./	LL#FJJ/A1$E#F G"ii'YviooFW?X'YYc]cddeg h	LL4VZZ@WYZ5[4\\^_`	LL8DUWX9Y8ZZ\]^	LL,VZZ8H!-L,MRPQ	LL jj!4a89= >	LL 55(.BL>s<?P>QQTUV*	
 !%4( 	E99VS)D99VS)Dii)/R668FYYx,F1$q9D%x((&++,=> +F8266LLyy-.9%@T:U9V WE E	& |t#LL:c,&7$&>%??UVWR UU8_"F>s6{m3OPB	
 ET] 	-E %&yy-))H-3::<TF!}eF8,,DT"s5'^,	- v;LL:c&kD&8%99OPQR EE'N bE0UC@Apq 	%ALL3qc$	%R EE%&,"E.s5zl#>? 	TALL3quuV}oSvuQUU4[MRS	TR
 eeO$*G Z06B@EWAX@YY\]^R	

 '. 	AEyy,J		(0CJsHYYx(.BFIIg&,"EYYx(.BFii	*0bGLLhZtDIIf$5#6gdii>N>ZRZ=[[\] #LLN JSwse73%7GHIJ Awtf,>?@A	A !"T)LLS!34t;< =- - 	R UU;%2,,.E-.UR%%&,"LL+,LL!LL 
LL!	LLOP	LLef	LL[\	LL99Ur   c                    t        j                         } | syt               }dt        |        dg}| D ]  }|j	                  d|d    d|d    d|j                  d	      rd
nd d|d   |v rd
nd d|j                  dd       d|j                  dd       d|j                  dd       d|j                  dd       d|j                  d      xs d         dj                  |      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=rf   z	  pinned=r   yesnoz  cron=z  activity=activity_countr   z  use=r   z  view=
view_countz
  patches=patch_countz  last_activity=r   neverr4  )r   r   r   r7  r   r0   r8  )rowsr   r@  r<  s       r   _render_candidate_listr    s!   ++-D3-/O%c$i[56E 
6 wZL ! uuXeD9 :vY/9EtD E.23 455a() *EE,*+ ,uu]A./ 0UU#56A'BD
	

 99Ur   c                    
t               t        j                  t        j                        r&	 t        j                         }t        |      ddddn@	 ddl	m
} |j                  d      }| r	   d|j                   d       t              g }d   r|j!                  d    d       d   r|j!                  d    d       d   r|j!                  d    d       |rdj#                  |      ndt%               }	s4j'                         |	d<   t)        |	j+                  dd            dz   |	d<   rdnd  |	d<   t-        |	        fd}
|r |
        n(t/        j0                  |
d
d      }|j3                          j'                         dS # t        $ r dddddY )w xY w# t        $ r Y Ew xY w# t        $ r#}t        j                  d	|d
       Y d}~pd}~ww xY w)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: %sTr_   )r   r   z marked staler   z	 archivedr   z reactivatedr  z
no changesr>   rD   r   zdry-run auto: zauto: r@   c            
         	 t        j                         } | D ch c]$  }t        |t              s|j                  d      & }}s  d}ddddg d d}t        j                  t        j                        z
  j                         }t               }||d<   ||d<   	 t        j                         }	 t        || |||      }|t        |      |d	<   t!        |       r	  d|        y y i }	 t#               }
d|
v r  d}ddddg d d}nVd}t%               rd}rt&         dt(         | d|
 }nt(         | d|
 }t+        |      }  d|j                  dd       }	 t-        |t        j                         |j                  dg       xs g |j                  dd      xs d      }|r| d| }t        j                  t        j                        z
  j                         }t               }||d<   ||d<   	 t        j                         }	 t        || |||      }|t        |      |d	<   t!        |       r	  d|        y y # t        $ r g } Y Rw xY wc c}w # t        $ r g }Y w xY w# t        $ r#}	t        j                  d
|	d       Y d }	~	d }	~	ww xY w# t        $ r Y y w xY w# t        $ rC}	t        j                  d|	d         d|	 d}dd|	 dddg t        |	      d}Y d }	~	d }	~	ww xY w# t        $ r#}	t        j                  d|	d       Y d }	~	yd }	~	ww xY w# t        $ r g }Y 2w xY w# t        $ r#}	t        j                  d
|	d       Y d }	~	9d }	~	ww xY w# t        $ r Y y w xY w)Nr   z"; llm: skipped (consolidation off)r   zskipped (consolidation off)rI  r`  r    r   r   rN  r?   r@   )rO  rj  rk  rl  rm  r9  r:  rn  rB   zCurator report write failed: %sTr_   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   rI  )r9  r:  r   r;  r4  z'Curator rename summary build failed: %s)r   r   ra   r/   r.   r0   r   r   r   r   total_secondsr[   r|  r   rS   rT   rb   r  r   CURATOR_DRY_RUN_BANNERCURATOR_REVIEW_PROMPT_run_llm_reviewrE  )rm  r<  r9  final_summaryrn  elapsedstate2r:  report_pathrZ   candidate_listbuiltins_notepromptrename_linesrl  r   r   dry_run
on_summaryprefixstarts                 r   	_llm_passz%run_curator_review.<locals>._llm_pass/  sL   	'<<>M 0=T!
1d@SfTT (<.(JK  8 H  ||HLL1E9HHJG\F29F./)6F%&"*??AR/$$+ &!-"/!-!-%	 *14[1AF-. v=/:; #%3	35N(N:#)(<.8V W8 ""$! !#%'; " 12$01-)*,  !66}oT.IYZF*62h|nGHLLK4X3YZ &
	V0)(==?#<<b9?R$LL"5;	L #0/L> B <<-5DDF.5*+%2!"
	&;;=L	N+  '")+))!	K &-0-=)* 	6Y}o67 o  	M	T0  "!"  R>DQQR ! \  
	LL6DLI%h|nN1#QGM$QCq> QH
	2  	VLLBAPTLUU	V  	L	  	NLL:ALMM	N  s   I I-I-!I2 6#J 'J3 7A5K -AL M  #M N I*)I*2J J	J0J++J03	J?>J?	L8L		L	L=L88L= MM	M>M99M>	NNzcurator-review)r   daemonr   )rO  rQ  summary_so_far)r   r   r   r   r   r   r   r7  ra   agentr  snapshot_skillsr   rS   rT   r   r   r8  r[   r   ry   r0   rb   	threadingThreadr  )r  synchronousr  r   reportr  snaprZ   auto_summary_partsrf   r  r  rl  r   r  r  s   ` ``        @@@@r   run_curator_reviewr    s2   D %'LL&E		X 557Fv; ! 	F		R,!119J1KDJ!<TYYKqIJ
 -7n!!VN%;$<M"JKj!!VJ%7$8	"BCm!!VM%:$;<"HI4F499/0LL LE$0m ;!:;a?k!(hF#)(<. 9E
u` `D IdAQR		 oo'"& e  	X!"A1UVWF	X !  	RLL>DLQQ	RsG   $F5  G =G 5G	G		GG GG 	H%HHc                P   t        | j                  d      t              r| j                  di       ni }|j                  d      xs d}|j                  d      xs |j                  d      xs d}t        | j                  d      t              r| j                  di       ni }t        |j                  d      t              r|j                  di       ni }|j                  d      xs dj                         xs d}|j                  d      xs dj                         xs d}|r_|dk7  rZ|rXt	        ||t        |j                  d	            t        |j                  d
            t        i |j                  d                  S t        | j                  d      t              r| j                  di       ni }t        |j                  d      t              r|j                  di       ni }	|	j                  d      xs d}
|	j                  d      xs d}|
r|rt        j                  d       t	        t        |
      t        |      t        |	j                  d	            t        |	j                  d
            t        i |	j                  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   	auxiliaryrm   Napi_keybase_urlr-   u|   curator: using deprecated curator.auxiliary.{provider,model} config — please migrate to auxiliary.curator.{provider,model})
r/   r0   r.   r   r   r   r5   rS   infor   )ro   _main_main_provider_main_model_aux	_cur_task_task_provider_task_model_cur_legacy_legacy_provider_legacy_models               r   _resolve_review_runtimer    s1    %/swww/?$FCGGGR BEYYz*4fN))I&B%))G*<BK (2#''+2F'M377;#SUD+5dhhy6I4+PB'VXImmJ/52<<>F$N==)/R668@DK.F2{$!)--	":;!)--
";<$R|)DE
 	
 &0	0BD%I3779b!rD+5dhh{6KT+Rdhh{B'XZG{{:.6$KK(0DMMN	
 % !!'++i"89!'++j"9:$R\)BC
 	
 !dD"MMr   c                H    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    )ro   bs     r   _resolve_review_modelr    s!     	 $A::qwwr   c                6	   ddl }ddddg dd}	 ddlm} d}d}d}d}d}	i }
d}d}d}d}	 dd	lm} dd
lm}  |       }t        |      }|j                  |j                  }} ||||j                  |j                        }|j                  d      }|j                  d      }|j                  d      }|j                  d      xs |}|j                  d      }	t        |j                  d      |j                  j                  d            }
|j                  d      }|j                  d      }t!        |j                  d      xs g       }t#        |j                  d      t$              r&|d   j'                         r|d   j'                         }||d<   |xs d|d<   d}	 i }t#        |t,              r||d<   t#        |t$              r|r||d<   |xs g |d<    |d3||||||	|
d|dddddd }d|_        d|_        d!|_        t5        t6        j8                  d"d#$      5 }|j;                  |      5  |j=                  |      5  |j?                  | %      }ddd       ddd       ddd       d}t#        t@              r,t%        |j                  d&      xs d      j'                         }||d'<   tC        |      d(kD  r|dd( d)z   n|xs d*|d<   g }tE        |d+g       xs g D ]  }t#        |t@              s|j                  d,      xs g }|D ]  }t#        |t@              s|j                  d-      xs i }|j                  d.      xs d}|j                  d/      xs d} t#        | t$              rtC        |       d0kD  r| dd0 d)z   } |jG                  || d1         ||d,<   |	 |jI                          |S |S # t        $ r}d| |d<   |d   |d<   |cY d}~S d}~ww xY w# t        $ r#}t(        j+                  d|d       Y d}~|d}~ww xY w# 1 sw Y   xY w# 1 sw Y   xY w# 1 sw Y   xY w# t        $ r}d2| |d<   |d   |d<   Y d}~d}~ww xY w# t        $ r Y |S w xY w# |!	 |jI                          w # t        $ r Y w w xY ww xY w)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: rN  r`  rk   )resolve_runtime_provider)	requestedtarget_modelr"   r#   r  r  api_moder   credential_poolr%   r-   max_output_tokenscommandr   r    z&Curator provider resolution failed: %sTr_   
max_tokensacp_commandacp_args)r    r   r  r  r  r  r%   i'  rm   )max_iterations
quiet_modeplatformskip_context_filesskip_memorybackground_reviewwrG   rH   )user_messagefinal_responserI     u   …r  _session_messagesr   functionr   r   i  )r   r   zerror: r+   )%
contextlib	run_agentr  ra   rn   rl   hermes_cli.runtime_providerr  r  r   r    r"   r#   r0   r5   r%   r  r/   r   r   rS   rT   ry   _memory_nudge_interval_skill_nudge_interval_memory_write_originopenosdevnullredirect_stdoutredirect_stderrrun_conversationr.   r7  getattrr   close)!r  r  result_metar  rZ   _api_key	_base_url	_api_mode_resolved_provider_credential_pool_request_overrides_max_tokens_acp_command	_acp_args_model_namerl   r  _cfg_binding	_provider_rpreview_agent_agent_kwargs_devnullconv_resultrI  _callsmsgtcsr   fnr   args_raws!                                    r   r  r  !  s4    #K%" HII)+KLIKQ1H}*40!)!2!2HNN;	&$%66&88	
 779%GGJ'	GGJ'	 WWZ0=I77#455GG'(&&**<8
 gg12wwy).B/	cggg&,W1C1C1Eg,,,.K 'K06BK
LR(*k3'*5M,'lC(\+7M-((1RM*% 
',0
 
  #%
* /0+-.* -@) "**cG4 	M''1	M''1	M '77V7LK	M 	M 	M
 k4((89?R@FFHE$G:=e*s:J%+"5QVQeZeI (*<)<bAGR 	ECc4('','-2C E!"d+VVJ'-2vvf~+66+.4"h,X1D'~5Ht(CDE		E %+L!
 #""$ ;]  !8<G!,W!5Ih  Q=q4PPQf	M 	M 	M 	M 	M 	M<  6!(}G!,W!5I6   #""$  $s   N? E(O' #B P= #P05P#PP#"P0*D>P= +Q# ?	O$OO$O$'	P0PPP P##P-	(P00P:5P= =	Q QQ3 Q  Q3 #	Q0/Q03R7RR	RRRR)idle_for_secondsr  c                    	 t               sy| t               dz  }| |k  ryt        |      S # t        $ r"}t        j                  d|d       Y d}~y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: %sTr_   )r   r   r  ra   rS   rT   )r  r  
min_idle_srZ   s       r   maybe_run_curatorr    s_    '+-6J*,!Z88 3QFs   
. . . 	AAA)r   r   returnr!   )r2   r   r3   r   r   r$   )r   r   )r   r$   )rV   r$   r   None)rC   re   r   r!  )r   re   )r   ry   )r   r   )r   r!   r   Optional[datetime]r   )r   r"  r   re   )r   Set[str])r   r"  r   Dict[str, int])r   r   rU   r   r   re   )
r   	List[str]r   r%  r   r#  r   List[Dict[str, Any]]r   Dict[str, List[Dict[str, Any]]])r  r   r   Dict[str, List[Dict[str, str]]])r   r&  r   zDict[str, Dict[str, Any]])r   r%  r"  r'  r#  r(  r   r#  r$  z#Optional[Dict[str, Dict[str, Any]]]r   r'  )
r9  r#  r:  r&  r   r&  r;  r   r   r   )rO  r   rj  r   rk  r$  rl  r   rm  r&  r9  r#  r:  r&  rn  r$   r   zOptional[Path])ry  r$   r   r   )r   r   )NFFN)
r  Optional[Callable[[str], None]]r  re   r  re   r   zOptional[bool]r   r$   )ro   r$   r   r   )ro   r$   r   ztuple[str, str])r  r   r   r$   )r  zOptional[float]r  r)  r   zOptional[Dict[str, Any]])Ir)   
__future__r   rL   loggingr  r   r  r   r   r   pathlibr   typingr   r	   r
   r   r   r   r   hermes_constantsr   r   r   utilsr   	getLoggerr&   rS   r   r   r5   rz   r   r   r   r   r<   rE   r[   rb   rg   ri   rq   rv   r}   r   r   r   r   r   r   r   r   r   r  r  r   r   r  r  r  r.  rE  r|  ri  r  r  r  r  r  r  r+   r   r   <module>r1     s  * #   	 	  2 2  G G G ,  #			8	$&J &


 
      
  ;	K, *&&(*	1=(2$rNlD 8UM |,$w<w<w< w< %	w<
 %w<tNN$Nb3$33v BF@<@<.@< 1@< 	@<
 ?@< %@<FWW 'W %	W
 W 	Wt}} }  	}
 } (} } '} } }@tv. 37"&	E/EE E  	E
 EP-N`&f^ )-26% 0 	r   