
    `gj                       U 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mZ ddl	m	Z	m
Z
 ddlmZ ddlmZmZmZmZmZmZ ddlmZ dd	lmZmZ  ej2                  e      ZdZ	 ddlZd
ZdZ dZ!ee e!hZ"dhZ#de$d<   d@dZ%dAdZ&dAdZ'ed        Z(dAdZ)dBdZ*dCdZ+dDdZ,dEdZ-dFdZ.dFdZ/dGdZ0dAdZ1dFdZ2dHdZ3dIdZ4dId Z5dJd!Z6dJd"Z7dKd#Z8d@d$Z9d@d%Z:d@d&Z;dLd'Z<dMdNd(Z=dOd)Z>dPd*Z?dQd+Z@dRd,ZAdSd-ZBdId.ZCd/d0dTd1ZDdId2ZEdId3ZFdId4ZGdId5ZHdUd6ZIdVd7ZJdId8ZKdWd9ZLdWd:ZMdXd;ZNdXd<ZOdYd=ZPdLd>ZQdYd?ZRy# e$ r dZ	 ddlZn# e$ r Y nw xY wY w xY w)Za  Skill usage telemetry + provenance tracking for the Curator feature.

Tracks per-skill usage metadata in a sidecar JSON file (~/.hermes/skills/.usage.json)
keyed by skill name. Counters are bumped by the existing skill tools (skill_view,
skill_manage); the curator orchestrator reads the derived activity timestamp to
decide lifecycle transitions.

Design notes:
  - Sidecar, not frontmatter. Keeps operational telemetry out of user-authored
    SKILL.md content and avoids conflict pressure for bundled/hub skills.
  - Atomic writes via tempfile + os.replace (same pattern as .bundled_manifest).
  - All counter bumps are best-effort: failures log at DEBUG and return silently.
    A broken sidecar never breaks the underlying tool call.
  - Provenance filter: curator-managed skills are explicitly marked when
    created through skill_manage. Bundled / hub-installed skills stay
    off-limits, and manually authored skills are not inferred from location.

Lifecycle states:
    active    -> default
    stale     -> unused > stale_after_days (config)
    archived  -> unused > archive_after_days (config); moved to .archive/
    pinned    -> opt-out from auto transitions (boolean flag, orthogonal to state)
    )annotationsN)contextmanager)datetimetimezone)Path)AnyDictListOptionalSetTupleget_hermes_home)is_excluded_skill_pathis_external_skill_pathactivestalearchivedplanSet[str]PROTECTED_BUILTIN_SKILLSc                    | t         v S )a:  Whether *skill_name* is a load-bearing built-in the curator never touches.

    Protected built-ins are exempt from archival and consolidation on every
    path: the automatic state-transition walk, the LLM consolidation pass (they
    are dropped from the candidate list), and direct ``archive_skill`` calls.
    )r   
skill_names    D/root/.hermes/venv/lib/python3.12/site-packages/tools/skill_usage.pyis_protected_builtinr   G   s     111    c                     t               dz  S )Nskillsr    r   r   _skills_dirr!   Q   s    x''r   c                     t               dz  S )Nz.usage.jsonr!   r    r   r   _usage_filer$   U   s    ==((r   c               #  
  K   t               j                  d      } | j                  j                  dd       t        t
        d yt
        r@| j                         r| j                         j                  dk(  r| j                  dd       t        | t
        rd	nd
d      }	 t        r%t	        j                  |t        j                         nD|j                  d       t        j                  |j                         t
        j                   d       d t        r&	 t	        j                  |t        j"                         nKt
        rE	 |j                  d       t        j                  |j                         t
        j(                  d       |j+                          y# t$        t&        f$ r Y "w xY w# t$        t&        f$ r Y 7w xY w# t        r;	 t	        j                  |t        j"                         nv# t$        t&        f$ r Y new xY wt
        r[	 |j                  d       t        j                  |j                         t
        j(                  d       n# t$        t&        f$ r Y nw xY w|j+                          w xY ww)z@Serialize .usage.json read-modify-write cycles across processes.z
.json.lockTparentsexist_okNr    utf-8encodingzr+za+   )r$   with_suffixparentmkdirfcntlmsvcrtexistsstatst_size
write_textopenflockLOCK_EXseeklockingfilenoLK_LOCKLOCK_UNOSErrorIOErrorLK_UNLCKclose)	lock_pathfds     r   _usage_file_lockrE   Y   s     )),7I4$7}y'')Y^^-=-E-E-JS73	iTG	DBKKEMM*GGAJNN299;:B. 
ryy{FOOQ? 	
 W%  W%  B.W% 
ryy{FOOQ?W% 

s   B$J'A3G J!$F" JAF7 J"F41J3F44J7G	JG		JJ $G98J 9HJ 
H	J AIJ I,)J +I,,J  Jc                     t               dz  S )Nz.archiver#   r    r   r   _archive_dirrG   }   s    =:%%r   c                 d    t        j                  t        j                        j	                         S N)r   nowr   utc	isoformatr    r   r   _now_isorM      s    <<%//11r   c                    | sy	 t        j                  t        |             }|j
                   |j                  t        j                        }|S # t        t        f$ r Y yw xY w)z<Parse an ISO timestamp defensively for activity comparisons.N)tzinfo)	r   fromisoformatstr	TypeError
ValueErrorrO   replacer   rK   )valueparseds     r   _parse_iso_timestamprW      s_    ''E
3 }}x||4M	 z" s   A A#"A#c                    d}d}dD ]6  }| j                  |      }t        |      }|"|||kD  s*|}t        |      }8 |S )a(  Return the newest actual activity timestamp for a usage record.

    "Activity" means a skill was used, viewed, or patched. Creation time is
    intentionally excluded so callers can still distinguish never-active skills;
    lifecycle code can fall back to ``created_at`` as its own anchor.
    N)last_used_atlast_viewed_atlast_patched_at)getrW   rQ   )record	latest_dt
latest_rawkeyrawdts         r   latest_activity_atrc      s^     %)I $JD "jjo!#&:YISJ" r   c                    d}dD ]$  }	 |t        | j                  |      xs d      z  }& |S # t        t        f$ r Y :w xY w)zFReturn the total observed activity count across use/view/patch events.r   )	use_count
view_countpatch_count)intr\   rR   rS   )r]   totalr`   s      r   activity_countrj      sW    E9 	SC-A..E
 L :& 		s   !.A A c                    t               dz  } | j                         s
t               S t               }	 | j                  d      j	                         D ]L  }|j                         }|s|j                  dd      d   j                         }|s<|j                  |       N 	 |S # t        $ r!}t        j                  d|       Y d}~|S d}~ww xY w)	zReturn the set of skill names that were seeded from the bundled repo.

    Reads ~/.hermes/skills/.bundled_manifest (format: "name:hash" per line).
    Returns empty set if the file is missing or unreadable.
    z.bundled_manifestr*   r+   :r-   r   z#Failed to read bundled manifest: %sN)r!   r3   set	read_text
splitlinesstripsplitaddr?   loggerdebug)manifestnameslinenamees        r   _read_bundled_manifest_namesrz      s     }22H??ueE	?&&&8CCE 	 D::<D::c1%a(..0D		$	  L  ?:A>>L?s   AB% B% %	C.C

Cc                    t               dz  dz  } | j                         s
t               S 	 t        j                  | j                  d            }t        |t              r<|j                  d      xs i }t        |t              r|j                         D ch c]  }t        |       }}t               }|j                         D ]  }t        |t              s|j                  d      }t        |t              r|j                         sFt        |      }|j                         s||z  }	 |j                         }	|	j!                  |j                                |	dz  }
|
j                         s|j'                  t)        |
|	j*                                |S t               S c c}w # t"        t$        f$ r Y w xY w# t"        t        j,                  f$ r)}t.        j1                  d	|       Y d
}~t               S d
}~ww xY w)zReturn the set of skill names installed via the Skills Hub.

    Reads ~/.hermes/skills/.hub/lock.json (see tools/skills_hub.py :: HubLockFile).
    z.hubz	lock.jsonr*   r+   	installedinstall_pathSKILL.mdfallbackz Failed to read hub lock file: %sN)r!   r3   rm   jsonloadsrn   
isinstancedictr\   keysrQ   valuesrp   r   is_absoluteresolverelative_tor?   rS   rr   _read_skill_namerx   JSONDecodeErrorrs   rt   )rC   datar|   krv   
skills_direntryr}   	skill_dirresolvedskill_mdry   s               r   _read_hub_installed_namesr      s   
 &4Iu<zz)--w-?@dD!-3I)T*)2)9:AQ::(]
&--/ VE%eT2 #(99^#<L%lC8@R@R@T  $\ 2I$002$.$:	!#,#4#4#6 ,,Z-?-?-AB  (*4H(		"28hmm"TU!V"  5L- ; $Z0 ! ! T))* <7;;5L<sU   A.G F(,BG //F-G 4)G (G -F?<G >F??G HG??Hc                    	 ddl m}   |        }t        |t              r|j	                  d      nd}t        |t              rt        |j	                  dd            S 	 y# t        $ r }t        j                  d|       Y d}~yd}~ww xY w)u  Whether bundled built-in skills are eligible for curator pruning.

    Reads ``curator.prune_builtins`` from config (default True). Lazy import
    keeps this module importable without the CLI config layer (e.g. in the
    update/sync context); on any failure we fall back to the default. The real
    safety against a mass-prune is the curator's seed-on-first-sight, not this
    flag — built-ins only archive after a fresh inactivity window.
    r   )load_configcuratorNprune_builtinsTz)Failed to read curator.prune_builtins: %s)	hermes_cli.configr   r   r   r\   bool	Exceptionrs   rt   )r   cfgcurry   s       r   _prune_builtins_enabledr      s    E1m$.sD$9cggi tc4  0$788 !   E@!DDEs   AA 	B(BBc                     t               dz  S )Nz.curator_suppressedr#   r    r   r   _suppressed_filer     s    =000r   c                 v   t               } | j                         s
t               S t               }	 | j                  d      j	                         D ]8  }|j                         }|s|j                  d      r(|j                  |       : 	 |S # t        $ r!}t        j                  d|       Y d}~|S d}~ww xY w)u  Built-in skills the curator pruned — the re-seeder must leave archived.

    One skill name per line in ``~/.hermes/skills/.curator_suppressed``. This is
    what makes pruning a built-in durable: without it, ``hermes update`` would
    re-copy the bundled skill on the next sync.
    r*   r+   #z+Failed to read curator suppression list: %sN)r   r3   rm   rn   ro   rp   
startswithrr   r?   rs   rt   )pathrv   rw   ry   s       r   read_suppressed_namesr     s     D;;=ueEGNNGN4??A 	 D::<DDOOC0		$	  L  GBAFFLGs#   5B &B 8B 	B8B33B8c                   t               }	 |j                  j                  dd       dj                  t	        |             | rdndz   }t        j                  t        |j                        dd      \  }}	 t        j                  |dd	
      5 }|j                  |       |j                          t        j                  |j                                d d d        t        j                  ||       y # 1 sw Y    xY w# t        $ r' 	 t        j                   |        # t"        $ r Y  w xY ww xY w# t$        $ r"}t&        j)                  d|d       Y d }~y d }~ww xY w)NTr&   
 z.curator_suppressed_.tmpdirprefixsuffixwr*   r+   z,Failed to write curator suppression list: %sexc_info)r   r/   r0   joinsortedtempfilemkstemprQ   osfdopenwriteflushfsyncr<   rT   BaseExceptionunlinkr?   r   rs   rt   )rv   r   r   rD   tmpfry   s          r   _write_suppressed_namesr      s-   DW$6yy'54bA""s4;;'7@V_efC	2sW5 %	$% JJsD!	% %
  			#   	  WCQQUVVWsg   A,D4 9D AC5D 5C>:D 	D1D! D1!	D-*D1,D--D11D4 4	E=EEc                `    | syt               }| |vr|j                  |        t        |       yy)zBRecord that a built-in skill was pruned, so sync won't restore it.N)r   rr   r   r   rv   s     r   add_suppressed_namer   6  s1    !#E		*& r   c                `    | syt               }| |v r|j                  |        t        |       yy)z7Clear a built-in's suppression entry (e.g. on restore).N)r   discardr   r   s     r   remove_suppressed_namer   @  s1    !#EUj!& r   c                 B   t               } | j                         sg S t               }t               }t	               }t               }g }| j                  d      D ]  }t        |      rt        |      r	 |j                  |        t        ||j                  j                        }||v rSt        |      r_||v r|sf|j                  |       xt!        |j#                  |            s|j                  |        t%        t'        |            S # t        $ r Y w xY w)u  Enumerate skills the curator may manage.

    Always includes agent-authored skills (those marked in ``.usage.json`` via
    ``skill_manage(action="create")``). When ``curator.prune_builtins`` is
    enabled, bundled built-in skills are ALSO included even though they have no
    agent-created usage record — their inactivity clock is anchored on first
    sight (see ``apply_automatic_transitions``). Hub-installed skills are never
    included; manually authored skills are not inferred from filesystem
    location.
    r~   r   )r!   r3   r   rz   r   
load_usagerglobr   r   r   rS   r   r/   rx   r   append_is_curator_managed_recordr\   r   rm   )basehubbundledr   usagerv   r   rx   s           r   list_agent_created_skill_namesr   J  s    =D;;=	
#
%C*,G,.NLEEJJz* !(+ "(+	  &  8??3G3GH3;  %7? "LL)%))D/:T=> #e*+  		s   4D	DDc                     t               } | j                         sg S t        | j                         D ch c]  }|j	                         s|j
                  ! c}      S c c}w )a  Enumerate skills in ``~/.hermes/skills/.archive/``.

    Archive layout is flat (``.archive/<skill>/``) as set by ``archive_skill``,
    so the directory name is the skill name. Used by ``hermes curator
    list-archived`` to help users pass a name to ``hermes curator restore``.
    )rG   r3   r   iterdiris_dirrx   )archive_rootps     r   list_archived_skill_namesr     sJ      >L 	<#7#7#9HaQXXZ166HIIHs   A
Ac                Z   	 | j                  dd      dd }d}|j                  d      D ]l  }|j                         }|dk(  r|r |S d	} |s#|j	                  d
      s5|j                  dd      d   j                         j                  d      }|sj|c S  |S # t        $ r |cY S w xY w)z9Parse the `name:` field from a SKILL.md YAML frontmatter.r*   rT   )r,   errorsNi  Fr   z---Tzname:rl   r-   z"')rn   r?   rq   rp   r   )r   r   textin_frontmatterrw   strippedrU   s          r   r   r     s    !!79!EetL N

4  
::<u O "Nh11':NN3*1-335;;EBE
 O  s   B B*)B*c                p    t               t               z  }| |v ryt        |       du xr t        |       du S )z:Whether *skill_name* is neither bundled nor hub-installed.FN)rz   r   _find_skill_dir_find_external_skill_dir)r   
off_limitss     r   is_agent_createdr     sG    -/2K2MMJZ
#t+ 	=$Z0< r   c                    | t               v S )z6Whether *skill_name* was installed via the Skills Hub.)r   r   s    r   is_hub_installedr     s    2444r   c                    | t               v S )z=Whether *skill_name* was seeded from the bundled repo skills.)rz   r   s    r   
is_bundledr     s    5777r   c                    d|  dS )Nskill 'zM' lives in skills.external_dirs; external skills are read-only to the curatorr    r   s    r   _external_read_only_messager     s    
* 7 	7r   c                    |t        |      ryt        |       ryt        |       ryt        |       r
t	               S t        |       }|t        |       S t        |       yy)u  Whether the curator may track/archive *skill_name*.

    Agent-created skills are always eligible. Bundled built-ins become eligible
    only when ``curator.prune_builtins`` is enabled. Hub-installed and external
    skill-dir skills are NEVER eligible — they have an external upstream owner.
    Protected built-ins (``PROTECTED_BUILTIN_SKILLS``) are NEVER eligible
    regardless of any flag — they back load-bearing UX and must never be
    archived or consolidated.
    FT)r   r   r   r   r   r   r   )r   
skill_path	local_dirs      r   is_curation_eligibler     sk     "8"DJ'
#*&((
+I))444
+7r   c                v    t        | t              sy| j                  d      dk(  xs | j                  d      du S )zEReturn True when a usage record opts a skill into curator management.F
created_byagentagent_createdT)r   r   r\   )r]   s    r   r   r     s7    fd#::l#w.U&**_2MQU2UUr   c                 6    d ddd d dd t               t        dd dS )Nr   F)r   re   rf   rY   rZ   rg   r[   
created_atstatepinnedarchived_at)rM   STATE_ACTIVEr    r   r   _empty_recordr     s/    j r   c                    t               } | j                         si S 	 t        j                  | j	                  d            }t        |t              si S i }|j                         D ]$  \  }}t        |t              s||t        |      <   & |S # t
        t        j                  f$ r#}t        j                  d| |       i cY d}~S d}~ww xY w)zGRead the entire .usage.json map. Returns empty dict on missing/corrupt.r*   r+   zFailed to read %s: %sN)r$   r3   r   r   rn   r?   r   rs   rt   r   r   itemsrQ   )r   r   ry   cleanr   vs         r   r   r     s    =D;;=	zz$..'.:; dD!	')E

 1aE#a&M L T))* ,dA6	s   %B C)CCCc                   t               }	 |j                  j                  dd       t        j                  t        |j                        dd      \  }}	 t        j                  |dd      5 }t        j                  | |d	dd
       |j                          t        j                  |j                                ddd       t        j                  ||       y# 1 sw Y    xY w# t        $ r' 	 t        j                  |        # t         $ r Y  w xY ww xY w# t"        $ r#}t$        j'                  d||d       Y d}~yd}~ww xY w)uN   Write the usage map atomically. Best-effort — errors are logged, not raised.Tr&   z.usage_r   r   r   r*   r+      F)indent	sort_keysensure_asciiNzFailed to write %s: %sr   )r$   r/   r0   r   r   rQ   r   r   r   dumpr   r   r<   rT   r   r   r?   r   rs   rt   )r   r   rD   tmp_pathr   ry   s         r   
save_usager     s   =DG$6''DKK 6
H	2sW5 %		$!t%P	$% JJx&	% %
  			(#   	  G-tQFFGsg   AD C) 0AC>C) C&"C) )	D3D	D		DDDDD 	E%EEc                    t               }|j                  |       }t        |t              s
t	               S t	               }|j                         D ]  \  }}|j                  ||        |S )zDReturn the record for *skill_name*, creating a fresh one if missing.)r   r\   r   r   r   r   
setdefault)r   r   recr   r   r   s         r   
get_recordr     s\    <D
((:
Cc4 ?D

 1q!Jr   c                T   | rt        |       sy	 t               5  t               }t        |j	                  |       t
              r
	 ddd       yt               || <   t        |       ddd       y# 1 sw Y   yxY w# t        $ r#}t        j                  d| |d       Y d}~yd}~ww xY w)u  Persist a baseline usage record for a curation-eligible skill.

    Built-ins carry no usage record until something touches them, which leaves
    their inactivity clock with no anchor. Seeding a record here fixes
    ``created_at`` to the moment the curator first sees the skill, so the
    archive/stale clock measures non-use FROM THEN — not from epoch. No-op when
    a record already exists or the skill isn't curation-eligible.
    Nz1skill_usage.seed_record_if_missing(%s) failed: %sTr   )r   rE   r   r   r\   r   r   r   r   rs   rt   r   r   ry   s      r   seed_record_if_missingr  -  s     1*=h 	<D$((:.5	 	  -Dt	 	 	  hH*VWbfgghs@   
A; +A/A; A/&A; /A84A; 8A; ;	B'B""B'Frequire_curation_eligiblec               ^   | sy	 |rt        |       syt               5  t               }|j                  |       }t	        |t
              s
t               } ||       ||| <   t        |       ddd       y# 1 sw Y   yxY w# t        $ r#}t        j                  d| |d       Y d}~yd}~ww xY w)u  Load, apply *mutator(record)* in place, save. Best-effort.

    By default this records telemetry for ANY skill — bundled, hub-installed,
    or agent-created — because usage tracking is pure observability and is
    orthogonal to whether a skill is ever curated. Lifecycle mutators
    (``set_state``, ``set_pinned``, ``mark_agent_created``) pass
    ``require_curation_eligible=True`` so they never write meaningless state
    onto a skill the curator can't manage (e.g. an ``archived`` flag on a
    hub-installed skill).
    Nz"skill_usage._mutate(%s) failed: %sTr   )r   rE   r   r\   r   r   r   r   r   rs   rt   )r   mutatorr  r   r  ry   s         r   _mutater  C  s     Y$-A*-M 	<D((:&Cc4(#oCL"Dt	 	 	  Y9:qSWXXYs:   B  
B  AA4+B  4A=9B  =B   	B,	B''B,c                $    dd}t        | |       y)u   Bump view_count and last_viewed_at. Called from skill_view().

    Tracks every skill regardless of provenance — built-ins and hub skills
    included. Usage telemetry is observability, not a curation signal.
    c                f    t        | j                  d      xs d      dz   | d<   t               | d<   y )Nrf   r   r-   rZ   rh   r\   rM   r  s    r   _applyzbump_view.<locals>._applyi  s1     5 :;a?L (
r   Nr  Dict[str, Any]returnNoner  r   r  s     r   	bump_viewr  c  s    + Jr   c                $    dd}t        | |       y)zBump use_count and last_used_at. Called when a skill is actively used
    (e.g. loaded into the prompt path or referenced from an assistant turn).

    Tracks every skill regardless of provenance.
    c                f    t        | j                  d      xs d      dz   | d<   t               | d<   y )Nre   r   r-   rY   r  r  s    r   r  zbump_use.<locals>._applyu  s0    sww{38q9A=K&jNr   Nr  r  r  s     r   bump_user  o  s    ) Jr   c                $    dd}t        | |       y)zBump patch_count and last_patched_at. Called from skill_manage (patch/edit).

    Tracks every skill regardless of provenance.
    c                f    t        | j                  d      xs d      dz   | d<   t               | d<   y )Nrg   r   r-   r[   r  r  s    r   r  zbump_patch.<locals>._apply  s1     !7!<1=AM!)r   Nr  r  r  s     r   
bump_patchr  {  s    
, Jr   c                (    dd}t        | |d       y)zOpt a skill created by skill_manage into curator management.

    Viewing or invoking a manually authored skill may still create telemetry,
    but only this explicit marker makes it eligible for automatic curation.
    c                    d| d<   y )Nr   r   r    r  s    r   r  z"mark_agent_created.<locals>._apply  s    #Lr   Tr  Nr  r  r  s     r   mark_agent_createdr     s    $J$?r   c                n    t         vrt        j                  d|        ydfd}t        | |d       y)zSet lifecycle state. No-op if *state* is invalid or the skill isn't
    curator-manageable (hub skills, or built-ins with pruning disabled).z"set_state: invalid state %r for %sNc                \    | d<   t         k(  rt               | d<   y t        k(  rd | d<   y y )Nr   r   )STATE_ARCHIVEDrM   r   )r  r   s    r   r  zset_state.<locals>._apply  s7    GN"!)Cl"!%C #r   Tr  r  )_VALID_STATESrs   rt   r  )r   r   r  s    ` r   	set_stater%    s4     M!95*M& J$?r   c                .    dfd}t        | |d       y )Nc                "    t              | d<   y )Nr   )r   )r  r   s    r   r  zset_pinned.<locals>._apply  s    VHr   Tr  r  r  )r   r   r  s    ` r   
set_pinnedr(    s    %J$?r   c                    | sy	 t               5  t               }| |v r|| = t        |       ddd       y# 1 sw Y   yxY w# t        $ r#}t        j                  d| |d       Y d}~yd}~ww xY w)zFDrop a skill's usage entry entirely. Called when the skill is deleted.Nz!skill_usage.forget(%s) failed: %sTr   )rE   r   r   r   rs   rt   r  s      r   forgetr*    st    X 	!<DT!$4 		! 	! 	!
  X8*aRVWWXs.   
A 5A >A A 	A-
A((A-c                   t        |       }|t        |       dt        |       fS t        | |      s.t	        |       rdd|  dfS t        |       rdd|  dfS dd|  dfS |}|dd|  dfS t        |      rdt        |       fS t               }	 |j                  dd	       ||j                  z  }|j                         rD||j                   dt        j                  t        j                        j!                  d       z  }	 |j#                  |       t-        |       rt/        |        t1        | t2               dd| fS # t        $ r}dd
| fcY d}~S d}~ww xY w# t        $ rV}ddl}	 |j'                  t)        |      t)        |             n"# t*        $ r}dd| fcY d}~cY d}~S d}~ww xY wY d}~d}~ww xY w)as  Move a curator-eligible skill directory to ~/.hermes/skills/.archive/.

    Returns (ok, message). Never archives hub-installed skills. Bundled
    built-ins are only archivable when ``curator.prune_builtins`` is enabled;
    when one is archived, its name is added to the suppression list so the
    update-time re-seeder leaves it archived instead of restoring it.
    NFr   zY' is a protected built-in; it backs load-bearing UX and is never archived or consolidatedz!' is hub-installed; never archivezJ' is a bundled built-in; enable curator.prune_builtins to allow pruning itz' not foundTr&   zfailed to create archive dir: -z%Y%m%d%H%M%Sr   zfailed to archive: zarchived to )r   r   r   r   r   r   r   rG   r0   r?   rx   r3   r   rJ   r   rK   strftimerenameshutilmoverQ   r   r   r   r%  r#  )r   local_skill_dirr   r   ry   destr/  e2s           r   archive_skillr4    s    &j1O#;J#G#S1*===
O<
+* &H H  J'GJ</PQQQj\ "9 9
 	

  I
|;777i(1*===>L;4$7 )..(D{{} 0(,,x||2L2U2UVd2e1fgg5 *J'j.)<v&&&1  ;6qc:::;  5	5KKID	2 	5/t4444	5 3	5sl   E E! 	EEEE!	G *F;/$FF;	F3F.#F3$F;(G .F33F;;G c           	        t        |       rdd|  dfS t        |       rt               sdd|  dfS t               }|j	                         sy|j                  d      D cg c]%  }|j                         s|j                  | k(  s$|' }}|s|  d}t        |j                  d      D cg c]y  }|j                         rg|j                  j                  |      rLt        |j                        t        |      z
  dk(  r(|j                  t        |      d	 j                         r|{ c}d
      }|sdd|  dfS |d   }t               | z  }|j	                         rdd| fS 	 |j                  |       t'        |        t)        | t*               d
d| fS c c}w c c}w # t        $ rL dd	l}	 |j!                  t#        |      t#        |             n# t$        $ r}dd| fcY d	}~cY S d	}~ww xY wY w xY w)u<  Move an archived skill back to ~/.hermes/skills/. Restores to the flat
    top-level layout; original category nesting is NOT reconstructed.

    Refuses to restore under a name that now collides with a hub-installed
    skill — that would shadow the upstream version. Also refuses to restore
    over a bundled built-in UNLESS ``curator.prune_builtins`` is enabled (in
    which case built-ins are curator-managed and restoring is the documented
    way to lift a prune). Restoring clears any suppression entry so future
    updates may re-seed the built-in again.
    Fr   zA' is now hub-installed; restore would shadow the upstream versionz;' is now bundled; restore would shadow the upstream version)Fzno archive directory*r,     NT)reversez' not found in archiver   zdestination already exists: zfailed to restore: zrestored to )r   r   r   rG   r3   r   r   rx   r   r   lenisdigitr!   r.  r?   r/  r0  rQ   r   r   r%  r   )	r   r   r   
candidatesr   srcr2  r/  ry   s	            r   restore_skillr=    s    
#j\ "8 8
 	
 *&=&?j\ "8 8
 	
  >L ,
 *//4\
qvvQ[G[!\J\ <q!'--c288:FF%%f-K#f+-3FF3v;<(002	  	

 
|+ABBB
Q-C=:%D{{}4TF;;;4

4 :&j,'<v&&&S ]&  4	4KKC#d), 	4/s333	4 -4s[   F5FF*A>F%F" "G70$GG7	G1G,$G1%G7,G11G76G7c                    t               }|j                         sy|j                  d      D ]L  }t        |      rt	        |      rt        ||j                  j                        | k(  s@|j                  c S  y)zLocate the directory for a skill by its frontmatter `name:` field.

    Handles both flat (~/.hermes/skills/<skill>/SKILL.md) and category-nested
    (~/.hermes/skills/<category>/<skill>/SKILL.md) layouts.
    Nr~   r   )r!   r3   r   r   r   r   r/   rx   )r   r   r   s      r   r   r   @  sm     =D;;=JJz* #!(+!(+Hx/C/CD
R??"# r   c                    ddl m}  |       dd D ]i  }|j                         s|j                  d      D ]B  }t	        |      rt        ||j                  j                        | k(  s4|j                  c c S  k y)zBLocate a skill under configured external dirs by frontmatter name.r   )get_all_skills_dirsr-   Nr~   r   )agent.skill_utilsr@  r3   r   r   r   r/   rx   )r   r@  r   r   s       r   r   r   S  su    5#%ab) '{{}

:. 	'H%h/8??3G3GHJV&		'' r   c                    t               } g }t               D ]  }| j                  |      }t        |t              }t        |t              r|n	t               }t               }|j                         D ]  \  }}|j                  ||        d|i|d|i}	t        |	      |	d<   t        |	      |	d<   |j                  |	        |S )a  Return a list of {name, state, pinned, last_activity_at, ...}
    records for every curator-managed skill. Missing usage records are
    backfilled with defaults so callers can always index fields.

    Each row carries ``_persisted``: True when a real record exists in
    ``.usage.json``, False when the row is a fresh backfill (e.g. a built-in
    seen for the first time). The curator uses this to seed the inactivity
    clock instead of treating an unrecorded skill as ancient.
    rx   
_persistedlast_activity_atrj   )r   r   r\   r   r   r   r   r  rc   rj   r   )
r   rowsrx   ra   	persistedr  r   r   r   rows
             r   agent_created_reportrH  f  s     <D!#D.0 
hhtnsD)	%/T%:cJJL 	!DAqNN1a 	!t<s<L)<"4S"9 .s 3C
 Kr   c                4    t        |       ryt        |       ryy)u   Classify a skill's origin: 'hub', 'bundled', or 'agent'.

    'agent' covers both agent-authored and local manually-authored skills —
    anything not seeded from the bundled repo or installed via the hub.
    r   r   r   )r   r   r   s    r   
provenancerJ    s     
#*r   c                    t               } | j                         sg S t               }g }t               }| j	                  d      D ]  }t        |      rt        ||j                  j                        }||v r5|j                  |       |j                  |      }t        |t              }t        |t              r|n	t               }t               }	|	j                         D ]  \  }
}|j                  |
|        d|i|t!        |      |d}t#        |      |d<   t%        |      |d<   |j'                  |        t)        |d       S )	u  Return usage telemetry for EVERY skill on disk, with provenance.

    Unlike ``agent_created_report()`` (which is scoped to curator-managed
    candidates), this surfaces all skills — bundled built-ins and
    hub-installed included — so callers can answer "how often is this skill
    used" independent of whether it's ever curated. Rows carry a
    ``provenance`` field ('agent' | 'bundled' | 'hub') and ``_persisted``
    (whether a real ``.usage.json`` record backs the row).
    r~   r   rx   )rJ  rC  rD  rj   c                    | d   S )Nrx   r    )rs    r   <lambda>zusage_report.<locals>.<lambda>  s
    ai r   )r`   )r!   r3   r   rm   r   r   r   r/   rx   rr   r\   r   r   r   r   r  rJ  rc   rj   r   r   )r   r   rE  seenr   rx   ra   rF  r  base_recr   r   rG  s                r   usage_reportrQ    s:    =D;;=	<D!#DDJJz* !(+8??3G3GH4<hhtnsD)	%/T%:c ?NN$ 	!DAqNN1a 	! D

 %T*#	
 #5S"9 .s 3C+, $/00r   )r   rQ   r  r   )r  r   )r  rQ   )rU   r   r  zOptional[datetime])r]   r  r  zOptional[str])r]   r  r  rh   )r  r   )r  r   )rv   r   r  r  )r   rQ   r  r  )r  z	List[str])r   r   r   rQ   r  rQ   )r   rQ   r  rQ   rI   )r   rQ   r   Optional[Path]r  r   )r]   r   r  r   )r  r  )r  Dict[str, Dict[str, Any]])r   rS  r  r  )r   rQ   r  r  )r   rQ   r  r   r  r  )r   rQ   r   rQ   r  r  )r   rQ   r   r   r  r  )r   rQ   r  zTuple[bool, str])r   rQ   r  rR  )r  zList[Dict[str, Any]])S__doc__
__future__r   r   loggingr   r   
contextlibr   r   r   pathlibr   typingr   r	   r
   r   r   r   hermes_constantsr   rA  r   r   	getLogger__name__rs   r2   r1   ImportErrorr   STATE_STALEr#  r$  r   __annotations__r   r!   r$   rE   rG   rM   rW   rc   rj   rz   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*  r4  r=  r   r   rH  rJ  rQ  r    r   r   <module>r`     s  0 #   	  % '  8 8 , L			8	$ 
 {N; & ( 
2()    F&2
(.#L*1*W,''4n
J*5
8
4V (G0
h, LQ Y@	 	  @@@X$:'zH'V&&4
&1A  E 	s6   !D9 9EEEEEEEE