
    sj                        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mZ ddl	m
Z
mZ ej        ej        fD ]4Z eed          r&	 e                    dd           '# eef$ r Y 0w xY w5dd	lmZmZmZ dd
lmZ ddlmZmZmZmZmZ ddlm Z m!Z!  ej"        e#          Z$ e            Z%e%dz  Z&e&dz  Z'e%Z(e&Z)e'Z*de
fdZ+de
fdZ,de
fdZ-dZ.de
fdZ/de
fdZ0dee1         fdZ2dee1e1f         fdZ3de4fdZ5dee1e1f         fdZ6de
de1de1fdZ7de
deee1e
f                  fdZ8d e
de
de
fd!Z9d"e
de1fd#Z:d$e
d%e
de1fd&Z;d e
dee1         fd'Z<d"e
de1fd(Z=dee1ee1e1e
f         f         fd)Z>d$e
d*e
de1fd+Z?d,d-d.e1d/e@deAfd0ZBdee1ee
         f         fd1ZC	 dcd2e1d3eee1ee
         f                  dee
         fd4ZDddd5e@dee1         fd6ZEdee1         fd7ZFdee1ee
         f         fd8ZGd9e1d:e1d;e
d<ee1ee
         f         d=ee1         d5e@dee1         fd>ZHddd5e@deAfd?ZId$e
ddfd@ZJddd.e1d/e@deAfdAZKd:e1dBe1de@fdCZLdeeA         fdDZMd$e
deeeN         ee1         f         fdEZOd.e1deAfdFZPdGe@deAfdHZQde@fdIZRdddJe@deAfdKZSe#dLk    rW eTdM            eId,N          ZU eVeUdO                    dP eVeUdQ                    dReUdS          dTgZWeUdU         rneUdU         ZXdVZYdWZ                    eXdeY                   Z[ eVeX          eYk    re[dX eVeX          eYz
   dYz  Z[eW\                     eVeX           dZe[            eUd[         r'eW\                     eVeUd[                    d\           eU]                    d]          r'eW\                     eVeUd]                    d^            eTd_dWZ                    eW           d`eUda          db           dS dS )eu=  
Skills Sync -- Manifest-based seeding and updating of bundled skills.

Copies bundled skills from the repo's skills/ directory into ~/.hermes/skills/
and uses a manifest to track which skills have been synced and their origin hash.

Manifest format (v2): each line is "skill_name:origin_hash" where origin_hash
is the MD5 of the bundled skill at the time it was last synced to the user dir.
Old v1 manifests (plain names without hashes) are auto-migrated.

Update logic:
  - NEW skills (not in manifest): copied to user dir, origin hash recorded.
  - EXISTING skills (in manifest, present in user dir):
      * If bundled still matches origin hash: no update → skip without reading
        the user copy.
      * If bundled changed and user copy matches origin hash: safe to update.
      * If bundled changed and user copy differs: user customized it → SKIP.
  - DELETED by user (in manifest, absent from user dir): respected, not re-added.
  - REMOVED from bundled (in manifest, gone from repo): cleaned from manifest.

The manifest lives at ~/.hermes/skills/.bundled_manifest.
    N)datetimetimezone)PathPurePosixPathreconfigureutf-8replaceencodingerrors)get_bundled_skills_dirget_hermes_homeget_optional_skills_dir)is_excluded_skill_path)DictListOptionalSetTuple)atomic_replaceatomic_write_textskills.bundled_manifestreturnc                  `    t          t                    } | t          k    r| S t                      S )z5Return the active profile's HERMES_HOME at call time.)r   HERMES_HOME_HERMES_HOME_AT_IMPORTr   
configureds    5/home/agent/.hermes/hermes-agent/tools/skills_sync.py_hermes_homer!   K   s-    k""J+++    c                  f    t          t                    } | t          k    r| S t                      dz  S )z:Return the active profile's skills directory at call time.r   )r   
SKILLS_DIR_SKILLS_DIR_AT_IMPORTr!   r   s    r    _skills_dirr&   S   s0    j!!J***>>H$$r"   c                  f    t          t                    } | t          k    r| S t                      dz  S )zAReturn the active profile's bundled-skills manifest at call time.r   )r   MANIFEST_FILE_MANIFEST_FILE_AT_IMPORTr&   r   s    r    _manifest_filer*   [   s1    m$$J---==...r"   z.no-bundled-skillsc                  ^    t          t          t                    j        j        dz            S )zLocate the bundled skills/ directory.

    Checks HERMES_BUNDLED_SKILLS env var first (set by Nix wrapper),
    then falls back to the relative path from this source file.
    r   )r   r   __file__parent r"   r    _get_bundled_dirr/   l   s#     "$x.."7">"IJJJr"   c                  ^    t          t          t                    j        j        dz            S )z/Locate the official optional-skills/ directory.optional-skills)r   r   r,   r-   r.   r"   r    _get_optional_dirr2   u   s"    "4>>#8#?BS#STTTr"   c                     	 ddl m} m} n# t          $ r t	                      cY S w xY w |             t	                      } |             D ]r}|                    d          D ]Z}t          |          r|j        }|                    |j	                   t          |d          }|r|                    |           [s|S )zIndex every skill available in external_dirs by name and frontmatter name.

    Returns a set of skill names that are already provided by external dirs.
    Used to prevent sync_skills from shadowing externally-delegated skills.
    r   )get_external_skills_dirs_external_dirs_cache_clearSKILL.md )agent.skill_utilsr4   r5   ImportErrorsetrglobr   r-   addname_read_skill_name)r4   r5   external_namesext_dirskill_md	skill_dirfrontmatter_names          r    _build_external_skill_indexrD   z   s   ZZZZZZZZZ   uu    "uuN++-- 
5 
5j11 		5 		5H%h//  Iy~.../"== 5""#3444		5 s    &&c                     t                                                      si S 	 i } t                                          d                                          D ]e}|                                }|sd|v rC|                    d          \  }}}|                                | |                                <   `d| |<   f| S # t          t          f$ r i cY S w xY w)z
    Read the manifest as a dict of {skill_name: origin_hash}.

    Handles both v1 (plain names) and v2 (name:hash) formats.
    v1 entries get an empty hash string which triggers migration on next sync.
    r   r   :r7   )r*   exists	read_text
splitlinesstrip	partitionOSErrorIOError)resultliner=   _hash_vals        r    _read_manifestrS      s     ""$$ 	"$$...@@KKMM 
	" 
	"D::<<D d{{$(NN3$7$7!a'/~~'7'7tzz||$$  "tW   			s   BC CCc                     	 ddl m}   |             S # t          $ r t                      dz  }|                                st                      cY S t                      }	 |                    d                                          D ]B}|                                }|r*|	                    d          s|
                    |           Cn# t          $ r Y nw xY w|cY S w xY w)u  Built-in skills the curator pruned — must NOT be re-seeded on sync.

    Delegates to ``tools.skill_usage`` (single source of truth) and falls back
    to reading ``~/.hermes/skills/.curator_suppressed`` directly if that import
    is unavailable in a packaged/update context.
    r   )read_suppressed_namesz.curator_suppressedr   rF   #)tools.skill_usagerU   	Exceptionr&   rH   r:   rI   rJ   rK   
startswithr<   rM   )rU   pathnamesrP   s       r    _read_suppressed_namesr\      s   ;;;;;;$$&&&   }}44{{}} 	55LLL	88CCEE $ $zz|| $ 4 4 $IIdOOO$  	 	 	D	s:    =C!C! A+CC!
CC!CC! C!entriesc                    t                      j                            dd           d                    d t	          |                                           D                       dz   }	 t          t                      |dd           d	S # t          $ r5}t          	                    dt                      |d           Y d	}~d	S d	}~ww xY w)
u@  Write the manifest file atomically in v2 format (name:hash).

    Uses the shared atomic writer so an existing manifest's permission
    bits (and owner, best-effort) survive the replace instead of being
    reset to mkstemp's 0600 — the same mode-preservation contract as the
    skill manager's document writes.
    Tparentsexist_ok
c              3   *   K   | ]\  }}| d | V  dS )rG   Nr.   ).0r=   rR   s      r    	<genexpr>z"_write_manifest.<locals>.<genexpr>   s4      XXndH))x))XXXXXXr"   z.bundled_manifest_)
tmp_prefixpreserve_modez&Failed to write skills manifest %s: %sexc_infoN)
r*   r-   mkdirjoinsorteditemsr   rX   loggerdebug)r]   dataes      r    _write_manifestrr      s     !!$!>>>99XXw}}@W@WXXXXX[__Dc+		
 	
 	
 	
 	
 	
  c c c=~?O?OQR]abbbbbbbbbcs   +B 
C*CCrA   fallbackc                    	 |                      dd          dd         }n# t          $ r |cY S w xY wd}|                    d          D ]}|                                }|dk    r|r nbd	}#|r\|                    d
          rG|                    dd          d                                                             d          }|r|c S |S )zORead the name field from SKILL.md YAML frontmatter, falling back to *fallback*.r   r	   r
   Ni  Frb   z---Tzname:rG      z"')rI   rM   splitrK   rY   )rA   rs   contentin_frontmatterrP   strippedvalues          r    r>   r>      s   $$gi$HH$O   Nd## 
 
::<<u !N 	h11':: 	NN3**1-3355;;EBBE Os   " 11bundled_dirc                    g }|                                  s|S |                     d          D ]Z}t          |                    |           |           r'|j        }t          ||j                  }|                    ||f           [|S )zz
    Find all SKILL.md files in the bundled directory.
    Returns list of (skill_name, skill_directory_path) tuples.
    r6   root)rH   r;   r   relative_tor-   r>   r=   append)r{   r   rA   rB   
skill_names        r    _discover_bundled_skillsr      s    
 F %%j11 / /
 "  --K
 
 
 	 O	%h	??
z9-....Mr"   rB   c                 N    |                      |          }t                      |z  S )z
    Compute the destination path in the skills dir preserving the category structure.
    e.g., bundled/skills/mlops/axolotl -> ~/.hermes/skills/mlops/axolotl
    )r   r&   )rB   r{   rels      r    _compute_relative_destr     s&    
 


,
,C==3r"   	directoryc                    t          j                    }	 t          |                     d                    D ]}|                                rq|                    |           }|                    t          |                              d                     |                    |	                                           n# t          t          f$ r Y nw xY w|                                S )zHCompute a hash of all file contents in a directory for change detection.*r   )hashlibmd5rl   r;   is_filer   updatestrencode
read_bytesrM   rN   	hexdigest)r   hasherfpathr   s       r    	_dir_hashr     s    []]FIOOC0011 	2 	2E}} 2''	22c#hhoog66777e..00111		2
 W   s   B*C   CCrZ   basec                 @   |                      |          }|                                }t          |          }d |j        D             }|                                s|rt          d |D                       rt          d|           d                    |          S )zLReturn a normalized relative POSIX path, rejecting traversal/absolute paths.c                     g | ]}|d v|	S )>   r7   .r.   rd   parts     r    
<listcomp>z*_safe_rel_install_path.<locals>.<listcomp>,  s"    BBBdD	,A,AT,A,A,Ar"   c              3   "   K   | ]
}|d k    V  dS )z..Nr.   r   s     r    re   z)_safe_rel_install_path.<locals>.<genexpr>-  s&      -M-Mtddl-M-M-M-M-M-Mr"   zUnsafe optional skill path: /)r   as_posixr   partsis_absoluteany
ValueErrorrk   )rZ   r   r   posixpurer   s         r    _safe_rel_install_pathr   '  s    


4
 
 CLLNNEDBBdjBBBE A A#-M-Mu-M-M-M*M*M A???@@@88E??r"   c                     g }t          |                     d                    D ]P}|                                r:|                    |                    |                                                      Q|S )z8List files inside a skill directory in lock-file format.r   )rl   r;   r   r   r   r   )rB   filesr   s      r    _skill_file_listr   2  so    E	,,-- B B==?? 	BLL**955>>@@AAALr"   c                 d    	 ddl m}  ||           S # t          $ r t          |           cY S w xY w)zJReturn the same hash style the skills hub lock uses, falling back locally.r   )content_hash)tools.skills_guardr   rX   r   )r   r   s     r    _content_hashr   ;  sZ    $333333|I&&& $ $ $ #####$s    //c                     t                      } i }|                                 s|S t          |                     d                    D ]v}t	          |                    |           |           r'|j        }	 t          ||           }n# t          $ r Y Lw xY w|j	        }t          ||          }|||f}|||<   |||<   w|S )a%  Return official optional skills keyed by folder name and frontmatter name.

    Values are ``(folder_name, install_path, source_dir)``. Multiple keys may
    point to the same skill so callers can accept either the folder slug used
    by the hub lock or the user-facing frontmatter name.
    r6   r}   )r2   rH   rl   r;   r   r   r-   r   r   r=   r>   )optional_dirindexrA   srcinstall_pathfolder_namerC   rz   s           r    _optional_skill_indexr   G  s     %&&L.0E   <--j99:: ( (!  ..\
 
 
 	 o	1#|DDLL 	 	 	H	h+HkBBlC0"k"'Ls   8B		
BBbackup_rootc                 D   |                      t                                }||z  }|j                            dd           |                                rd}|                    |j         d|                                           r6|dz  }|                    |j         d|                                           6|                    |j         d|           }t          j        t          |           t          |                     |
                                S )zLMove an existing skill directory into a restore backup, preserving rel path.Tr_   ru   -)r   r&   r-   rj   rH   	with_namer=   shutilmover   r   )rZ   r   r   targetsuffixs        r    _move_to_restore_backupr   d  s   


;==
)
)C3F
Mt444}} =&+888899@@BB 	aKF &+888899@@BB 	!!V[";";6";";<<
KD		3v;;'''<<>>r"   F)restorer=   r   c                D   t                      }|sddg g g dS | dv r1t          t          |                                          d           ng }|s%|                    |           }|dd|  g g g dS |g}g }g }t          j        t          j                  	                    d	          }t                      d
z  d| z  }|D ]1\  }	}
}t                      t          |
                    d           z  }t          |          }|                                ot          |          |k    }t          |dz  |	          }g }t                                                      rt          t                                          d                    D ]}t#          |          r|j        }	 |                    t                                 n# t(          $ r Y Hw xY wt          ||j                  }||k    rh|j        |	k    s||	|hv r|                    |           |r|D ]9}|                                r#|                    t/          ||                     :|                                r%|s#|                    t/          ||                     |                                sF|j                            dd           t3          j        ||           |                    |	           -|s13t7          d          }dd||||rt9          |          nddS )a3  Restore one or all official optional skills from repo source.

    ``restore=False`` only performs exact-match provenance backfill. ``restore=True``
    repairs already-mutated/reorganized skills by backing up matching active
    copies and copying the official optional source into its canonical path.
    Fz,No official optional skills directory found.)okmessagerestored
backfilled	backed_up>   r   allc                     | d         S )Nru   r.   )items    r    <lambda>z1restore_official_optional_skill.<locals>.<lambda>}  s
    47 r"   keyNz#Official optional skill not found: z%Y%m%d-%H%M%Sz.restore-backupszofficial-optional-r   r6   Tr_   quietz(Official optional skill repair complete.r7   )r   r   r   r   r   
backup_dir)r   rl   r:   valuesgetr   nowr   utcstrftimer&   r   rv   r   rH   r>   r;   r   r-   r   r   r=   r   r   rj   r   copytree_backfill_optional_provenancer   )r=   r   r   targetsr   r   r   	timestampr   r   r   r   destsrc_hashcanonical_oksrc_frontmattermatchesrA   	candidatecandidate_namematchr   s                         r    restore_official_optional_skillr   r  s    "##E K(Vdfvx  HJ  K  K  	KGK|G[G[fS((.B.BCCCCacG 4>,XRV,X,Xfhxz  JL  M  M  M(HIX\**33ODDI--"447WI7W7WWK*1 # #&\3}}t\%7%7%<%<==S>>{{}}D4H)D +3+;[II ==!! 	.";==#6#6z#B#BCC . .)(33 $O	))+--8888!   H!1(IN!K!K$$>[00N{TcFd4d4dNN9--- 	  R R<<>> R$$%<UK%P%PQQQ{{}} M\ M  !8{!K!KLLL;;== -!!$!>>>T***,,, 		 /T:::J= *3;c+&&&  s   *!G
GGc                     i } t                                                      s| S t                                          d          D ]}t          |          r|j        }	 |                                                    t                                                                 n# t          t          f$ r Y sw xY w| 	                    |j
        g                               |           | S )zCIndex installed skills by directory name with one active-tree scan.r6   )r&   rH   r;   r   r-   resolver   rM   r   
setdefaultr=   r   )r   rA   r   s      r    #_index_installed_skill_dirs_by_namer     s    #%E==!! MM''
33 	? 	?!(++ 	O		++KMM,A,A,C,CDDDD$ 	 	 	H	,,33I>>>>Ls    AB&&B:9B:skill_dir_nameinstalled_indexc                     | r t                                                      sdS |t                      }|                    | g           }t	          |          dk    rdS |d         S )u  Locate an installed skill directory by its directory name.

    Used only as a fallback when the repo-derived install path doesn't exist in
    the active tree (upstream recategorized the skill after it was installed).
    Returns None when there is no match, or when the name is AMBIGUOUS — two
    skills sharing a directory name give us no basis to pick one, and guessing
    would write provenance onto the wrong skill. The caller still verifies a
    byte-identical content hash before recording anything.
    Nru   r   )r&   rH   r   r   len)r   r   r   s      r    !_find_installed_skill_dir_by_namer     sn      !5!5!7!7 t=??!!."55G
7||qt1:r"   r   c                 `   t                      }|                                sg S t                      dz  dz  }	 |                                r(t          j        |                    d                    ndi d}n!# t          j        t          f$ r di d}Y nw xY w|                    di           }d |	                                D             }g }d	}d
}t          |                    d                    D ]}	t          |	          r|	j        }
	 t          |
|          }n3# t          $ r&}t                               d|
|           Y d
}~Wd
}~ww xY w|
j        }||v s||v rot                      t'          |                    d           z  }|                                r|                                sz|t-                      }t/          |
j        |          }|	 t          |t                                }n4# t          $ r'}t                               d||           Y d
}~4d
}~ww xY w||v rBt1          |          t1          |
          k    rdt3          j        t6          j                                                  }dd| ddt=          |          |t?          |          ddi||d
||<   |                     |           |!                    |           d}| stE          d| d           |r,|j        #                    dd           dd
l$}t          j%        |dd	          dz   }|&                    tO          |j                  dd           \  }}	 tQ          j)        |d!d          5 }|*                    |           |+                                 tQ          j,        |-                                           d
d
d
           n# 1 swxY w Y   t]          ||           n5# t^          $ r( 	 tQ          j0        |           n# t          $ r Y nw xY w w xY w|S )"a  Mark already-present official optional skills as hub-installed.

    This covers the migration case where a skill used to be bundled (or was
    manually copied into the active skills tree) and later lives under
    optional-skills/. If the active copy is byte-identical to the official
    optional source, record official hub provenance without copying or
    reinstalling anything. Modified/local skills are left alone.
    .hub	lock.jsonr   rF   ru   )version	installedr   c                 b    h | ],}t          |t                    |                    d           -S )r   )
isinstancedictr   )rd   entrys     r    	<setcomp>z0_backfill_optional_provenance.<locals>.<setcomp>  sD       eT""		.!!  r"   FNr6   z/Skipping optional skill with unsafe path %s: %sr   z(Skipping relocated optional skill %s: %sofficialz	official/builtinr   backfilled_fromr1   )
source
identifiertrust_levelscan_verdictr   r   r   metadatainstalled_at
updated_atTz  = z* (official optional provenance backfilled)r_   r      )indentensure_asciirb   z.lock_z.tmp)dirprefixr   w)1r2   rH   r&   jsonloadsrI   JSONDecodeErrorrM   r   r   rl   r;   r   r-   r   r   rn   ro   r=   r   rv   is_dirr   r   r   r   r   r   r   	isoformatr   r   r<   r   printrj   tempfiledumpsmkstempr   osfdopenwriteflushfsyncfilenor   BaseExceptionunlink)r   r   	lock_pathrp   r   existing_pathsr   changedinstalled_dir_indexrA   r   r   rq   	lock_namer   r   r  payloadfdtmp_pathfs                        r    r   r     s    %&&L   	&4I/DMDTDTDVDV{tz)--w-??@@@hixz\{\{ '* / / /2../R00I %%''  N JG;?<--j99:: 6P 6P!(++ 	o	1#|DDLL 	 	 	LLJCQRSSSHHHH	 H		!!\^%C%C}}t\%7%7%<%<=={{}} 	DKKMM 	 #*&I&K&K#4SX?RSSD|5dKMMJJ   GqQQQ >))T??inn,,L..88::	 4l44$()$//(%d++*,=>%# 
  
	) 	<((()$$$ 	PNNNNOOO td;;; 	*T!%@@@4G''I$%% ( 
 
H
	2sW555 %   			$$$% % % % % % % % % % % % % % % 8Y//// 	 	 		(####   	 s   AA< <BBD%%
E/EE$H
H2H--H2*O9 AOO9 O!!O9 $O!%O9 9
P+PP+
P&#P+%P&&P+c                  P   t                      dz  dz  } |                                 st                      S 	 t          j        |                     d                    }n*# t          j        t          f$ r t                      cY S w xY wt                      }|                    d          pi 	                                D ]c}t          |t                    rL|                    d          }|r5|                    t          |                              d                     d|S )aq  Return install paths recorded in the skills-hub lock, as POSIX strings.

    Hub-installed skills are owned by the hub (``hermes skills uninstall``),
    never by bundled sync. Rename recovery must not move them even when their
    content happens to match a bundled origin hash, or the lock's
    ``install_path`` would point at a directory that no longer exists.
    r   r   r   rF   r   r   r   )r&   rH   r:   r  r  rI   r  rM   r   r   r   r   r<   r   rK   )r  rp   pathsr   r   s        r    _read_hub_install_pathsr  R  s    &4I uuz)--w-??@@ '*   uueeE((;''-25577 8 8eT"" 	8 99^44L 8		#l++11#66777Ls   (A! !$BBc                  @   i } t                                                      s| S t                                          d          D ]W}t          |          r|j        }t          ||j                  }|                     |g                               |           X| S )zIndex every skill in the user's tree by frontmatter name.

    Returns ``{skill_name: [skill_dir, ...]}``. Used by rename recovery to
    locate a bundled skill that upstream moved to a new category/directory.
    r6   )	r&   rH   r;   r   r-   r>   r=   r   r   )r   rA   rB   r=   s       r    _index_active_skillsr  j  s     $&E==!! MM''
33 5 5!(++ 	O	).99r"")))4444Lr"   r   origin_hashr   active_index	hub_pathsc                    |sdS |                     | g           D ]}||k    s|                                s	 |                    t                                                                }n# t
          $ r Y _w xY w||v rht          |          |k    rP|sMt          d|  d|                    t                                                                 d| d|  d	           	 |j        	                    dd           t          j        t          |          t          |                     n7# t          t          f$ r# t                              d	||d
           Y  dS w xY wt                              d||           |sJt          d|  d| d|                    t                                                                 d           |c S dS )uI  Move a bundled skill's stale copy to its new canonical path.

    When upstream RENAMES or RECATEGORIZES a bundled skill, the manifest key
    (frontmatter name) still matches but ``dest`` is a brand-new path that does
    not exist yet. Without recovery, ``sync_skills()`` falls through to its
    "in manifest but not on disk" branch and misreads the skill as
    *user-deleted*: the old directory is stranded forever and never receives
    another update.

    A stale copy is only moved when it is byte-identical to ``origin_hash`` —
    the hash recorded the last time sync wrote that skill — which proves the
    directory is the copy *we* placed there rather than the user's own work.
    Anything else (user-edited, hub-installed) is left untouched.

    Returns the relative source path when a move happened, else ``None``.
    N     ⚠ z: upstream moved this skill to z, but your modified copy at uD    was kept — it will not receive updates. Run `hermes skills reset z( --restore` to move to the new location.Tr_   z)Could not relocate renamed skill %s -> %srh   z)Relocated renamed bundled skill: %s -> %su     → z (moved u    → ))r   r  r   r&   r   r   r   r  r-   rj   r   r   r   rM   rN   rn   warninginfo)r   r   r   r!  r"  r   r   r   s           r    _recover_renamed_skillr(  |  sD   0  t!%%j"55 # #	I$4$4$6$6	''66??AACC 	 	 	H	 )Y;..  4Z 4 4''66??AA4 4(+4 4 :D4 4 4   	KdT:::KID		2222! 	 	 	NN;Y     444	 	?DQQQ 	hf:ffsff9I9I+--9X9X9a9a9c9cfffggg


4s$   3A..
A;:A;'AD33/E'&E'c                    t                      t          z                                  r| st          d           g g dg g dg ddS t	                      }|                                sg g dg g g dg dS t                                          dd           t                      }t          |          }d |D             }t                      }t                      }g }d}d}	g }
g }g }g }g }d}|D ]\  }}||v r|                    |            t          ||          }t          |          }|                    d	          }|                                r|                                s	 |j                            dd           t!          j        t%          |          t%          |                     t&                              d
|           n4# t*          t,          f$ r  t&                              d|d           Y nw xY w|                                so||v rk|t1                      }t3                      }	t5          ||                    |d          |||	pt9                      |           }|r|                    |           ||v r|                    |           |dz  }| st          d| d           |                                rLt          |          |k    r9t;          |           | st          d|            |                    |d           0||vr	 |                                r7|dz  }t          |          |k    r|||<   nx| st          d| d| d           n_|j                            dd           t!          j        ||           |
                    |           |||<   | st          d|            # t*          t,          f$ r"}| st          d| d|            Y d}~d}~ww xY w|                                r|                    |d          }|r||k    r|dz  }Qt          |          }|s|||<   ||k    r|dz  }n|dz  }ztA          ||          r,|                    |           | st          d| d           ||k    r	 |                    d	          }|                                rt;          |           t!          j        t%          |          t%          |                     	 t!          j        ||           |||<   |                    |           | st          d| d           	 t;          |           n4# t*          t,          f$ r  t&          !                    d|d           Y nw xY wn# t*          t,          f$ r |                                r|                                rE	 t;          |           n4# t*          t,          f$ r  t&                              d|d           Y nw xY w|                                s/t!          j        t%          |          t%          |                      w xY wx# t*          t,          f$ r"}| st          d| d|            Y d}~d}~ww xY w|dz  }|dz  }tE          t9          |#                                          |z
            }|D ]}||= |$                    d          D ]}|%                    |          }t                      |z  }|                                sm	 |j                            dd           t!          j&        ||           o# t*          t,          f$ r&}t&          !                    d ||           Y d}~d}~ww xY wtO          |           tQ          | !          }|
||||||tS          |          ||d"
S )#z
    Sync bundled skills into ~/.hermes/skills/ using the manifest.

    Returns:
        dict with keys: copied (list), updated (list), skipped (int),
                        user_modified (list), cleaned (list), total_bundled (int)
    uJ     (skipped — profile opted out of bundled skills via .no-bundled-skills)r   T)copiedupdatedskippeduser_modifiedcleanedtotal_bundledoptional_provenance_backfilledskipped_opt_out)r*  r+  r,  r-  r.  
suppressedr/  r0  r_   c                     h | ]\  }}|S r.   r.   )rd   r=   rQ   s      r    r   zsync_skills.<locals>.<setcomp>  s    888gdAT888r"   Nz.bakz#Recovered orphaned skill backup: %sz*Could not recover orphaned skill backup %srh   r7   ru   u     ⇢ z7 (deferred to external_dirs, not written to local tree)u     ✓ removed stale shadow of r$  uw   : bundled version shipped but you already have a local skill by this name — yours was kept. Run `hermes skills reset z)` to replace it with the bundled version.z  + z  ! Failed to copy : z  ~ z (user-modified, skipping)u     ↑ z
 (updated)zCould not remove backup %sz.Could not clear partial copy %s during restorez  ! Failed to update zDESCRIPTION.mdzCould not copy %s: %sr   )
r*  r+  r,  r-  r.  r2  	relocatedr/  r0  shadowed_by_external)*r!   NO_BUNDLED_SKILLS_MARKERrH   r  r/   r&   rj   rS   r   r\   rD   r   r   r   with_suffixr-   r   r   r   rn   r'  rM   rN   r&  r  r  r(  r   r:   _rmtree_writablepopr   _is_tracked_user_modificationro   rl   keysr;   r   copy2rr   r   r   ) r   r{   manifestbundled_skillsbundled_namesr2  external_indexr6  r!  r"  r*  r+  r-  suppressed_skippedr5  r,  r   	skill_srcr   bundled_hash_orphan_moved_fromrq   r   	user_hashbackupr.  r=   desc_mdr   	dest_descr0  s                                    r    sync_skillsrK    s	    	1199;; 
 	`^___RAB.0T
 
 	
 #$$K 
RABbST.0
 
 	
 MMt444H-k::N88888M'))J022N&( 59L$(IFGM$&IG!/  
I ##%%j111%i== ++ ""6**>> 		DKKMM 		!!$!>>>CLL#d))444A7KKKKW%   @'!       {{}} 	-x!7!7#355355	0Z,,"SUU K  -  ,,,''
 !''
333qLG 1Z 1 1 1   {{}} /4L!@!@ &&& IG:GGHHHZ...X%%C;;== 3 qLG ,66/;,," GZ G GBLG G G   K%%dT%BBBOIt444MM*---+7HZ(  31Z11222W% C C C CA
AAaAABBBC
 [[]] P	",,z266K  |{::1!$I 	 (1$,,qLGG qLG,[)DD $$Z000 IGGGGHHH {**'I!--f55F
 }} 1(000KD		3v;;777	4888/;,z222$ C!"A:"A"A"ABBB^,V4444 '1 ^ ^ ^"LL)EvX\L]]]]]^#W-    "==?? 
D#{{}} &!&$4T$:$:$:$:(/'9 !& !& !&$*NN(X(,t %3 %& %& %& %& %&!&
 $(;;== D &CKKT C C C   ) I I I  IGjGGAGGHHHI 1 qLGG S))M9::G  TNN $$%566 B B!!+..MMC'	!! 	BB &&td&CCCWi0000W% B B B4gqAAAAAAAAB		B H%B%O%O%O" &(^,,*H 4  s   +A&G.HH#B*OP O==P,A'Y&AVU)(V).VVVVY&9Y!W('Y!(.XY!XAY!!Y&&Z7ZZ51]''^8^^c                    t          |                                           }t                                                      }||j        vrt	          d|d|d          ddlfd}t          j        | |           dS )a  Remove a directory tree, making read-only entries writable first.

    Handles immutable package sources (Nix store, deb/rpm installs) that
    preserve read-only permissions on copied files *and* directories
    (``r-xr-xr-x``).  Removing a child requires write permission on its
    parent directory, so the retry handler makes the failing path **and its
    parent** writable before re-attempting.  See #34860, #34972.
    zrefusing to rmtree z: not strictly under u    (scope guard — see #48200)r   Nc                     t           j                            |          |fD ]-}	 t          j        |j                   # t
          $ r Y *w xY w | |           d S N)r
  rZ   dirnamechmodS_IRWXUrM   )funcr   ri   r   stats       r    	_on_errorz#_rmtree_writable.<locals>._on_error  st     wu--u5 	 	F....   Us   A
AA)onerror)r   r   r&   r`   r   rS  r   rmtree)rZ   r   skills_rootrT  rS  s       @r    r9  r9    s    ( $ZZ!!F--''))K &.((,& , , , , ,
 
 	
 KKK     M$	******r"   c                 .   t                      }t                      }t          |          }t          |          }| |v }| |v }|s|sddd|  dddS d}|rv|sddd|  dddS t	          ||          |          }	|	                                r?	 t          |	           d	}n,# t          t          f$ r}
dd
d|	 d|
 dddcY d}
~
S d}
~
ww xY w|r|| = t          |           t          d	          }|r|r	d}d|  d}n|r	d}d|  d}nd}d|  d}d	|||dS )u?  
    Reset a bundled skill's manifest tracking so future syncs work normally.

    When a user edits a bundled skill, subsequent syncs mark it as
    ``user_modified`` and skip it forever — even if the user later copies
    the bundled version back into place, because the manifest still holds
    the *old* origin hash. This function breaks that loop.

    Args:
        name: The skill name (matches the manifest key / skill frontmatter name).
        restore: If True, also delete the user's copy in the skills dir and let
                 the next sync re-copy the current bundled version. If False
                 (default), only clear the manifest entry — the user's
                 current copy is preserved but future updates work again.

    Returns:
        dict with keys:
          - ok: bool, whether the reset succeeded
          - action: one of "manifest_cleared", "restored", "not_in_manifest",
                    "bundled_missing"
          - message: human-readable description
          - synced: dict from sync_skills() if a sync was triggered, else None
    Fnot_in_manifest'zi' is not a tracked bundled skill. Nothing to reset. (Hub-installed skills use `hermes skills uninstall`.)N)r   actionr   syncedbundled_missingur   ' has no bundled source — manifest entry preserved but cannot restore from bundled (skill was removed upstream).T	not_resetzCould not delete user copy at r4  u3   . Manifest entry preserved — nothing was changed.r   r   z
Restored 'z' from bundled source.z/' (no prior user copy, re-copied from bundled).manifest_clearedzCleared manifest entry for 'zf'. Future `hermes update` runs will re-baseline against your current copy and accept upstream changes.)rS   r/   r   r   r   rH   r9  rM   rN   rr   rK  )r=   r   r>  r{   r?  bundled_by_namein_manifest
is_bundleddeleted_user_copyr   rq   r\  r[  r   s                 r    reset_bundled_skillrd    sG   0 H"$$K-k::N>**O("K(J 	
z 	
'ID I I I 
 
 	
   		+U U U U    &od&;[II;;== 	 &&&$(!!W% 	 	 	)M M M M M M #       	  "TN!!! t$$$F 
$ 
;t;;;	 	
TtTTT#W4 W W W 	
 &WOOOs   B   C	1C>C	C	rG  c                 ,    t          |           o|| k    S )a  Whether an on-disk skill counts as a user modification ``hermes update`` keeps.

    Shared by the sync loop (which decides what to skip) and
    ``list_user_modified_bundled_skills`` (which surfaces the names) so the two
    can never drift. A skill is a tracked modification only when it has a
    recorded origin hash (an un-baselined / v1 entry with an empty hash is not)
    and its current content hash differs from that origin.
    )bool)r   rG  s     r    r;  r;  f  s     9k!99r"   c                     t                      } | sg S t                      }g }t          |          D ]y\  }}|                     |d          }|st	          ||          }|                                sCt          |t          |                    r|                    |||d           z|	                    d            |S )u  Return the bundled skills that ``hermes update`` keeps because the user
    edited them locally.

    A skill counts as user-modified when its on-disk copy no longer matches the
    origin hash recorded in the manifest the last time it was synced — the exact
    same test the sync loop uses to decide what to skip. This is the discovery
    half of that behavior, so a user can find the names the ``~ N user-modified
    (kept)`` notice only counts.

    Returns a list (sorted by name) of dicts:
        ``{"name": str, "dest": Path, "bundled_src": Path}``
    where ``dest`` is the user's copy and ``bundled_src`` is the current stock
    copy (so callers can diff or restore).
    r7   )r=   r   bundled_srcc                     | d         S )Nr=   r.   )rq   s    r    r   z3list_user_modified_bundled_skills.<locals>.<lambda>  s
    &	 r"   r   )
rS   r/   r   r   r   rH   r;  r   r   sort)r>  r{   modifiedr   rB   r   r   s          r    !list_user_modified_bundled_skillsrl  r  s     H 	"$$KH!9+!F!F  
Ill:r22  	%i=={{}} 	(iooFF 	OO#T)LL   MM))M***Or"   c                     	 |                                  }n# t          $ r Y dS w xY wd|v r|dfS 	 ||                    d          fS # t          $ r |dfcY S w xY w)zRead a file once for diffing.

    Returns ``(raw_bytes, text)`` where ``text`` is ``None`` if the file is
    binary; ``(None, None)`` if it could not be read. Returning the raw bytes
    lets the caller compare binary files without re-reading them.
    NN    Nr   )r   rM   decodeUnicodeDecodeError)rZ   rp   s     r    _read_for_diffrr    s         zz$TzT[[))))   Tzs    
%%A AAc                 H   ddl }t                      }t          t          |                    }|                    |           }|d| ddg d|  ddS t          ||          }|                                sd| ddg d|  d	| d
dS t          t          |                    }t          t          |                    }g }t          ||z            D ]}	|	|v }
|	|v }|
rt          ||	z            nd\  }}|rt          ||	z            nd\  }}|
r|r|| ||k    r|                    |	ddd           e||k    rld                    |                    |                    d          |                    d          d|	 d|	                     }|                    |	d|d           |
r|                    |	dd|	 d           |                    |	dd|	 d           t          |          }d| d|||sd|  dnd|  dt!          |           ddS )u8  Diff a user's copy of a bundled skill against the current stock version.

    Lets a user see exactly what diverged before deciding whether to keep their
    edits or ``hermes skills reset`` back to upstream.

    Returns a dict:
        ``ok`` (bool), ``name`` (str), ``found`` (bool — bundled source exists),
        ``modified`` (bool), ``message`` (str),
        ``diffs``: list of ``{"path": str, "status": str, "diff": str}`` where
        status is one of ``modified`` / ``added`` (only in user copy) /
        ``removed`` (only in bundled) / ``binary``.
    r   NFrZ  zv' is not a tracked bundled skill (no stock version to diff against). Hub-installed skills use `hermes skills inspect`.)r   r=   foundrk  diffsr   TzNo local copy of 'z' found at r   rn  binaryz<binary file differs>)rZ   statusdiffr7   )keependszstock/zyours/)fromfiletofilerk  addedz+ only in your copy: removedz- only in stock: z' matches the stock version.z$' differs from the stock version in z	 file(s).)difflibr/   r   r   r   r   rH   r:   r   rl   rr  r   rk   unified_diffrJ   rf  r   )r=   r~  r{   r`  rh  r   
user_filesstock_filesru  r   in_userin_stock
user_bytes	user_textstock_bytes
stock_texttextrk  s                     r    diff_bundled_skillr    s4    NNN"$$K3K@@AAO!%%d++KTD T T T

 

 
	
 "+{;;D;;== 
DDDDTDDD
 
 	
 %d++,,J&{3344KEj;.// % %#+%*1CN4#:&&&| 	
I 2:KN;,---| 	 Z  	x 	 J$6 ,,LL!$BYZZ   J&&77$$))4)88(($(77+c^^)C>>	 %   D LL#TJJKKKK 	LL9VQT9V9VWW    LL	;Ts;T;TUU    E{{H U22222TTTTs5zzTTT  r"   enabledc                    t                      t          z  }|                                }	 | rDt                                          dd           |                    dd           | }|rdnd}n|r|                                 |}|rdnd	}n1# t          $ r$}d
d
t          |          d| d| dcY d}~S d}~ww xY wd|t          |          |dS )aj  Toggle the .no-bundled-skills opt-out marker for the active profile.

    When ``enabled`` is True, writes HERMES_HOME/.no-bundled-skills so the
    installer, ``hermes update``, and any direct sync stop seeding bundled
    skills. When False, removes the marker so seeding resumes on the next
    sync. This is the on-disk-state half of ``hermes skills opt-out`` /
    ``opt-in``; removal of already-present skills is a separate, explicit
    step (see ``remove_pristine_bundled_skills``).

    Returns:
        dict with keys: ok (bool), changed (bool), marker (str path),
                        message (str).
    Tr_   zThis profile opted out of bundled-skill seeding (`hermes skills opt-out`).
Delete this file to re-enable sync on the next `hermes update`.
r   rF   zpOpted out of bundled skills. Future install / update / sync runs will not seed bundled skills into this profile.u1   Already opted out — marker was already present.zgOpted back in. The next `hermes update` (or `hermes skills opt-in --sync`) will re-seed bundled skills.u&   Not opted out — no marker to remove.Fz#Could not update opt-out marker at r4  )r   r  markerr   N)r!   r7  rH   rj   
write_textr  rM   r   )r  r  existedr  r   rq   s         r    set_bundled_skills_opt_outr    s]    ^^66FmmooG
 	NN   ===T !	     "kG I G G I	 G   G > ? ? >	   
 
 
ES[[JVJJqJJ
 
 	
 	
 	
 	
 	
 	


 7c&kkgVVVs   A$B 
B?B:4B?:B?c                  R    t                      t          z                                  S )z=Return True if the active profile carries the opt-out marker.)r!   r7  rH   r.   r"   r    is_bundled_skills_opt_outr  ?  s    NN55==???r"   dry_runc                 l   t                      }t                      }t          t          |                    }g }g }t	          |                                          D ]\  }}|                    |          }||                    |dd           6t          ||          }	|		                                s
| s||v r||= dt          |	          }
|
|k    r|                    |dd           | r|                    |           	 t          |	           n9# t          t          f$ r%}|                    |d| d           Y d}~d}~ww xY w||v r||= |                    |           | s|rt          |           | rdnd}| dt          |           d	t          |           d
}d||| |dS )a  Delete bundled skills that are present, manifest-tracked, AND unmodified.

    Safety is the whole point of this function. A skill on disk is removed
    ONLY when all of these hold:
      - it is recorded in the sync manifest (so it is genuinely a bundled
        skill, not a hub-installed or hand-written one), AND
      - it still exists in the bundled source (so we can hash-compare), AND
      - its on-disk copy is byte-identical to the manifest origin hash
        (so the user has not edited it).

    Anything user-modified, hub-installed, or locally authored is left
    untouched and reported under ``skipped``. The manifest entry for each
    removed skill is dropped so a later opt-in re-seed treats it as new.

    Args:
        dry_run: When True, compute what would be removed without deleting.

    Returns:
        dict with keys: ok (bool), removed (list[str]),
                        skipped (list[dict]) where each dict is
                        {name, reason}, dry_run (bool), message (str).
    Nz$no bundled source (removed upstream))r=   reasonzuser-modified (kept)zdelete failed: zWould removeRemoved z! pristine bundled skill(s); kept r   T)r   r}  r,  r  r   )rS   r/   r   r   rl   rm   r   r   r   rH   r   r9  rM   rN   rr   r   )r  r>  r{   r`  r}  r,  r=   r   r   r   on_diskrq   verbr   s                 r    remove_pristine_bundled_skillsr  D  s#   . H"$$K3K@@AAOGG#HNN$4$455  k!!$'';NND4Z[[\\\%c;77{{}} 	 #tx//TND//k!!NND4JKKLLL 	NN4   	T""""! 	 	 	NND4Ia4I4IJJKKKHHHH	 8t "w "!!!$3>>)DVVGVVs7||VVVGw7w  s   	DE*E

E__main__z1Syncing bundled skills into ~/.hermes/skills/ ...r   r*  z newr+  z updatedr,  z
 unchangedr-     z, z, +z morez user-modified (kept): r.  z cleaned from manifestr0  z official optional backfilledz
Done: z. r/  z total bundled.rN  )F)^__doc__r   r  loggingr
  r   sysr   r   pathlibr   r   stdoutstderr_streamhasattrr   r   	TypeErrorhermes_constantsr   r   r   r8   r   typingr   r   r   r   r   utilsr   r   	getLogger__name__rn   r   r$   r(   r   r%   r)   r!   r&   r*   r7  r/   r2   r   rD   rS   r:   r\   rr   r>   r   r   r   r   r   r   r   r   rf  r   r   r   r   r   r  r  r(  rK  r9  rd  r;  rl  bytesrr  r  r  r  r  r  rO   r   r   r[   MAX_SHOWrk   shownr   r   r.   r"   r    <module>r     s[	   .    				  



 ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' ' 
CJ'  Gww&& 	CCCCI& 	 	 	D	
 ^ ] ] ] ] ] ] ] ] ] 4 4 4 4 4 4 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3		8	$	$ o8#
00 % " ( d    %T % % % %/ / / / / 0 K$ K K K KU4 U U U U
SX    :S#X    6    4cT#s(^ c c c c,t s s    *$ 4c4i8H3I    2d  $     #     T c     c    	$T 	$c 	$ 	$ 	$ 	$tCsC~)>$>?    :$ T c     CH D D D# D4 DD D D D DNT#tDz/-B    ( 8< d3T
?34 d^   .m m m$s) m m m m`S    0d3T
?3    $??? ? sDJ'	?
 3x? ? c]? ? ? ?DQ Qt Q Q Q Q Qh.+4 .+D .+ .+ .+ .+b`P `Pc `PD `PT `P `P `P `PF	:s 	:s 	:t 	: 	: 	: 	:"4: " " " "J %#(F"G    &`S `T ` ` ` `F/W /W /W /W /W /Wd@4 @ @ @ @
C CD CT C C C CL z	E
=>>>[u%%%F3vh  &&&3vi !!+++)(((E
 o D'		%		*++3u::  733u::07777EE

BB5BBCCCi HF9-..FFFGGGzz233 fF#CDEEdddeee	E
QTYYu%%
Q
Q)@
Q
Q
QRRRRR' s   A  A*)A*