
    Pmj}                       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j        e          ZdZ	 ddlZn# e$ r dZ	 ddlZn# e$ r Y nw xY wY nw xY wd
ZdZ dZ!ee e!hZ"dhZ#de$d<   dbdZ%dcdZ&dcdZ'ed             Z(dcdZ)dddZ*dedZ+dfd"Z,dgd$Z-dhd%Z.dhd&Z/did'Z0dcd(Z1dhd)Z2djd,Z3dkd-Z4dkd.Z5dld0Z6dld1Z7dmd4Z8dbd5Z9dbd6Z:dbd7Z;dnd8Z<dodpd;Z=dqd<Z>dbd=Z?dld>Z@drd@ZAdsdBZBdtdCZCdudEZDdvdGZEdwdHZFdkdIZGdJdKdxdMZHdkdNZIdkdOZJdkdPZKdkdQZLdydSZMdzdUZNd{dWZOdbdXZPdkdYZQdsdZZRdsd[ZSd|d\ZTd|d]ZUdrd^ZVdrd_ZWdnd`ZXdrdaZYdS )}a  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_SKILLS
skill_namestrreturnboolc                    | 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   r   s    7/home/thesage/.hermes/hermes-agent/tools/skill_usage.pyis_protected_builtinr   G   s     111    r   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               #  X  K   t                                          d          } | j                            dd           t          t
          dV  dS t
          rH|                                 r|                                 j        dk    r| 	                    dd           t          | t
          rd	nd
d          }	 t          r t	          j        |t          j                   nG|                    d           t          j        |                                t
          j        d           dV  t          r8	 t	          j        |t          j                   n~# t$          t&          f$ r Y nkw xY wt
          r`	 |                    d           t          j        |                                t
          j        d           n# t$          t&          f$ r Y nw xY w|                                 dS # t          r8	 t	          j        |t          j                   n~# t$          t&          f$ r Y nkw xY wt
          r`	 |                    d           t          j        |                                t
          j        d           n# t$          t&          f$ r Y nw xY w|                                 w xY 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_lockrH   Y   s      )),77I4$777} 4y'')) 4Y^^-=-=-E-J-JS7333	i1TG	D	D	DB 	;KEM****GGAJJJN299;;::: 
	B....W%    	


ryy{{FOQ????W%   





  
	B....W%    	


ryy{{FOQ????W%   




s{   ?A2G- 9E E-,E-8AG   GG-J)6HJ)H*'J))H**
J)5AI=<J)=JJ)JJ)c                 $    t                      dz  S )Nz.archiver&   r#   r    r   _archive_dirrJ   }   s    ==:%%r    c                 b    t          j        t          j                                                  S N)r   nowr   utc	isoformatr#   r    r   _now_isorP      s     <%%//111r    valuer   Optional[datetime]c                    | sdS 	 t          j        t          |                     }n# t          t          f$ r Y dS w xY w|j         |                    t          j                  }|S )z<Parse an ISO timestamp defensively for activity comparisons.N)tzinfo)	r   fromisoformatr   	TypeError
ValueErrorrT   replacer   rN   )rQ   parseds     r   _parse_iso_timestamprZ      su     t'E

33z"   tt}x|44Ms   !( ==recordDict[str, Any]Optional[str]c                    d}d}dD ]B}|                      |          }t          |          }|)|||k    r|}t          |          }C|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)getrZ   r   )r[   	latest_dt
latest_rawkeyrawdts         r   latest_activity_atrh      sf     %)I $JD " "jjoo!#&&:YISJr    intc                    d}dD ]A}	 |t          |                     |          pd          z  }+# t          t          f$ r Y >w xY w|S )zFReturn the total observed activity count across use/view/patch events.r   )	use_count
view_countpatch_count)ri   rb   rV   rW   )r[   totalre   s      r   activity_countro      sh    E9  	SC-A...EE:& 	 	 	H	Ls   '0AAc                    t                      dz  } |                                 st                      S t                      }	 |                     d                                          D ]^}|                                }|s|                    dd          d                                         }|r|                    |           _n2# t          $ r%}t          
                    d|           Y d}~nd}~ww xY w|S )	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.   :r0   r   z#Failed to read bundled manifest: %sN)r$   r6   set	read_text
splitlinesstripsplitaddrB   loggerdebug)manifestnameslinenamees        r   _read_bundled_manifest_namesr      s    }}22H?? uueeE	?&&&88CCEE 	  	 D::<<D ::c1%%a(..00D  		$	   ? ? ?:A>>>>>>>>?Ls   BC 
C:C55C:c                    t                      dz  dz  } |                                 st                      S 	 t          j        |                     dd                    }t          |t                    r|                    d          pi }t          |t                    rZd |	                                D             }t                      }|
                                D ]}t          |t                    s|                    d          }t          |t                    r|                                sXt          |          }|                                s||z  }	 |                                }|                    |                                           n# t"          t$          f$ r Y w xY w|d	z  }	|	                                r)|                    t)          |	|j        
                     |S n># t"          t          j        f$ r%}
t.                              d|
           Y d}
~
nd}
~
ww xY wt                      S )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-   rX   r/   errors	installedc                ,    h | ]}t          |          S r#   )r   ).0ks     r   	<setcomp>z,_read_hub_installed_names.<locals>.<setcomp>   s    :::AQ:::r    install_pathSKILL.mdfallbackz Failed to read hub lock file: %sN)r$   r6   rr   jsonloadsrs   
isinstancedictrb   keysvaluesr   ru   r   is_absoluteresolverelative_torB   rW   rw   _read_skill_namer}   JSONDecodeErrorrx   ry   )rF   datar   r{   
skills_direntryr   	skill_dirresolvedskill_mdr~   s              r   _read_hub_installed_namesr      sK   
 &4I uu"< z)--wy-QQRRdD!! 	--3I)T** ::)9)9:::(]]
&--// V VE%eT22 ! #(99^#<#<L%lC88 !@R@R@T@T !  $\ 2 2I$0022 ;$.$:	!#,#4#4#6#6 ,,Z-?-?-A-ABBBB#Z0 ! ! ! !'*4H(( V		"28hm"T"T"TUUUT)* < < <7;;;;;;;;<55Ls=   D,G? %;F! G? !F52G? 4F55AG? ?H:H55H:c                 V   	 ddl m}   |             }t          |t                    r|                    d          nd}t          |t                    r#t          |                    dd                    S n2# t          $ r%}t                              d|           Y d}~nd}~ww xY wdS )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   rb   r   	Exceptionrx   ry   )r   cfgcurr~   s       r   _prune_builtins_enabledr      s    E111111kmm$.sD$9$9Ccggi   tc4   	9 0$77888	9 E E E@!DDDDDDDDE4s   A3A7 7
B&B!!B&c                 $    t                      dz  S )Nz.curator_suppressedr&   r#   r    r   _suppressed_filer     s    ==000r    c                    t                      } |                                 st                      S t                      }	 |                     d                                          D ]B}|                                }|r*|                    d          s|                    |           Cn2# t          $ r%}t          
                    d|           Y d}~nd}~ww xY w|S )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   r6   rr   rs   rt   ru   
startswithrw   rB   rx   ry   )pathr{   r|   r~   s       r   read_suppressed_namesr     s     D;;== uueeEGNNGN44??AA 	  	 D::<<D  DOOC00  		$	   G G GBAFFFFFFFFGLs    A+B, ,
C6CCr{   Nonec                   t                      }	 |j                            dd           d                    t	          |                     | rdndz   }t          j        t          |j                  dd          \  }}	 t          j	        |dd	
          5 }|
                    |           |                                 t          j        |                                           d d d            n# 1 swxY w Y   t          j        ||           d S # t          $ r( 	 t          j        |           n# t"          $ r Y nw xY w w xY w# t$          $ r(}t&                              d|d           Y d }~d S d }~ww xY w)NTr)   
 z.curator_suppressed_.tmpdirprefixsuffixwr-   r.   z,Failed to write curator suppression list: %sexc_info)r   r2   r3   joinsortedtempfilemkstempr   osfdopenwriteflushfsyncr?   rX   BaseExceptionunlinkrB   r   rx   ry   )r{   r   r   rG   tmpfr~   s          r   _write_suppressed_namesr   (  s   DW$666yy''5+@44bA"s4;'7'7@V_efffC	2sW555 %			$$$% % % % % % % % % % % % % % % JsD!!!!! 	 	 		#   	  W W WCQQUVVVVVVVVVWss   A1E D AC5)D 5C99D <C9=D 
E	"D76E	7
EE	EE		E 
E>E99E>c                ~    | sdS t                      }| |vr&|                    |            t          |           dS dS )zBRecord that a built-in skill was pruned, so sync won't restore it.N)r   rw   r   r   r{   s     r   add_suppressed_namer   >  sS     !##E		*&&&&& r    c                ~    | sdS t                      }| |v r&|                    |            t          |           dS dS )z7Clear a built-in's suppression entry (e.g. on restore).N)r   discardr   r   s     r   remove_suppressed_namer   H  sS     !##EUj!!!&&&&& r    	List[str]c                    t                      } |                                 sg S t                      }t                      }t	                      }t                      }g }|                     d          D ]}t          |          rt          |          r"	 |	                    |            n# t          $ r Y Ew xY wt          ||j        j                  }||v rit          |          ry||v r|s|                    |           t!          |                    |                    s|                    |           t%          t'          |                    S )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$   r6   r   r   r   
load_usagerglobr   r   r   rW   r   r2   r}   r   append_is_curator_managed_recordrb   r   rr   )basehubbundledr   usager{   r   r}   s           r   list_agent_created_skill_namesr   R  sy    ==D;;== 	
#
%
%C*,,G,..NLLEEJJz**  !(++ 	 "(++ 		  &&&& 	 	 	H	8?3GHHH3;;  %% 	7?? " LL)%))D//:: 	T#e**s   B--
B:9B:c                     t                      } |                                 sg S t          d |                                 D                       S )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``.
    c                D    h | ]}|                                 |j        S r#   is_dirr}   )r   ps     r   r   z,list_archived_skill_names.<locals>.<setcomp>  s'    HHHaQXXZZH16HHHr    )rJ   r6   r   iterdir)archive_roots    r   list_archived_skill_namesr     sO      >>L   	HH<#7#7#9#9HHHIIIr    r   r   c                   	 |                      dd          dd         }n# t          $ r |cY S w xY wd}|                    d          D ]}|                                }|dk    r|r nbd	}#|r\|                    d
          rG|                    dd          d                                                             d          }|r|c S |S )z9Parse the `name:` field from a SKILL.md YAML frontmatter.r-   rX   r   Ni  Fr   z---Tzname:rq   r0   z"')rs   rB   rv   ru   r   )r   r   textin_frontmatterr|   strippedrQ   s          r   r   r     s   !!79!EEeteL   N

4   
 
::<<u !N 	h11':: 	NN3**1-3355;;EBBE Os   " 11c                    t                      t                      z  }| |v rdS t          |           du ot          |           du S )z:Whether *skill_name* is neither bundled nor hub-installed.FN)r   r   _find_skill_dir_find_external_skill_dir)r   
off_limitss     r   is_agent_createdr     sX    -//2K2M2MMJZu
##t+ 	=$Z0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44r    c                "    | t                      v S )z=Whether *skill_name* was seeded from the bundled repo skills.)r   r   s    r   
is_bundledr     s    57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 	7 	7r    
skill_pathOptional[Path]c                   |t          |          rdS t          |           rdS t          |           rdS t          |           rt	                      S t          |           }|t          |           S t          |           dS dS )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.
    Org-shared skills ARE eligible for improvement (the curator may patch them
    like any other skill; edits stay local until proposed) but are protected
    from ARCHIVE/DELETE elsewhere — removing a shared skill is an org-admin
    action, not a local curation decision.
    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.
    NFT)r   r   r   r   r   r   r   )r   r   	local_dirs      r   is_curation_eligibler     s     "8"D"DuJ'' u
## u* )&(((
++I))4444
++7u4r    c                    t          | t                    sdS |                     d          dk    p|                     d          du S )u  Return True when a usage record opts a skill into curator management.

    NAMING (issue #67140): the on-disk field is ``created_by``, which reads
    like provenance but is consumed as a **curator-management opt-in policy
    flag**. The two are not the same question:

    * provenance = "who authored this file" — historical fact, and for records
      written before the marker existed it is simply unrecoverable.
    * management = "may autonomous curation mutate/archive this" — a policy
      decision the user can change at any time via ``hermes curator adopt``.

    ``created_by: "agent"`` therefore means "curator-managed", NOT "proof the
    agent wrote it". The field name is retained because it is already on disk
    in every user's ``.usage.json``; renaming it would strand those records.
    Read it as policy, and prefer ``is_curator_managed()`` at call sites so the
    intent is unambiguous.
    F
created_byagentagent_createdT)r   r   rb   )r[   s    r   r   r     sI    $ fd## u::l##w.U&**_2M2MQU2UUr    c                ^    t          t                                          |                     S )a  Whether *skill_name* is opted into curator management.

    Policy-intent alias for the ``created_by``-marker check, so call sites read
    as the question they are actually asking (see ``_is_curator_managed_record``
    for why the stored field name says "created_by").
    )r   r   rb   r   s    r   is_curator_managedr     s$     &jll&6&6z&B&BCCCr    c                    t                      } |                                 sg S t                      }t                      }t	                      }g }|                     d          D ]}t          |          st          |          r!	 |                    |            n# t          $ r Y Dw xY wt          ||j        j                  }||v s||v st          |          r{t          |                    |                    rt!          ||          s|                    |           t%          t'          |                    S )u  Enumerate curation-ELIGIBLE skills that carry no provenance marker.

    These are skills the curator *could* manage (they are not hub-installed,
    not external, not protected built-ins) but never will, because nothing
    ever wrote ``created_by: agent`` onto their usage record. Two ways a skill
    lands here:

    * It predates the provenance mechanism entirely — records written before
      ``created_by`` existed carry no key at all, so their authorship is
      unknowable from the record alone.
    * It was created by a FOREGROUND ``skill_manage(action="create")`` call,
      which deliberately does not mark provenance (skills a user asks for
      belong to the user).

    Either way the skill is invisible to ``curated_report()`` and therefore to
    every automatic transition. ``hermes curator status`` surfaces this count
    so the blind spot is legible instead of silent, and ``hermes curator
    adopt`` lets the user hand specific skills over explicitly.

    Provenance is a DECLARATION, never an inference: this function only
    reports, and callers must not auto-adopt what it returns. Heavy patch or
    use counts are evidence of maintenance, not of authorship — the agent
    edits user-authored skills on the user's behalf routinely.
    r   r   )r$   r6   r   r   r   r   r   r   r   rW   r   r2   r}   r   r   rb   r   r   r   rr   )r   r   r   r   r{   r   r}   s          r   list_unmanaged_skill_namesr     sU   2 ==D;;== 	
#
%
%C*,,GLLEEJJz**  !(++ 	/Eh/O/O 		  &&&& 	 	 	H	8?3GHHH 3;;$'//-A$-G-G/%eiioo66 	#D(33 	T#e**s   B
B+*B+List[Dict[str, Any]]c                 4   t                      } g }t                      D ]}|                     |          }t          |t                    rt	          |          nt                      }t                                                      D ]\  }}|                    ||           d|i|}t          |t                    od|v |d<   t          |t                    |d<   t          |          |d<   t          |          |d<   |
                    |           |S )a  Rows for every skill :func:`list_unmanaged_skill_names` returns.

    Each row carries the usual activity fields plus ``has_provenance_key``:
    False when the record has no ``created_by`` key at all (pre-dates the
    mechanism), True when the key is present but unset (a foreground create
    under the current policy). The distinction matters for explaining WHY a
    skill is unmanaged; it is not a signal to adopt on.
    r}   r   has_provenance_key
has_recordlast_activity_atro   )r   r   rb   r   r   _empty_recorditems
setdefaultrh   ro   r   )r   rowsr}   rf   recr   vrows           r   unmanaged_reportr
  ;  s    LLE!#D*,, 
 
iioo+5c4+@+@Ud3iiimoo!OO))++ 	! 	!DAqNN1a    t#s#$.sD$9$9$Qlc>Q !&sD11L"4S"9"9 .s 3 3CKr    Tuple[bool, str]c                >   | sdS t          |           rdd|  dfS t          |           rdd|  dfS t          |           rdd|  dfS t          |           }|t	          |           dd|  dfS dd	|  d
fS t          |          rdt          |           fS t                      }t          |	                    |                     rdd|  dfS t          |            t          t                      	                    |                     sdd|  dfS dd|  dfS )uw  Hand *skill_name* to the curator by user declaration.

    Writes the same ``created_by: agent`` marker the background review fork
    writes, so the skill joins ``curated_report()`` and the automatic
    transition walk. The inactivity clock is NOT reset: the skill's existing
    ``last_activity_at`` still governs staleness, so adopting something idle
    for months does not buy it a fresh window (nor does it archive it on the
    spot — the state machine decides on the next pass).

    Returns (ok, message). Refuses hub-installed, external, and protected
    built-in skills, which have an owner other than the user.
    )Fzno skill name givenF'z7' is a protected built-in; the curator never manages itz(' is hub-installed; its upstream owns ituU   ' is a bundled built-in — it is governed by curator.prune_builtins, not by adoptionNz?' lives in skills.external_dirs and is read-only to the curatorr   ' not foundTz' is already curator-managedzcould not mark 'z' as curator-managedz	adopted 'z' into curator management)r   r   r   r   r   r   r   r   r   rb   mark_agent_created)r   r   r   s      r   adopt_skillr  T  s     ,++J'' ^]*]]]]]
## ON*NNNNN* 

 6
 6 6 6
 	
  
++I#J//;ijiiiii7
77777i(( >1*====LLE!%))J"7"788 BAAAAAAz"""%jll&6&6z&B&BCC JIIIIIIBZBBBBBr    c                 @    d ddd d dd t                      t          dd dS )Nr   F)r   rk   rl   r_   r`   rm   ra   
created_atstatepinnedarchived_at)rP   STATE_ACTIVEr#   r    r   r  r    s6    jj  r    Dict[str, Dict[str, Any]]c                    t                      } |                                 si S 	 t          j        |                     d                    }nA# t
          t          j        f$ r(}t                              d| |           i cY d}~S d}~ww xY wt          |t                    si S i }|                                D ],\  }}t          |t                    r||t          |          <   -|S )zGRead the entire .usage.json map. Returns empty dict on missing/corrupt.r-   r.   zFailed to read %s: %sN)r'   r6   r   r   rs   rB   r   rx   ry   r   r   r  r   )r   r   r~   cleanr   r  s         r   r   r     s    ==D;;== 	z$..'.::;;T)*   ,dA666						 dD!! 	')E

  1a 	E#a&&MLs   (A B%BBBr   c                   t                      }	 |j                            dd           t          j        t          |j                  dd          \  }}	 t          j        |dd          5 }t          j	        | |d	dd
           |
                                 t          j        |                                           ddd           n# 1 swxY w Y   t          j        ||           dS # t          $ r( 	 t          j        |           n# t           $ r Y nw xY w w xY w# t"          $ r)}t$                              d||d           Y d}~dS d}~ww xY w)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'   r2   r3   r   r   r   r   r   r   dumpr   r   r?   rX   r   r   rB   r   rx   ry   )r   r   rG   tmp_pathr   r~   s         r   
save_usager!    s   ==DG$666'DK  6
 
 
H	2sW555 %	$!t%PPPP			$$$% % % % % % % % % % % % % % % Jx&&&&& 	 	 		(####   	  G G G-tQFFFFFFFFFGss   AD' C2 0ACC2 CC2 CC2 2
D$=DD$
DD$DD$$D' '
E1EEc                   t                      }|                    |           }t          |t                    st	                      S t	                      }|                                D ]\  }}|                    ||           |S )zDReturn the record for *skill_name*, creating a fresh one if missing.)r   rb   r   r   r  r  r  )r   r   r  r   r   r  s         r   
get_recordr#    sx    <<D
((:

Cc4   ??D

  1q!Jr    c                   | rt          |           sdS 	 t                      5  t                      }t          |                    |           t
                    r	 ddd           dS t                      || <   t          |           ddd           dS # 1 swxY w Y   dS # t          $ r)}t          
                    d| |d           Y d}~dS 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   rH   r   r   rb   r   r  r!  r   rx   ry   r   r   r~   s      r   seed_record_if_missingr&    s[     1*== h 	 	<<D$((:..55 	 	 	 	 	 	 	 	  -Dt	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	  h h hH*VWbfggggggggghsF   B" 8BB" ( BB" BB" BB" "
C,CCFrequire_curation_eligibler(  c                  | sdS 	 |rt          |           sdS t                      5  t                      }|                    |           }t	          |t
                    st                      } ||           ||| <   t          |           ddd           dS # 1 swxY w Y   dS # t          $ r)}t          
                    d| |d           Y d}~dS 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   rH   r   rb   r   r   r  r!  r   rx   ry   )r   mutatorr(  r   r  r~   s         r   _mutater+    sQ     Y$ 	-A*-M-M 	F 	 	<<D((:&&Cc4(( &#ooGCLLL"Dt	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	 	  Y Y Y9:qSWXXXXXXXXXYs@   B' B' A&BB' BB' !B"B' '
C1CCc                .    dd}t          | |           dS )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.
    r  r\   r   r   c                |    t          |                     d          pd          dz   | d<   t                      | d<   d S )Nrl   r   r0   r`   ri   rb   rP   r  s    r   _applyzbump_view.<locals>._apply	  s?     5 5 :;;a?L (

r    Nr  r\   r   r   r+  r   r0  s     r   	bump_viewr4    s.    + + + + Jr    c                .    dd}t          | |           dS )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.
    r  r\   r   r   c                |    t          |                     d          pd          dz   | d<   t                      | d<   d S )Nrk   r   r0   r_   r.  r/  s    r   r0  zbump_use.<locals>._apply  s>    sww{338q99A=K&jjNr    Nr1  r2  r3  s     r   bump_user7    s.    ) ) ) ) Jr    c                .    dd}t          | |           dS )zBump patch_count and last_patched_at. Called from skill_manage (patch/edit).

    Tracks every skill regardless of provenance.
    r  r\   r   r   c                |    t          |                     d          pd          dz   | d<   t                      | d<   d S )Nrm   r   r0   ra   r.  r/  s    r   r0  zbump_patch.<locals>._apply   s?     !7!7!<1==AM!)r    Nr1  r2  r3  s     r   
bump_patchr:    s.    
, , , , Jr    c                2    d	d}t          | |d           dS )
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.
    r  r\   r   r   c                    d| d<   d S )Nr   r   r#   r/  s    r   r0  z"mark_agent_created.<locals>._apply,  s    #Lr    Tr'  Nr1  r2  r3  s     r   r  r  &  s1    $ $ $ $J$??????r    r  c                    t           vrt                              d|            dS d
fd}t          | |d	           dS )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 %sNr  r\   r   r   c                r    | d<   t           k    rt                      | d<   d S t          k    rd | d<   d S d S )Nr  r  )STATE_ARCHIVEDrP   r  )r  r  s    r   r0  zset_state.<locals>._apply7  sN    GN""!)Cl""!%C #"r    Tr'  r1  )_VALID_STATESrx   ry   r+  )r   r  r0  s    ` r   	set_staterA  1  sg     M!!95*MMM& & & & & & J$??????r    r  c                8    dfd}t          | |d           d S )	Nr  r\   r   r   c                ,    t                    | d<   d S )Nr  r   )r  r  s    r   r0  zset_pinned.<locals>._applyA  s    VHr    Tr'  r1  r2  )r   r  r0  s    ` r   
set_pinnedrE  @  s<    % % % % % %J$??????r    syncc                8    d	fd}t          | |d           dS )
a  Set the sync opt-in flag on a skill's usage record.

    Sync is OPT-IN: nothing propagates to the sync plane unless the user marks
    a skill with ``sync: true`` here. Sits alongside ``pinned``/``created_by``
    on the ``.usage.json`` sidecar and is read by
    ``tools.skills_sync_client.list_synced_skill_names``. Gated on curation
    eligibility so bundled/hub/external skills (which never sync) can't be
    marked. Provisional per the M1-D default.
    r  r\   r   r   c                ,    t                    | d<   d S )NrF  rD  )r  rF  s    r   r0  zset_sync.<locals>._applyP  s    4jjFr    Tr'  Nr1  r2  )r   rF  r0  s    ` r   set_syncrI  F  s<    ! ! ! ! ! !J$??????r    c                J    t          |                               d          du S )zBWhether a skill is opted into sync (``sync: true`` in its record).rF  T)r#  rb   r   s    r   is_sync_enabledrK  U  s#    j!!%%f--55r    c                   | sdS 	 t                      5  t                      }| |v r|| = t          |           ddd           dS # 1 swxY w Y   dS # t          $ r)}t                              d| |d           Y d}~dS 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   )rH   r   r!  r   rx   ry   r%  s      r   forgetrM  Z  s    X 	! 	!<<DT!!$4   		! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	! 	!
  X X X8*aRVWWWWWWWWWXs9   A %AA A

A A
A 
BBBc                   t          |           }| t          |           dt          |           fS t          | |          s6t	          |           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                      }	 |                    dd	           n# t          $ r}dd
| fcY d}~S d}~ww xY w||j
        z  }|                                r>||j
         dt          j        t          j                                      d           z  }	 |                    |           nf# t          $ rY ddl}	 |                    t)          |          t)          |                     n # t*          $ r}dd| fcY d}~cY S d}~ww xY wY nw xY wt-          |           rt/          |            t1          | t2                     dd| fS )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 itr  Tr)   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   rJ   r3   rB   r}   r6   r   rM   r   rN   strftimerenameshutilmover   r   r   r   rA  r?  )r   local_skill_dirr   r   r~   destrR  e2s           r   archive_skillrW  l  s    &j11O#;J#G#G#S1*====
O<< 

++ 	H* H H H  J'' 	RQJQQQQQ9j 9 9 9
 	

  I7
77777i(( >1*====>>L;4$7777 ; ; ;:q:::::::::;
 ).(D{{}} hgg(,x|2L2L2U2UVd2e2eggg5 5 5 5	5KKID		2222 	5 	5 	5444444444444	5 32	5 * (J'''j.)))&&&&&s`   3C 
C&C!C&!C&E F?+0FF?
F9&F4,F9-F?4F99F?>F?c                B    t                     rdd  dfS t                     rt                      sdd  dfS t                      }|                                sdS  fd|                    d          D             }|s5  dt          fd	|                    d          D             d
          }|sdd  dfS |d         }t                       z  }|                                rdd| fS 	 |                    |           nf# t          $ rY ddl
}	 |                    t          |          t          |                     n # t          $ r}dd| fcY d}~cY S d}~ww xY wY nw xY wt                      t           t                      d
d| fS )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 directoryc                R    g | ]#}|                                 |j        k    !|$S r#   r   )r   r   r   s     r   
<listcomp>z!restore_skill.<locals>.<listcomp>  s3    \\\

\qvQ[G[G[!G[G[G[r    *rO  c                   g | ]}|                                 rr|j                                      rXt          |j                  t                    z
  d k    X|j        t                    d                                         |S )   N)r   r}   r   lenisdigit)r   r   r   s     r   rZ  z!restore_skill.<locals>.<listcomp>  s       88:: F%%f-- KK#f++-33F3v;;<<(0022 4  433r    T)reversez' not found in archiver   zdestination already exists: Nzfailed to restore: zrestored to )r   r   r   rJ   r6   r   r   r$   rQ  rB   rR  rS  r   r   r   rA  r  )r   r   
candidatessrcrU  rR  r~   r   s   `      @r   restore_skillrc    sw    
## 
8j 8 8 8
 	
 * 
&=&?&? 
8j 8 8 8
 	
  >>L   -,,
 ]\\\\//44\\\J 
 !!!   '--c22   	
 	
 	

  CB
BBBBB
Q-C==:%D{{}} <;T;;;;4

4 4 4 4	4KKC#d)),,,, 	4 	4 	4333333333333	4 -,4 :&&&j,'''&&&&&sB   :D E30EE3
E-E( E-!E3(E--E32E3c                    t                      }|                                sdS ddlm}  ||d          D ]:}t	          |          rt          ||j        j                  | k    r	|j        c S ;dS )aW  Locate 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. Uses the gated
    index iterator so M2 org mirrors resolve ONLY for the active org
    (stale ``_org/<other>/`` trees never match).
    Nr   )iter_skill_index_filesr   r   )r$   r6   agent.skill_utilsre  r   r   r2   r}   )r   r   re  r   s       r   r   r     s     ==D;;== t888888**4<< # #!(++ 	Hx/CDDD
RR?""" S4r    c                
   ddl m}  |            dd         D ]i}|                                s|                    d          D ]<}t	          |          rt          ||j        j                  | k    r|j        c c S =jdS )zBLocate a skill under configured external dirs by frontmatter name.r   )get_all_skills_dirsr0   Nr   r   )rf  rh  r6   r   r   r   r2   r}   )r   rh  r   r   s       r   r   r   	  s    555555##%%abb) ' '{{}} 	

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

    ``provenance`` is 'agent', 'bundled', or 'hub' (see :func:`provenance`).
    Bundled skills are only included when ``curator.prune_builtins`` is enabled.
    Hub-installed skills are never included.

    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.
    r}   
_persistedr  ro   
provenance)r   r   rb   r   r   r  r  r  rh   ro   rk  r   )
r   r  r}   rf   	persistedr  r   r   r  r	  s
             r   curated_reportrm    s    <<D!#D.00  hhtnnsD))	%/T%:%:OccJJLL 	! 	!DAqNN1a    t<s<L)<<"4S"9"9 .s 3 3&t,,LCKr    c                     t                      S )uW  DEPRECATED — use :func:`curated_report` instead.

    Used to return everything :func:`curated_report` returns (including bundled
    skills when ``curator.prune_builtins`` is enabled), which made the
    "agent-created" name misleading. Kept as a compatibility alias for
    external callers; new code should call ``curated_report()``.
    )rm  r#   r    r   agent_created_reportro  ;  s     r    c                J    t          |           rdS t          |           rdS dS )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   rk  rk  F  s3     
## u* y7r    c                 $   t                      } |                                 sg S t                      }g }t                      }|                     d          D ]&}t          |          rt          ||j        j                  }||v r3|	                    |           |
                    |          }t          |t                    }t          |t                    r|nt                      }t                      }	|	                                D ]\  }
}|                    |
|           d|i|t!          |          |d}t#          |          |d<   t%          |          |d<   |                    |           (t)          |d           S )	u  Return usage telemetry for EVERY skill on disk, with provenance.

    Unlike ``curated_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   r}   )rk  rj  r  ro   c                    | d         S )Nr}   r#   )rs    r   <lambda>zusage_report.<locals>.<lambda>y  s
    ai r    )re   )r$   r6   r   rr   r   r   r   r2   r}   rw   rb   r   r   r  r  r  rk  rh   ro   r   r   )r   r   r  seenr   r}   rf   rl  r  base_recr   r  r	  s                r   usage_reportrw  S  s    ==D;;== 	<<D!#DDJJz**  !(++ 	8?3GHHH4<<hhtnnsD))	%/T%:%:Occ ??NN$$ 	! 	!DAqNN1a    D

 %T**#	
 
 
 #5S"9"9 .s 3 3C$//0000r    )r   r   r   r   )r   r   )r   r   )rQ   r   r   rR   )r[   r\   r   r]   )r[   r\   r   ri   )r   r   )r   r   )r{   r   r   r   )r   r   r   r   )r   r   )r   r   r   r   r   r   )r   r   r   r   rL   )r   r   r   r   r   r   )r[   r   r   r   )r   r   )r   r   r   r  )r   r\   )r   r  )r   r  r   r   )r   r   r   r\   )r   r   r(  r   r   r   )r   r   r  r   r   r   )r   r   r  r   r   r   )r   r   rF  r   r   r   )r   r   r   r   )Z__doc__
__future__r   r   loggingr   r   
contextlibr   r   r   pathlibr   typingr   r	   r
   r   r   r   hermes_constantsr   rf  r   r   	getLogger__name__rx   r5   r4   ImportErrorr  STATE_STALEr?  r@  r   __annotations__r   r$   r'   rH   rJ   rP   rZ   rh   ro   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  r7  r:  r  rA  rE  rI  rK  rM  rW  rc  r   r   rm  ro  rk  rw  r#   r    r   <module>r     s(    0 # " " " " "   				  % % % % % % ' ' ' ' ' ' ' '       8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 8 , , , , , , L L L L L L L L		8	$	$ 
LLLL   E   	 {N; &     
2 2 2 2( ( ( () ) ) )      F& & & &2 2 2 2
 
 
 
   (      .+ + + +\   *1 1 1 1   *W W W W,' ' ' '' ' ' '4 4 4 4n
J 
J 
J 
J   *   5 5 5 5
8 8 8 8
       <V V V V.D D D D2 2 2 2j   2)C )C )C )C`       (G G G G0
 
 
 
h h h h, LQ Y Y Y Y Y Y@	  	  	  	 	  	  	  	        @ @ @ @@ @ @ @@ @ @ @@ @ @ @6 6 6 6
X X X X$:' :' :' :'zH' H' H' H'V   *   &   >   
 
 
 
&1 &1 &1 &1 &1 &1s6   A$ $A?,A10A?1A96A?8A99A?>A?