
    )j                     x   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mZ ddl	m
Z
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  ej        e          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Z'dZ(dedefdZ)de"dee"         fdZ*dZ+dZ, ej-        d          Z.h dZ/de"dee"         fdZ0dee"         dee"         fdZ1de"dee"         fdZ2d\de"d e"dee"         fd!Z3d]de"de"defd"Z4de"deee"ef                  fd#Z5de"deee"ef                  fd$Z6d^de"d&e"de"fd'Z7d(e"dee"         fd)Z8ded(e"deee         ee"         f         fd*Z9d_d(ede"d,e"ddfd-Z:d]de"de"de"dee"ef         fd.Z;de"de"dee"ef         fd/Z<	 	 d`de"d0e"d1e"d(e"d2e dee"ef         fd3Z=d]de"d4ee"         dee"ef         fd5Z>de"d(e"d6e"dee"ef         fd7Z?de"d(e"dee"ef         fd8Z@ddlAZB eBjC        d9d
:          ZDd;eEd<<   d= ZFd>ee"ef         de"fd?ZG	 	 	 	 	 	 	 	 dad@e"de"de"de"d(e"d6e"d0e"d1e"d2e d4e"de"fdAZHdBdC e             dDdEdFg dGdHdIdFdJdKdFdLdKdFdMdKdFdNdKdOdPdKdFdQdKdFdRdKdFdSdKdFdTdKdU
d@dgdVdWZIddXlJmKZKmLZL  eKjM        dBdeIdY dZ[           dS )bu  
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)get_hermes_homedisplay_hermes_home)DictAnyListOptionalTuple)atomic_replaceis_truthy_value)cfg_get)
scan_skillshould_allow_installformat_scan_reportTFreturnc                      	 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_createdFdefault)hermes_cli.configr   r   r   	Exception)r   cfgs     =/home/ubuntu/.hermes/hermes-agent/tools/skill_manager_tool.py_guard_agent_created_enabledr   ;   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_AVAILABLEr   r   r   r   loggerwarningr   )r   resultallowedreasonreportes         r   _security_scan_skillr*   N   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_utilsr.   resolveOSErrorrelative_to
ValueError
SKILLS_DIR)r,   r.   resolvedroots       r   _containing_skills_rootr7   s   s     655555%%''    $#%%  	  000KKKG$ 	 	 	H	s    ,,(A''A;:A;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 %sTr    N)toolsr:   
get_recordgetr   r#   debug)r8   r:   recs      r   _pinned_guardrB      s    O%%%%%%$$T**778 	($ ( ()-( ( (	  O O O8$NNNNNO4s   8< 'A&%A&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match)r8   s    r   _validate_namerM      sn     )((
4yy?""B_BBBBt$$ 
R4 R R R	
 4    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 rH   )
isinstancestrstriprI   rJ   rK   rL   )rO   s    r   _validate_categoryrV      s    th$$ ,++~~H t
h$(**Z Z Z Z	
 8}}&&@?@@@@x(( 
Z Z Z Z	
 4rN   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.z
\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).r8   z&Frontmatter must include 'name' field.descriptionz-Frontmatter must include 'description' field.zDescription exceeds rH   zRSKILL.md must have content after the frontmatter (instructions, procedures, etc.).)rU   
startswithresearchstartyaml	safe_load	YAMLErrorrS   dictrI   rT   MAX_DESCRIPTION_LENGTHend)rW   	end_matchyaml_contentparsedr)   bodys         r   _validate_frontmatterri      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)rI   MAX_SKILL_CONTENT_CHARS)rW   rk   s     r   _validate_content_sizero      sY    
 7||--- - -#g,,> - -.5- - -	
 4rN   c                 4    |rt           |z  | z  S t           | z  S )zFBuild the directory path for a new skill, optionally under a category.)r4   )r8   rO   s     r   _resolve_skill_dirrq     s%     ,H$t++rN   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   )r.   is_excluded_skill_pathrj   pathN)r/   r.   rs   existsrglobparentr8   )r8   r.   rs   
skills_dirskill_mds        r   _find_skillrz     s     NMMMMMMM))++ 1 1
  "" 	"((44 	1 	1H%%h// #t++000000 ,	1
 4rN   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)rs   r   r   profilesrj   )hermes_constantsr|   r/   rs   r   r4   ru   r0   appendr1   RuntimeErroris_diriterdirr8   rv   rw   )r8   matchesr|   rs   r6   
active_dir
candidatesdefault_skillsprofiles_rootentrypskillsprofile_namerx   ry   s                 r   _find_skill_in_other_profilesr   *  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 ''.   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 )'N ).0p_s      r   	<genexpr>z)_skill_not_found_error.<locals>.<genexpr>  s.      ::41ah!hhh::::::rN   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   rI   join)	r8   r   r   activebaseothersother_profile
other_pathnamess	            r   _skill_not_found_errorr   l  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 	== KrN   	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.rj   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   partsr8   rI   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4rN   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   )r   r   r   targeterrors        r   _resolve_skill_targetr     sL    777777"F	22E U{4<rN   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 writer    )rw   mkdirtempfilemkstemprT   r8   osfdopenwriter   r   unlinkr1   r#   r   )r   rW   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d|  dt          |                    t                              t          |          d}|r||d<   d                    |           |d<   |S )z.Create a new user skill with SKILL.md content.Fsuccessr   zA skill named 'z' already exists at rt   r   Tr   rj   )ignore_errorsr<   z
' created.)r   messagert   ry   rO   zTo add reference files, templates, or scripts, use skill_manage(action='write_file', name='{}', file_path='references/example.md', file_content='...')hint)rM   rV   ri   ro   rz   rq   r   r   r*   shutilrmtreerT   r2   r4   format)	r8   rW   rO   errexistingr   ry   
scan_errorr%   s	            r   _create_skillr     s    

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 -T---I))*5566MM	 F  &%z	nntntuynznz 6N MrN   c                    t          |          }|rd|dS t          |          }|rd|dS t          |           }|sdt          |           dS |d         dz  }|                                r|                    d          nd}t          ||           t          |d                   }|r|t          ||           d|dS dd	|  d
t          |d                   dS )z:Replace the SKILL.md of any existing skill (full rewrite).Fr   rt   rj   r   r   NTr<   z
' updated.r   r   rt   )	ri   ro   rz   r   ru   	read_textr   r*   rT   )r8   rW   r   r   ry   original_contentr   s          r   _edit_skillr     s3   

(
(C
 0 3///
 
)
)C
 0 3///4  H I +A$+G+GHHH*,H?G?P?PZx))7);;;VZx))) &hv&677J 7'x)9::: :666 -T---HV$%%  rN   
old_string
new_stringreplace_allc           
      h   |sdddS |dddS t          |           }|sdt          |           dS |d         }|r1t          |          }|rd|dS t          ||          \  }}|rd|dS n|dz  }|                                sdd|                    |           d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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'.r   NzOnew_string is required for 'patch'. Use an empty string to delete matched text.rt   rj   zFile not found: r   r   r   )fuzzy_find_and_replacei  z...r   )format_no_match_hint)r   r   file_previewrk   z&Patch would break SKILL.md structure: TzPatched z in skill 'r   z replacementr   sz).r   r   )rz   r   r   r   ru   r2   r   tools.fuzzy_matchr   rI   r   r   ro   ri   r   r*   )r8   r   r   r   r   r   r   r   r   rW   r   new_contentmatch_count	_strategymatch_errorpreviewerr_msgr   target_labelr   r   s                        r   _patch_skillr   ?  sC     R +PQQQ +|}}}4  H I +A$+G+GHHH I 
(!),, 	4$s333+IyAA 	4$s333	4 Z'==?? _ +]f>P>PQZ>[>[+]+]^^^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  s   7D 
DDabsorbed_intoc                    t          |           }|sdt          |           dS t          |           }|rd|dS |ft          |t                    rQ|                                r=|                                }|| k    r	dd| ddS t          |          }|s	dd| ddS |d         }t          |          }t          j        |           |j	        }||k    rI|
                                r5t          |                                          s|                                 d|  d	}	|Dt          |t                    r/|                                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.
    Fr   Nzabsorbed_into='z'' cannot equal the skill being deleted.zR' does not exist. Create or patch the umbrella skill first, then retry the delete.rt   r<   z
' deleted.z Content absorbed into 'r   Tr   )rz   r   rB   rS   rT   rU   r7   r   r   rw   ru   anyr   rmdir)
r8   r   r   
pinned_errtarget_namer   r   skills_rootrw   r   s
             r   _delete_skillr     s    4  H I +A$+G+GHHHt$$J 7 :666  Zs%C%C H[H[H]H] #))++$ _;___   [)) 	 Xk X X X    I))44K
M) FV^^=M=M9N9N((((G Zs%C%C H[H[H]H] Gm.A.A.C.CGGGG   rN   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         |          \  }}|rd|d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.Fr   r   zfile_content is required.r   zFile content is rm   z bytes (limit: z7 bytes / 1 MiB). Consider splitting into smaller files.r   z& Create it first with action='create'.rt   Tr   r   N)
missing_okFile 'z' written to skill 'r   r   )r   rI   encodeMAX_SKILL_FILE_BYTESro   rz   r   r   rw   r   ru   r   r   r*   r   rT   )	r8   r   r   r   content_bytesr   r   r   r   s	            r   _write_filer     s   
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'(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  rN   c           	         t          |          }|rd|dS t          |           }|sdt          |           dS |d         }t          ||          \  }}|rd|dS |                                sg }t
          D ]|}||z  }|                                ra|                    d          D ]K}	|	                                r5|                    t          |	
                    |                               L}dd| d|  d|r|ndd	S |                                 |j        }
|
|k    rI|
                                r5t          |
                                          s|
                                 d
d| d|  ddS )z2Remove a supporting file from any skill directory.Fr   rt   *r   z' not found in skill 'r   N)r   r   available_filesTz' removed from skill 'r   )r   rz   r   r   ru   r   rv   is_filer   rT   r2   r   rw   r   r   r   )r8   r   r   r   r   r   	availablesubdirdr   rw   s              r   _remove_filer  
  s   
i
(
(C
 0 3///4  H I +A$+G+GHHH I'	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
 
 	
 MMOOO ]Fv}}s6>>;K;K7L7L EIEETEEE  rN   skill_gate_bypassr   z_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.
    >   editpatchcreatedelete
write_fileremove_fileNr   )write_approvalFr   )actionr8   c                     i | ]
\  }}|||S Nr   )r   kvs      r   
<dictcomp>z+_apply_skill_write_gate.<locals>.<dictcomp>X  s    MMMTQq}Aq}}}rN   rW   r   r   r   r   )rW   r   r   r   )summaryoriginTid)r   staged
pending_idgistr   ensure_ascii)r  r?   r=   r  r   evaluate_gateSKILLSallowblocked
tool_errorr   updateitems
skill_giststage_writecurrent_originjsondumps)r  r8   payload_kwargswadecisionpayloadr  records           r   _apply_skill_write_gater0  A  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r.  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.
    Tr  r   r8   rW   rO   r   r   r   r   r   Fr   
r  r8   rW   rO   r   r   r   r   r   r   )r  setskill_manager?   reset)r.  tokens     r   apply_skill_pendingr7  h  s     ""4((E(;;x,,VR((KK	**[[,,kk+.. ^44{{<00{{<00M599!++o66
 
 
 	  ''''  ''''s   CD D,r  c
                    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 ||           n!| dv r ||           n| dk    r ||           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.
    )rW   rO   r   r   r   r   r   r   Nr  zVcontent is required for 'create'. Provide the full SKILL.md text (frontmatter + body).Fr  r	  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_filer   r   r   ) clear_skills_system_prompt_cacheT)clear_snapshot)
bump_patchforgetmark_agent_created)is_background_review>   r	  r
  r  r  r  )r0  r#  r   r   r   r   r   r  r?   agent.prompt_builderr9  r   tools.skill_usager;  r<  r=  tools.skill_provenancer>  r)  r*  )r  r8   rW   rO   r   r   r   r   r   r   gate_resultr%   r9  r;  r<  r=  r>  s                    r   r4  r4  ~  s0   * *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##t 	 	 	D	 :f51111s%   E% %
E21E26AG 
GGr4  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)r  r
  r	  r  r  r  zThe action to perform.)typeenumrZ   zSkill name (lowercase, hyphens/underscores, max 64 chars). Must match an existing skill for patch/edit/delete/write_file/remove_file.)rE  rZ   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.)
r  r8   rW   r   r   r   rO   r   r   r   )rE  
propertiesrequired)r8   rZ   
parameters)registryr#  c                    t          |                     dd          |                     dd          |                     d          |                     d          |                     d          |                     d          |                     d          |                     d	          |                     d
d          |                     d          
  
        S )Nr  r   r8   rW   rO   r   r   r   r   r   Fr   r2  )r4  r?   )argskws     r   <lambda>rO  Y  s    |xx"%%XXfb!!##*%%((;''XXn--88L))88L))HH]E22hh//
 1 
 1 
 1 rN   u   📝)r8   toolsetschemahandleremoji)rj   r  )r   )r   )NF)NNNNNNFN)N__doc__r)  loggingr   r\   r   r   pathlibr   r~   r   r   typingr   r   r   r	   r
   utilsr   r   r   r   	getLogger__name__r#   tools.skills_guardr   r   r   r"   ImportErrorboolr   rT   r*   r_   HERMES_HOMEr4   rJ   rc   r7   rB   rn   r   compilerK   r   rM   rV   ri   ro   rq   rz   r   r   r   r   r   r   r   r   r   r   r  contextvars_ctxvars
ContextVarr  __annotations__r0  r7  r4  SKILL_MANAGE_SCHEMAtools.registryrK  r#  registerr   rN   r   <module>rg     sN    B   				 				         A A A A A A A A 3 3 3 3 3 3 3 3 3 3 3 3 3 3 1 1 1 1 1 1 1 1 % % % % % %		8	$	$WWWWWWWWWW   d    &D Xc]    4  o8#
      ,     6 "    
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6 6 6c 6S 6DcN 6 6 6 6rc C DcN    J [ [
[[ [ 	[
 [ 
#s(^[ [ [ [|6 6 6HSM 6T#s(^ 6 6 6 6r1c 1c 1 1c3h 1 1 1 1h(s (s (tCH~ ( ( ( (b    2E(2E3 3 3 /   
$ $ $N(c3h (C ( ( ( (2 Z2 Z2Z2
Z2 Z2 	Z2
 Z2 Z2 Z2 Z2 Z2 Z2 	Z2 Z2 Z2 Z2D 	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   A$ $A.-A.