
    (Gjd                        U d Z ddlZddlZddlZddlZddlZddlZddlZddl	m
Z
 ddlmZmZmZmZmZ ddlmZmZ ddlmZmZ ddlmZ  ej        e          Z ej        d e            	          Zd
ed<   de
ddfdZ de
de!fdZ"dkdZ#	 ddl$m%Z%m&Z&m'Z' dZ(n# e)$ r dZ(Y nw xY wde!fdZ*de
dee+         fdZ,ddl-Z- e            Z.e.dz  Z/dZ0dZ1de
de
fdZ2de
de!fdZ3de
dee+         fdZ4de+dee+         fdZ5de+de
d e+deee+ef                  fd!Z6de+d"e
d e+d#e+deee+ef                  f
d$Z7d e+de+deee+ef                  fd%Z8de+d&ee+         deee+ef                  fd'Z9d(Z:d)Z; ej<        d*          Z=h d+Z>de+dee+         fd,Z?d-ee+         dee+         fd.Z@d/e+dee+         fd0ZAdld/e+d2e+dee+         fd3ZBdmde+d-e+de
fd4ZCde+deee+ef                  fd5ZDde+deee+e
f                  fd6ZEdnde+d8e+de+fd9ZFd:e+dee+         fd;ZGde
d:e+deee
         ee+         f         fd<ZHdod:e
d/e+d>e+ddfd?ZIdmde+d/e+d-e+dee+ef         fd@ZJde+d/e+dee+ef         fdAZK	 	 dpde+dBe+dCe+d:e+dDe!dee+ef         fdEZLdmde+d&ee+         dee+ef         fdFZMde+d:e+dGe+dee+ef         fdHZNde+d:e+dee+ef         fdIZOddlZ ej        dJd	          ZPdKedL<   dM ZQdNee+ef         de+fdOZR	 	 	 	 	 	 	 	 dqd e+de+d/e+d-e+d:e+dGe+dBe+dCe+dDe!d&e+de+fdPZSdQdR e             dSdTdUg dVdWdXdUdYdZdUd[dZdUd\dZdUd]dZd^d_dZdUd`dZdUdadZdUdbdZdUdcdZdd
d dgdedfZTddglUmVZVmWZW  eVjX        dQdeTdh dij           dS )ru  
Skill Manager Tool -- Agent-Managed Skill Creation & Editing

Allows the agent to create, update, and delete skills, turning successful
approaches into reusable procedural knowledge. New skills are created in
~/.hermes/skills/. Existing skills (bundled, hub-installed, or user-created)
can be modified or deleted wherever they live.

Skills are the agent's procedural memory: they capture *how to do a specific
type of task* based on proven experience. General memory (MEMORY.md, USER.md) is
broad and declarative. Skills are narrow and actionable.

Actions:
  create     -- Create a new skill (SKILL.md + directory structure)
  edit       -- Replace the SKILL.md content of a user skill (full rewrite)
  patch      -- Targeted find-and-replace within SKILL.md or any supporting file
  delete     -- Remove a user skill entirely
  write_file -- Add/overwrite a supporting file (reference, template, script, asset)
  remove_file-- Remove a supporting file from a user skill

Directory layout for user skills:
    ~/.hermes/skills/
    ├── my-skill/
    │   ├── SKILL.md
    │   ├── references/
    │   ├── templates/
    │   ├── scripts/
    │   └── assets/
    └── category-name/
        └── another-skill/
            └── SKILL.md
    N)Path)AnyDictListOptionalTuple)get_hermes_homedisplay_hermes_home)atomic_replaceis_truthy_value)cfg_getbackground_review_read_pathsdefaultz#_ctxvars.ContextVar[frozenset[str]]_background_review_read_pathspathreturnc                    	 ddl m}  |            sdS n# t          $ r Y dS w xY w	 t          |                                           }n# t          $ r t          |           }Y nw xY wt          t                                                    }|                    |           t                              t          |                     dS )a  Record that the active background-review fork has read a skill file.

    The autonomous review fork is allowed to evolve skills, but it must not
    patch or rewrite content it has only inferred from the transcript.  The
    skill_view tool calls this after returning file content to the model; write
    paths below require the corresponding target path to be present when the
    current origin is ``background_review``.
    r   is_background_reviewN)
tools.skill_provenancer   	Exceptionstrresolvesetr   getadd	frozenset)r   r   resolvedcurrents       >/home/rurouni/.hermes/hermes-agent/tools/skill_manager_tool.py!mark_background_review_skill_readr"   8   s    ??????##%% 	F	   t||~~&&   t99/335566GKK!%%i&8&899999s    
##!A	 	A%$A%c                     	 t          |                                           }n# t          $ r t          |           }Y nw xY w|t                                          v S N)r   r   r   r   r   )r   r   s     r!   _background_review_has_readr%   Q   s`    t||~~&&   t99488::::s   !$ A A c                  R    t                               t                                 dS )zCTest helper: clear read-before-write marks for the current context.N)r   r   r        r!   #_reset_background_review_read_marksr)   Y   s     !%%ikk22222r(   )
scan_skillshould_allow_installformat_scan_reportTFc                      	 ddl m}   |             }t          t          |dd          d          S # t          $ r Y dS w xY w)a[  Read skills.guard_agent_created from config (default False).

    Off by default because the agent can already execute the same code
    paths via terminal() with no gate, so the scan adds friction without
    meaningful security.  Users who want belt-and-suspenders can turn it
    on via `hermes config set skills.guard_agent_created true`.
    r   )load_configskillsguard_agent_createdFr   )hermes_cli.configr.   r   r   r   )r.   cfgs     r!   _guard_agent_created_enabledr3   f   sr    111111kmmC#899
 
 
 	
    uus   /2 
A A 	skill_dirc                    t           sdS t                      sdS 	 t          | d          }t          |          \  }}|du rt	          |          }d| d| S |2t	          |          }t
                              d|           d| d| S n5# t          $ r(}t
                              d| |d	
           Y d}~nd}~ww xY wdS )zScan a skill directory after write. Returns error string if blocked, else None.

    No-op when skills.guard_agent_created is disabled (the default).
    Nzagent-created)sourceFz"Security scan blocked this skill (z):
z4Agent-created skill blocked (dangerous findings): %szSecurity scan failed for %s: %sTexc_info)_GUARD_AVAILABLEr3   r*   r+   r,   loggerwarningr   )r4   resultallowedreasonreportes         r!   _security_scan_skillrA   y   s   
  t')) tWIo>>>.v66e'//FLLLFLLL? (//FNNQSYZZZLLLFLLL   W W W8)QQUVVVVVVVVW4s   =B 3B 
C B;;C r/   @   i   
skill_pathc                 
   ddl m} 	 |                                 }n# t          $ r | }Y nw xY w |            D ]D}	 |                    |                                           |c S # t
          t          f$ r Y Aw xY wt          S )u   Return the skills root directory (local or external_dirs entry) that
    contains ``skill_path``.  Falls back to the local ``SKILLS_DIR`` if no
    match is found (defensive — callers should have located the skill via
    ``_find_skill`` first).
    r   get_all_skills_dirs)agent.skill_utilsrF   r   OSErrorrelative_to
ValueError
SKILLS_DIR)rC   rF   r   roots       r!   _containing_skills_rootrM      s     655555%%''    $#%%  	  000KKKG$ 	 	 	H	s    ,,(A''A;:A;c                     	 |                                  p#t          | d          o|                                 S # t          $ r Y dS w xY w)a  True when ``path`` is a symlink or (on Windows) a directory junction.

    Either form lets a poisoned skills tree redirect a subsequent
    ``shutil.rmtree`` to content outside the skills root. ``is_junction``
    only exists on Python 3.12+ Windows; gate with ``hasattr``.
    is_junctionF)
is_symlinkhasattrrO   rH   )r   s    r!   _is_path_redirectrR      sY      YWT=%A%A%XdFVFVFXFXY   uus   7: 
AAc                    ddl m} t          |           rd|  dS 	 |                                 }n # t          $ r}d|  d| dcY d}~S d}~ww xY wg } |            D ]:}	 |                    |                                           +# t          $ r Y 7w xY w|D ]A}||k    rd|  dc S 	 |                    |          }n# t          $ r Y 3w xY w|j        r dS Bd|  d	S )
a  Last-line guard before ``shutil.rmtree(skill_dir)`` in ``_delete_skill``.

    ``_find_skill`` already restricts ``skill_dir`` to a real ``SKILL.md``
    parent discovered by walking the skills roots, so the agent cannot inject
    an arbitrary path the way Kilo Code's HTTP endpoint could (their issue
    #11227: a built-in-skill sentinel resolved to the server cwd and a
    recursive delete wiped the user's entire working directory). This is the
    matching defense-in-depth for our agent-facing ``skill_manage`` delete
    path: even if discovery or a poisoned tree hands us a bad directory, never
    recursively delete

      1. a path that is not strictly *inside* one of the known skills roots,
      2. a skills root itself (would wipe every installed skill), or
      3. a directory reached via a symlink / junction (``rmtree`` would follow
         it into content outside the skills tree).

    Returns an error string to refuse on, or ``None`` when the delete is safe.
    r   rE   zRefusing to delete 'zZ': the skill directory is a symlink/junction. Remove the link target manually if intended.z': could not resolve path ().NzP': resolves to the skills root itself, which would remove every installed skill.z6': path does not resolve inside any known skills root.)	rG   rF   rR   r   rH   appendrI   rJ   parts)r4   rF   r   excrootsrL   rels          r!   _validate_delete_targetrZ      s   & 655555 ## 
N9 N N N	

T$$&& T T TSiSSCSSSSSSSSST E##%%  	LL(((( 	 	 	H	   tEy E E E  
	&&t,,CC 	 	 	H	9 	44		y 	 	 	s>   2 
AA
A
A!'B		
BB,C
CCnamec                     	 ddl m} |                    |           }|                    d          r	d|  d|  dS n-# t          $ r  t
                              d| d	           Y nw xY wd
S )u  Return a refusal message if *name* is pinned, else None.

    Pin protects a skill from **deletion** — both the curator's auto-archive
    passes and the agent's ``skill_manage(action="delete")`` tool call. The
    agent can still patch/edit pinned skills; pin only guards against
    irrecoverable loss, not against content evolution.

    Best-effort: if the sidecar is unreadable we let the delete through
    rather than block on a broken telemetry file.
    r   skill_usagepinnedSkill 'z]' is pinned and cannot be deleted by skill_manage. Ask the user to run `hermes curator unpin zf` if they want to delete it. Patches and edits are allowed on pinned skills; only deletion is blocked.z!pinned-guard lookup failed for %sTr7   N)toolsr^   
get_recordr   r   r:   debug)r[   r^   recs      r!   _pinned_guardre      s    O%%%%%%$$T**778 	($ ( ()-( ( (	  O O O8$NNNNNO4s   8< 'A&%A&actionc                    	 ddl m}  |            sdS n# t          $ r Y dS w xY w	 ddlm} |                    |                               d          rdd| d|  d	|  d
dS n-# t          $ r  t                              d| d           Y nw xY w	 ddl	m
}  ||          rdd| d|  ddS n-# t          $ r  t                              d| d           Y nw xY w	 ddlm} |                    |           rdd| d|  ddS |                    |           rdd| d|  ddS |                    |           rdd| d|  ddS n-# t          $ r  t                              d| d           Y nw xY wdS )aN  Refuse autonomous curator writes to externally owned skills.

    Foreground agents may still perform user-directed edits to external,
    bundled, or hub-installed skills. The background review fork is different:
    it is autonomous lifecycle maintenance, so its write surface is restricted
    to local curator-owned sediment.
    r   r   Nr]   r_   FRefusing background curator z for pinned skill 'ze': pinned skills are off-limits to autonomous maintenance. Ask the user to run `hermes curator unpin z` if they want it changed.successerrorz'pinned skill guard lookup failed for %sTr7   )is_external_skill_path for skill 'zl': the skill lives in skills.external_dirs, which are externally owned and read-only to autonomous curation.z)external skill guard lookup failed for %sz for protected built-in skill ''.z for hub-installed skill 'z for bundled skill 'z&owned skill guard lookup failed for %s)r   r   r   ra   r^   rb   r   r:   rc   rG   rl   is_protected_builtinis_hub_installed
is_bundled)r[   r4   rf   r   r^   rl   s         r!   _background_review_write_guardrr     s   ??????##%% 	4	   ttU%%%%%%!!$''++H55 		 N6 N NN N .2N N N  		  U U U>tTTTTTUW<<<<<<!!),, 	 M6 M Mt M M M  	  W W W@$QUVVVVVWT%%%%%%++D11 	 06 0 0'+0 0 0   ''-- 	 '6 ' '"' ' '   !!$'' 	 '6 ' '"' ' '  	  T T T=tdSSSSST4sM    
##<A% %'BBB1 1'CC&E	  E	 ' E	 	'E32E3target
file_labelc                     	 ddl m}  |            sdS n# t          $ r Y dS w xY wt          |          rdS dd| d|  d| dd	d
S )zARequire review forks to load the exact target before mutating it.r   r   NFrh   rm   z': the current z content has not been loaded in this review turn. Call skill_view(name) for SKILL.md, or skill_view(name, file_path=...) for a supporting file, then retry the write using the content just returned.T)rj   rk   _read_before_write_required)r   r   r   r%   )r[   rs   rf   rt   r   s        r!   *_background_review_read_before_write_guardrw   n  s    ??????##%% 	4	   tt #6** t ?6 ? ?t ? ?%? ? ? (,
 
 
    
##c                 b    | dvrd S t          |          }|sd S t          ||d         |           S )N>   editpatchdelete
write_fileremove_filer   )_find_skillrr   )rf   r[   existings      r!   _background_review_preflightr     sD    MMMt4  H t)$0@&IIIr(   absorbed_intoc                     	 ddl m}  |            sdS n# t          $ r Y dS w xY wt          |t                    o|                                }|rdS dd|  d                    |           dd	S )
u  Fail closed on unverified deletes during the curator consolidation pass.

    The curator's forked review agent (``is_background_review()``) runs the
    LLM umbrella-building pass. Its only legitimate ``skill_manage(delete)`` is
    a *verified consolidation*: the skill's content was absorbed into an
    umbrella, declared via ``absorbed_into=<umbrella>`` where the umbrella
    exists on disk (validated separately in ``_delete_skill``).

    A delete with no forwarding target — ``absorbed_into`` omitted (``None``)
    or empty (``""``) — is the fail-open behavior reported in #29912: the
    consolidation pass archived whole clusters of active skills with zero
    verified consolidations (``consolidated_this_run == 0``), leaving active
    automations pointing at names that no longer resolve. The deterministic
    inactivity prune is the only legitimate prune path, and it archives via
    ``skill_usage.archive_skill()`` directly without ever calling
    ``skill_manage`` — so a bare prune reaching here can only be the LLM pass
    pruning without consolidation evidence. Refuse it; keep the skill active.

    Returns an error dict to abort the delete, or ``None`` when the delete is
    allowed to proceed (not the curator pass, or a declared consolidation).
    r   r   NFz-Refusing background curator delete of skill 'u_  ': the consolidation pass may only archive a skill it has absorbed into an umbrella. Pass absorbed_into=<umbrella> (the umbrella must already exist) to record a verified consolidation. Pruning a skill with no forwarding target is not permitted here — the deterministic inactivity prune handles staleness archival separately. Keeping '{name}' active.r[   T)rj   rk   _fail_closed)r   r   r   
isinstancer   stripformat)r[   r   r   declareds       r!   #_curator_consolidation_delete_guardr     s    0??????##%% 	4	   tt ---G-2E2E2G2GH t 3D 3 3 3 4:6t63D3D  rx   i i   z^[a-z0-9][a-z0-9._-]*$>   assetsscripts	templates
referencesc                     | sdS t          |           t          k    rdt           dS t                              |           sd|  dS dS )z>Validate a skill name. Returns error message or None if valid.zSkill name is required.zSkill name exceeds  characters.zInvalid skill name 'ze'. Use lowercase letters, numbers, hyphens, dots, and underscores. Must start with a letter or digit.N)lenMAX_NAME_LENGTHVALID_NAME_REmatchr   s    r!   _validate_namer     sn     )((
4yy?""B_BBBBt$$ 
R4 R R R	
 4r(   categoryc                    | dS t          | t                    sdS |                                 } | sdS d| v sd| v rd|  dS t          |           t          k    rdt           dS t
                              |           sd|  dS dS )	zFValidate an optional category name used as a single directory segment.NzCategory must be a string./\zInvalid category 'zn'. Use lowercase letters, numbers, hyphens, dots, and underscores. Categories must be a single directory name.zCategory exceeds r   )r   r   r   r   r   r   r   )r   s    r!   _validate_categoryr     s    th$$ ,++~~H t
h$(**Z Z Z Z	
 8}}&&@?@@@@x(( 
Z Z Z Z	
 4r(   contentc                 j   |                                  sdS |                     d          sdS t          j        d| dd                   }|sdS | d|                                dz            }	 t          j        |          }n!# t
          j        $ r}d| cY d}~S d}~ww xY wt          |t                    sd	S d
|vrdS d|vrdS t          t          |d                             t          k    rdt           dS | |                                dz   d                                          }|sdS dS )z
    Validate that SKILL.md content has proper frontmatter with required fields.
    Returns error message or None if valid.
    zContent cannot be empty.z---zPSKILL.md must start with YAML frontmatter (---). See existing skills for format.
\n---\s*\n   NzISKILL.md frontmatter is not closed. Ensure you have a closing '---' line.zYAML frontmatter parse error: z6Frontmatter must be a YAML mapping (key: value pairs).r[   z&Frontmatter must include 'name' field.descriptionz-Frontmatter must include 'description' field.zDescription exceeds r   zRSKILL.md must have content after the frontmatter (instructions, procedures, etc.).)r   
startswithresearchstartyaml	safe_load	YAMLErrorr   dictr   r   MAX_DESCRIPTION_LENGTHend)r   	end_matchyaml_contentparsedr@   bodys         r!   _validate_frontmatterr     s~   
 ==?? *))e$$ baa	-55I [ZZ1Y__..223L4--> 4 4 43333333334 fd## HGGV77F"">>
3vm$%%&&)???J&<JJJJ9==??Q&''(..00D dcc4s   /B B"BB"B"SKILL.mdlabelc                 t    t          |           t          k    r| dt          |           ddt          ddS dS )zCheck that content doesn't exceed the character limit for agent writes.

    Returns an error message or None if within bounds.
    z content is ,z characters (limit: za). Consider splitting into a smaller SKILL.md with supporting files in references/ or templates/.N)r   MAX_SKILL_CONTENT_CHARS)r   r   s     r!   _validate_content_sizer   #  sY    
 7||--- - -#g,,> - -.5- - -	
 4r(   c                 4    |rt           |z  | z  S t           | z  S )zFBuild the directory path for a new skill, optionally under a category.)rK   )r[   r   s     r!   _resolve_skill_dirr   2  s%     ,H$t++r(   c                     ddl m}m}  |            D ]X}|                                s|                    d          D ]+} ||          r|j        j        | k    rd|j        ic c S ,YdS )z
    Find a skill by name across all skill directories.

    Searches the local skills dir (~/.hermes/skills/) first, then any
    external dirs configured via skills.external_dirs.  Returns
    {"path": Path} or None.
    r   )rF   is_excluded_skill_pathr   r   N)rG   rF   r   existsrglobparentr[   )r[   rF   r   
skills_dirskill_mds        r!   r   r   9  s     NMMMMMMM))++ 1 1
  "" 	"((44 	1 	1H%%h// #t++000000 ,	1
 4r(   c                    g }	 ddl m} ddlm} n# t          $ r |cY S w xY w	  |            }n# t          $ r |cY S w xY wt
                                          rt
                                          nt
          }g }|dz  }	 |                                |k    r|                    d|f           n# t          t          f$ r Y nw xY w|dz  }|                                r	 |                                D ]j}	|	                                s|	dz  }
	 |
                                |k    r6n# t          t          f$ r Y Jw xY w|                    |	j        |
f           kn# t          $ r Y nw xY w|D ]}\  }}|                                s	 |                    d          D ]<} ||          r|j        j        | k    r|                    ||j        f            n=n# t          $ r Y zw xY w|S )u  Look for ``name`` under SKILL.md across OTHER Hermes profiles.

    Returns a list of ``(profile_name, skill_dir)`` pairs. Used to make
    the "Skill X not found" error explain when the user is editing the
    wrong profile. Empty list when no other profile has the skill (or
    when profile discovery fails — fail-quiet, the caller falls back to
    the plain "not found" error).
    r   )get_default_hermes_root)r   r/   r   profilesr   )hermes_constantsr   rG   r   r   rK   r   r   rU   rH   RuntimeErroris_diriterdirr[   r   r   )r[   matchesr   r   rL   
active_dir
candidatesdefault_skillsprofiles_rootentrypskillsprofile_namer   r   s                 r!   _find_skill_in_other_profilesr   M  s    ')G<<<<<<<<<<<<<   &&((   
 *4):):)<)<L##%%%*J)+J H_N!!##z11y.9:::\"    :%M 	&..00 	9 	9||~~ (*((J66  7.   H!!5:w"78888	9  	 	 	D	 %/   j  "" 		&,,Z88  ))(33 ?'4//NNL(/#BCCCE 0  	 	 	H	Nss      
/ >>/B2 2CC#0E# D.,E# .E?E# E E# #
E0/E0AG##
G0/G0 suffixc                     ddl m}  |            }d|  d| d}t          |           }|rWt          |          dk    r|d         \  }}|d| d| d	| d
z  }n.d                    d |D                       }|d| dz  }n|dz  }|r||z  }|S )a  Build a "skill not found" error that names other profiles holding
    the same skill, so the agent can recognize a profile-scoping mistake.

    ``suffix`` is appended after the cross-profile hint if present
    (e.g. ``" Create it first with action='create'."``).
    r   )_resolve_active_profile_namer`   z' not found in active profile 'rn      z) A skill by that name exists in profile '' (zC). To edit a skill in another profile, switch profiles (`hermes -p zB`) or operate via explicit file tools with ``cross_profile=True``., c              3   (   K   | ]\  }}d | d V  dS )'Nr'   ).0p_s      r!   	<genexpr>z)_skill_not_found_error.<locals>.<genexpr>  s.      ::41ah!hhh::::::r(   z. Skills by that name exist in other profiles: zu. Switch profiles (`hermes -p <name>`) to edit there, or operate via explicit file tools with ``cross_profile=True``.z+ Use skills_list() to see available skills.)agent.file_safetyr   r   r   join)	r[   r   r   activebaseothersother_profile
other_pathnamess	            r!   _skill_not_found_errorr     s    ?>>>>>))++FDTDD&DDDD*400F >v;;!(.q	%M:0!0 0&00 0 !0 0 0DD II::6:::::EP P P PDD 	== Kr(   	file_pathc                    ddl m} | sdS t          |           } ||           rdS |j        r=|j        dk    r2t          |j                  dk    st          |j                  dk    rdS |j        r|j        d         t          vr0d	                    t          t                              }d
| d|  dS t          |j                  dk     rd|j        d          dS dS )z
    Validate a file path for write_file/remove_file.
    Must be under an allowed subdirectory and not escape the skill dir.
    r   )has_traversal_componentzfile_path is required.z%Path traversal ('..') is not allowed.r   r      Nr   zFile must be under one of: z. Got: 'r   z5Provide a file path, not just a directory. Example: 'z/myfile.md')	tools.path_securityr   r   rV   r[   r   ALLOWED_SUBDIRSr   sorted)r   r   
normalizedr=   s       r!   _validate_file_pathr     s   
 <;;;;; (''iJ y)) 766  JOz99z  A%%Z-=)>)>!)C)C4  Kz/2/II))F?3344JWJJiJJJJ :q  gzGWXYGZgggg4r(   c                 D    ddl m} | |z  } |||           }|rd|fS |dfS )zNResolve a supporting-file path and ensure it stays within the skill directory.r   )validate_within_dirN)r   r   )r4   r   r   rs   rk   s        r!   _resolve_skill_targetr     sL    777777"F	22E U{4<r(   utf-8encodingc                    | j                             dd           t          j        t	          | j                   d| j         dd          \  }}	 t          j        |d|          5 }|                    |           d	d	d	           n# 1 swxY w Y   t          ||            d	S # t          $ rE 	 t          j        |           n-# t          $ r  t                              d
|d           Y nw xY w w xY w)au  
    Atomically write text content to a file.
    
    Uses a temporary file in the same directory and os.replace() to ensure
    the target file is never left in a partially-written state if the process
    crashes or is interrupted.
    
    Args:
        file_path: Target file path
        content: Content to write
        encoding: Text encoding (default: utf-8)
    Tparentsexist_ok.z.tmp.r   )dirprefixr   wr   Nz6Failed to remove temporary file %s during atomic writer7   )r   mkdirtempfilemkstempr   r[   osfdopenwriter   r   unlinkrH   r:   rk   )r   r   r   fd	temp_pathfs         r!   _atomic_write_textr    si    4$777$	 !!(9>(((  MB	

Yr3222 	aGGG	 	 	 	 	 	 	 	 	 	 	 	 	 	 	y),,,,,   	mIi     	m 	m 	mLLQS\gkLlllll	msT   B) *B B) BB) BB) )
C84C	C8	'C30C82C33C8c                    t          |           }|rd|dS t          |          }|rd|dS t          |          }|rd|dS t          |          }|rd|dS t	          |           }|rdd|  d|d          ddS t          | |          }|                    dd           |d	z  }t          ||           t          |          }|rt          j
        |d
           d|dS d}	 t          j        d|dd                   }	|	r\t          j        |d|	                                dz                      }
t!          |
                    dd                    dd         }n# t$          $ r Y nw xY wdd|  dt!          |                    t(                              t!          |          d|id}|r||d<   d                    |           |d<   |S )z.Create a new user skill with SKILL.md content.Fri   zA skill named 'z' already exists at r   r   Tr   r   )ignore_errorsr   r   r   Nr   x   r`   z
' created.)rj   messager   r   _changer   zTo add reference files, templates, or scripts, use skill_manage(action='write_file', name='{}', file_path='references/example.md', file_content='...')hint)r   r   r   r   r   r   r   r  rA   shutilrmtreer   r   r   r   r   r   r   r   rI   rK   r   )r[   r   r   errr   r4   r   
scan_error_desc_fm_end_parsedr<   s               r!   _create_skillr    sm    

C
 0 3///
X
&
&C
 0 3///  
(
(C
 0 3///
 
)
)C
 0 3/// 4  H 
TtTT&AQTTT
 
 	
 #422IOOD4O000 :%Hx))) &i00J 7it4444 :666 E)M7122;77 	>nWQw}}/B-B%CDDGM26677=E    -T---I))*5566MM!5) F  &%z	nntntuynznz 6N Ms   'A;E# #
E0/E0c                 J   t          |          }|rd|dS t          |          }|rd|dS t          |           }|sdt          |           dS t	          | |d         d          }|r|S |d         dz  }t          | |dd          }|r|S |                                r|                    d          nd}t          ||           t          |d                   }|r|t          ||           d|dS d	}		 t          j        d
|dd                   }
|
r\t          j        |d|
                                dz                      }t          |                    dd	                    dd         }	n# t"          $ r Y nw xY wdd|  dt          |d                   d|	idS )z:Replace the SKILL.md of any existing skill (full rewrite).Fri   r   rz   r   r   r   Nr   r   r   r   r  Tr`   z' updated (full rewrite).)rj   r	  r   r
  )r   r   r   r   rr   rw   r   	read_textr  rA   r   r   r   r   r   r   r   r   )r[   r   r  r   guardr   
read_guardoriginal_contentr  r  r  r  s               r!   _edit_skillr  L  s"   

(
(C
 0 3///
 
)
)C
 0 3///4  H I +A$+G+GHHH*4&1A6JJE *,H;h
 J   @H?P?PZx))7);;;VZx))) &hv&677J 7'x)9::: :666 E)M7122;77 	>nWQw}}/B-B%CDDGM26677=E    <T<<<HV$%%!5)	  s   9A;E5 5
FF
old_string
new_stringreplace_allc           
      b   |sdddS |dddS t          |           }|sdt          |           dS |d         }t          | |d          }|r|S |r5t          |          }|rd|dS t	          ||          \  }	}|rd|dS |	J n|dz  }	|	                                sdd	|	                    |           dS t          | |	d|sdn|          }
|
r|
S |	                    d
          }ddl	m
}  |||||          \  }}}}|rS|dd         t          |          dk    rdndz   }|}	 ddl	m} | |||||          z  }n# t          $ r Y nw xY wd||dS |sdn|}t          ||          }|rd|dS |st          |          }|rdd| dS |}t!          |	|           t#          |          }|rt!          |	|           d|dS dd|sdn| d|  d| d|dk    rdnd d	d}|dd         t          |          dk    rdndz   |dd         t          |          dk    rdndz   d |d!<   |S )"zTargeted find-and-replace within a skill file.

    Defaults to SKILL.md. Use file_path to patch a supporting file instead.
    Requires a unique match unless replace_all is True.
    Fz#old_string is required for 'patch'.ri   NzOnew_string is required for 'patch'. Use an empty string to delete matched text.r   r{   r   zFile not found: r   r   r   )fuzzy_find_and_replacei  z...r   )format_no_match_hint)rj   rk   file_previewr   z&Patch would break SKILL.md structure: TzPatched z in skill 'r   z replacementr   srT   rj   r	     u   …)oldnewr
  )r   r   rr   r   r   r   rI   rw   r  tools.fuzzy_matchr  r   r  r   r   r   r  rA   )r[   r  r  r   r  r   r4   r  r  rs   r  r   r  new_contentmatch_count	_strategymatch_errorpreviewerr_msgr  target_labelr  r  r<   s                           r!   _patch_skillr/    s      R +PQQQ +|}}}4  H I +A$+G+GHHH I*4GDDE  (!),, 	4$s333+IyAA 	4$s333!!!! Z'==?? _ +]f>P>PQZ>[>[+]+]^^^;#2

	 J  00G 9888887M7MZ8 84Ki  
$3$-CLL3,>,>55BG	>>>>>>++KjRYZZZGG 	 	 	D	 #
 
 	
 &/=::IL
 L
A
A
AC
 0 3///  #K00 	 G#GG  
 v{+++ &i00J 76#3444 :666  Z	Hjjy  Z  ZUY  Z  Z^i  Z  Z  J  MN  N  Nwzwz  TV  Z  Z  Z F $3$C
OOc,A,A55rJ$3$C
OOc,A,A55rJ F9 Ms   *E 
EEc                    t          |           }|sdt          |           dS t          | |d         d          }|r|S t          | |          }|r|S t	          |           }|rd|dS |)t          |t                    r|                                nd}t          |          }|r+|}|| k    r	dd| ddS t          |          }	|	s	dd| d	dS |d         }
t          |
          }t          |
          }|rd|dS 	 d
dlm}  |            }n# t          $ r d}Y nw xY w|rY	 d
dlm}  ||           \  }}n"# t          $ r}dd|  d| dcY d}~S d}~ww xY w|sd|dS d|  d| d}|r	|d| dz  }d|ddS t!          j        |
           |
j        }||k    rI|                                r5t)          |                                          s|                                 d|  d}|r	|d| dz  }d|dS )u<  Delete a skill.

    ``absorbed_into`` declares intent:
      - ``None`` / missing  → caller didn't declare (legacy / non-curator path);
        accepted for backward compat but logs a warning because the curator
        classification pipeline can't tell consolidation from pruning without it.
      - ``""`` (empty)      → explicit "truly pruned, no forwarding target".
      - ``"<skill-name>"``  → content was absorbed into that umbrella; the
        target must exist on disk. Validated here so the model can't claim an
        umbrella that doesn't exist.
    Fri   r   r|   Nr   zabsorbed_into='z'' cannot equal the skill being deleted.zR' does not exist. Create or patch the umbrella skill first, then retry the delete.r   r   )archive_skillzfailed to archive 'z': r`   z' archived (rT   z Content absorbed into 'rn   T)rj   r	  	_archivedz
' deleted.r#  )r   r   rr   r   re   r   r   r   boolrM   rZ   r   r   r   tools.skill_usager1  r  r  r   r   anyr   rmdir)r[   r   r   r  fail_closed
pinned_errabsorbed_targetis_consolidationtarget_namers   r4   skills_rootunsafer   curator_passr1  okarchive_msgr@   r	  r   s                        r!   _delete_skillrA    s_    4  H I +A$+G+GHHH*4&1A8LLE 
 6dMJJK t$$J 7 :666
 $M3)G)G$ 	 
 O,, %$ _;___   [)) 	 Xk X X X    I))44K %Y//F 3 6222??????++--     H	S777777+mD11OB 	S 	S 	S$/QT/Q/Qa/Q/QRRRRRRRR	S 	<${;;;=D==k=== 	FE/EEEEGG$GGG
M) FV^^=M=M9N9N((((G BAoAAAA   s0    D D D &D; ;
E
EEEfile_contentc                    t          |          }|rd|dS |s|dk    rdddS t          |                    d                    }|t          k    rdd|ddt          dd	dS t	          ||
          }|rd|dS t          |           }|sdt          | d          dS t          | |d         d          }|r|S t          |d         |          \  }}|rd|dS |J |	                                rt          | |d|          }|r|S |j                            dd           |	                                r|                    d          nd}	t          ||           t          |d                   }
|
r.|	t          ||	           n|                    d           d|
dS dd| d|  dt#          |          dS )z>Add or overwrite a supporting file within any skill directory.Fri   r   zfile_content is required.r   zFile content is r   z bytes (limit: z7 bytes / 1 MiB). Consider splitting into smaller files.r!  z& Create it first with action='create'.r   r}   NTr   r   )
missing_okFile 'z' written to skill 'rn   )rj   r	  r   )r   r   encodeMAX_SKILL_FILE_BYTESr   r   r   rr   r   r   rw   r   r   r  r  rA   r  r   )r[   r   rB  r  content_bytesr   r  rs   r  r  r  s              r!   _write_filerI  [  sz   
i
(
(C
 0 3/// HLB.. +FGGG ++G4455M+++:=; : :/D: : :
 
 	
 !Y
?
?
?C
 0 3///4  H s +A$Hp+q+qrrr*4&1A<PPE '(8)DDKFC
 0 3///}} ?&,	
 

  	
Mt444=C]]__Vv'''999RVv|,,, &hv&677J 7'v'78888MMTM*** :666 CICC4CCCF  r(   c           	      X   t          |          }|rd|dS t          |           }|sdt          |           dS |d         }t          | |d          }|r|S t	          ||          \  }}|rd|dS |J |                                sg }t          D ]|}||z  }	|	                                ra|	                    d          D ]K}
|
                                r5|	                    t          |
                    |                               L}dd| d|  d	|r|ndd
S t          | |d|          }|r|S |                                 |j        }||k    rI|                                r5t          |                                          s|                                 dd| d|  d	dS )z2Remove a supporting file from any skill directory.Fri   r   r~   N*rE  z' not found in skill 'rn   )rj   rk   available_filesTz' removed from skill 'r#  )r   r   r   rr   r   r   r   r   is_filerU   r   rI   rw   r  r   r5  r   r6  )r[   r   r  r   r4   r  rs   	availablesubdirdr  r  r   s                r!   _remove_filerQ    s    
i
(
(C
 0 3///4  H I +A$+G+GHHH I*4MJJE '	9==KFC
 0 3///==?? 
	% 	H 	HFF"Axxzz H H HAyy{{ H!((Q]]9-E-E)F)FGGGGiGGtGGG,5?yy4
 
 	
 <fmY J  
MMOOO ]Fv}}s6>>;K;K7L7L EIEETEEE  r(   skill_gate_bypassz_ctxvars.ContextVar[bool]_skill_gate_bypassc           
         | dvrdS t                                           rdS 	 ddlm} n# t          $ r Y dS w xY w|                    |j                  }|j        rdS |j        rt          |j
        d          S | |d}|                    d |                                D                        |                    | ||                    d	          pd
|                    d          pd
|                    d          pd
|                    d          pd
          }|                    |j        |||                                          }t!          j        dd|d         ||j
        dd          S )zEvaluate the skill write gate. Returns a JSON tool-result string when the
    write should NOT proceed (blocked or staged), or None to perform the real
    write. Bypassed during approved-pending replay.
    >   rz   r{   creater|   r}   r~   Nr   )write_approvalFrj   )rf   r[   c                     i | ]
\  }}|||S r$   r'   )r   kvs      r!   
<dictcomp>z+_apply_skill_write_gate.<locals>.<dictcomp>  s    MMMTQq}Aq}}}r(   r   r   r   r  r  )r   r   r  r  )summaryoriginTid)rj   staged
pending_idgistr	  ensure_ascii)rS  r   ra   rV  r   evaluate_gateSKILLSallowblocked
tool_errorr	  updateitems
skill_giststage_writecurrent_originjsondumps)rf   r[   payload_kwargswadecisionpayloadra  records           r!   _apply_skill_write_gateru    s   
 WWWt t.......   tt 	**H~ t ;(*E::::  ..GNNMM^%9%9%;%;MMMNNN==""9--3 $$[117R!%%l339r!%%l339r   D ^^BIwREVEVEXEX^YYF:Dt("2	4 	4   s   * 
88rs  c                 ^   t                               d          }	 t          |                     dd          |                     dd          |                     d          |                     d          |                     d          |                     d          |                     d	          |                     d
          |                     dd          |                     d          
  
        t                               |           S # t                               |           w xY w)zReplay a staged skill write, bypassing the gate. Returns the tool result
    JSON string. Called by the /skills approve handler.
    Trf   r   r[   r   r   r   rB  r  r  r  Fr   
rf   r[   r   r   r   rB  r  r  r  r   )rS  r   skill_manager   reset)rs  tokens     r!   apply_skill_pendingr{    s     ""4((E(;;x,,VR((KK	**[[,,kk+.. ^44{{<00{{<00M599!++o66
 
 
 	  ''''  ''''s   CD D,c
                 N   t          | |          }
|
t          j        |
d          S t          | |||||||||	
  
        }||S | dk    r%|st	          dd          S t          |||          }n| dk    r$|st	          d	d          S t          ||          }n| d
k    r:|st	          dd          S |t	          dd          S t          |||||          }n| dk    rt          ||	          }nq| dk    r8|st	          dd          S |t	          dd          S t          |||          }n3| dk    r$|st	          dd          S t          ||          }n	dd|  dd}|                    d          r	 ddlm}  |d           n# t          $ r Y nw xY w	 ddlm}m}m} ddlm} | dk    r |            r ||           n6| dv r ||           n&| dk    r |                    d          s ||           n# t          $ r Y nw xY wt          j        |d          S ) zz
    Manage user-created skills. Dispatches to the appropriate action handler.

    Returns JSON string with results.
    NFrb  )r   r   r   rB  r  r  r  r   rU  zVcontent is required for 'create'. Provide the full SKILL.md text (frontmatter + body).rW  rz   zGcontent is required for 'edit'. Provide the full updated SKILL.md text.r{   z=old_string is required for 'patch'. Provide the text to find.zLnew_string is required for 'patch'. Use empty string to delete matched text.r|   )r   r}   zJfile_path is required for 'write_file'. Example: 'references/api-guide.md'z*file_content is required for 'write_file'.r~   z(file_path is required for 'remove_file'.zUnknown action 'z<'. Use: create, edit, patch, delete, write_file, remove_fileri   rj   r   ) clear_skills_system_prompt_cacheT)clear_snapshot)
bump_patchforgetmark_agent_createdr   >   rz   r{   r}   r~   r2  )r   rn  ro  ru  rh  r  r  r/  rA  rI  rQ  r   agent.prompt_builderr}  r   r4  r  r  r  r   r   )rf   r[   r   r   r   rB  r  r  r  r   	preflightgate_resultr<   r}  r  r  r  r   s                     r!   rx  rx    sr   " -VT::Iz)%8888 *g,*}	  K  	Gv  AF  G  G  G  GtWh77	6		 	xgqvwwwwT7++	7		 	n]glmmmmlv{||||dJ
I{SS	8		t=AAA	<		 	{jtyzzzzJTYZZZZT9l;;	=	 	  	YHRWXXXXdI.. #  .E  .E  .E  .E  F  Fzz) 	MMMMMM,,DAAAAA 	 	 	D		PPPPPPPPPPCCCCCC!!'')) -&&t,,,III
4    8## zz+.. !F4LLL 	 	 	D	 :f51111s%   :F 
FFA"H 
HHrx  u   Manage skills (create, update, delete). Skills are your procedural memory — reusable approaches for recurring task types. New skills go to uo  /skills/; existing skills can be modified wherever they live.

Actions: create (full SKILL.md + optional category), patch (old_string/new_string — preferred for fixes), edit (full SKILL.md rewrite — major overhauls only), delete, write_file, remove_file.

On delete, pass `absorbed_into=<umbrella>` when you're merging this skill's content into another one, or `absorbed_into=""` when you're pruning it with no forwarding target. This lets the curator tell consolidation from pruning without guessing, so downstream consumers (cron jobs that reference the old skill name, etc.) get updated correctly. The target you name in `absorbed_into` must already exist — create/patch the umbrella first, then delete.

Create when: complex task succeeded (5+ calls), errors overcome, user-corrected approach worked, non-trivial workflow discovered, or user asks you to remember a procedure.
Update when: instructions stale/wrong, OS-specific failures, missing steps or pitfalls found during use. If you used a skill and hit issues not covered by it, patch it immediately.

After difficult/iterative tasks, offer to save as a skill. Skip for simple one-offs. Confirm with user before creating/deleting.

Good skills: trigger conditions, numbered steps with exact commands, pitfalls section, verification steps. Use skill_view() to see format examples.

Pinned skills are protected from deletion only — skill_manage(action='delete') will refuse with a message pointing the user to `hermes curator unpin <name>`. Patches and edits go through on pinned skills so you can still improve them as pitfalls come up; pin only guards against irrecoverable loss.objectstring)rU  r{   rz   r|   r}   r~   zThe action to perform.)typeenumr   zSkill name (lowercase, hyphens/underscores, max 64 chars). Must match an existing skill for patch/edit/delete/write_file/remove_file.)r  r   zFull SKILL.md content (YAML frontmatter + markdown body). Required for 'create' and 'edit'. For 'edit', read the skill first with skill_view() and provide the complete updated text.zText to find in the file (required for 'patch'). Must be unique unless replace_all=true. Include enough surrounding context to ensure uniqueness.zXReplacement text (required for 'patch'). Can be empty string to delete the matched text.booleanzZFor 'patch': replace all occurrences instead of requiring a unique match (default: false).zOptional category/domain for organizing the skill (e.g., 'devops', 'data-science', 'mlops'). Creates a subdirectory grouping. Only used with 'create'.zPath to a supporting file within the skill directory. For 'write_file'/'remove_file': required, must be under references/, templates/, scripts/, or assets/. For 'patch': optional, defaults to SKILL.md if omitted.z0Content for the file. Required for 'write_file'.u  For 'delete' only — declares intent so the curator can tell consolidation from pruning without guessing. Pass the umbrella skill name when this skill's content was merged into another (the target must already exist). Pass an empty string when the skill is truly stale and being pruned with no forwarding target. Omitting the arg on delete is supported for backward compatibility but downstream tooling (e.g. cron-job skill reference rewriting) will have to guess at intent.)
rf   r[   r   r  r  r  r   r   rB  r   )r  
propertiesrequired)r[   r   
parameters)registryrh  c                    t          |                     dd          |                     dd          |                     d          |                     d          |                     d          |                     d          |                     d          |                     d	          |                     d
d          |                     d          
  
        S )Nrf   r   r[   r   r   r   rB  r  r  r  Fr   rw  )rx  r   )argskws     r!   <lambda>r    s    |xx"%%XXfb!!##*%%((;''XXn--88L))88L))HH]E22hh//
 1 
 1 
 1 r(   u   📝)r[   toolsetschemahandleremoji)r   N)r   r$   )r   )r   )NF)NNNNNNFN)Y__doc__rn  loggingr   r   r  r   contextvars_ctxvarspathlibr   typingr   r   r   r   r   r   r	   r
   utilsr   r   r1   r   	getLogger__name__r:   
ContextVarr   r   __annotations__r"   r3  r%   r)   tools.skills_guardr*   r+   r,   r9   ImportErrorr3   r   rA   r   HERMES_HOMErK   r   r   rM   rR   rZ   re   rr   rw   r   r   r   rG  compiler   r   r   r   r   r   r   r   r   r   r   r   r  r  r  r/  rA  rI  rQ  rS  ru  r{  rx  SKILL_MANAGE_SCHEMAtools.registryr  rh  registerr'   r(   r!   <module>r     s^
    B   				 				             3 3 3 3 3 3 3 3 3 3 3 3 3 3 A A A A A A A A 1 1 1 1 1 1 1 1 % % % % % %		8	$	$GZxGZ"IIKKH H H D   
:D :T : : : :2;d ;t ; ; ; ;3 3 3 3WWWWWWWWWW   d    &D Xc]    4  o8#
      ,
D 
T 
 
 
 
:t : : : : :z     6R
RR R d38n	R R R Rj
  	
 d38n   <J JC JHT#s(^<T J J J J/
/&sm/d38n/ / / /d "    
455 CBB #    # 8C=    2$3 $8C= $ $ $ $N C  Xc]     S C 4    c htCH~6    (? ?U395E0F ? ? ? ?D" " "c "3 " " " "J"3 "8C= " " " "JT c eHTNT\]`TaDa>b     $   RV    HA A Ac AS ADcN A A A AH2c 2C 2DcN 2 2 2 2r n n
nn n 	n
 n 
#s(^n n n nbf f fHSM fT#s(^ f f f fR;c ;c ; ;c3h ; ; ; ;|2s 2s 2tCH~ 2 2 2 2v    2E(2E3 3 3 /   
$ $ $N(c3h (C ( ( ( (2 b2 b2b2
b2 b2 	b2
 b2 b2 b2 b2 b2 b2 	b2 b2 b2 b2T 	H//11	H 	H 	H<  !ZZZ7  !a  !U  !)  !2  "{ 
 !/  !N  !Q 
 !? {K
 K
X v&]O OAp p h 0 / / / / / / /  	
1 
1      s   B B%$B%