§
    ÷žyj#t  ã                  ó˜  — d Z ddlmZ ddlZddlZddlZddlZddlZddlmZm	Z	 ddl
mZ ddlmZmZmZmZmZmZ ddlmZ ddlmZ dd	lmZ  ej        e¦  «        Zd
ZddhZ ej        d¦  «        ZdBd„Z dBd„Z!dBd„Z"dZ#dCd„Z$dDdEd„Z%dFd„Z&dGd„Z'dHd„Z(dId!„Z)	 dDdJd(„Z*dKdd*œdLd.„Z+dDdMd2„Z,dNd4„Z-dOd6„Z.dPd9„Z/dQd;„Z0dRd>„Z1dDdSd@„Z2dTdA„Z3dS )UuÀ  Curator snapshot + rollback.

A pre-run snapshot of ``~/.hermes/skills/`` (excluding ``.curator_backups/``
itself) is taken before any mutating curator pass. Snapshots are tar.gz
files under ``~/.hermes/skills/.curator_backups/<utc-iso>/`` with a
companion ``manifest.json`` describing the snapshot (reason, time, size,
counted skill files). Rollback picks a snapshot, moves the current
``skills/`` tree aside into another snapshot so even the rollback itself
is undoable, then extracts the chosen snapshot into place.

The snapshot does NOT include:
  - ``.curator_backups/`` (would recurse)
  - ``.hub/`` (hub-installed skills â€” managed by the hub, not us)

It DOES include:
  - all SKILL.md files + their directories (``scripts/``, ``references/``,
    ``templates/``, ``assets/``)
  - ``.usage.json`` (usage telemetry â€” needed to rehydrate state cleanly)
  - ``.archive/`` (so rollback restores previously-archived skills too)
  - ``.curator_state`` (so rolling back also restores the last-run-at
    pointer â€” otherwise the curator would immediately re-fire on the next
    tick)
  - ``.bundled_manifest`` (so protection markers stay consistent)
  - ``.curator_suppressed`` (so rollback restores the set of pruned built-ins
    the re-seeder must leave archived)

Alongside the skills tarball, each snapshot also captures a copy of
``~/.hermes/cron/jobs.json`` as ``cron-jobs.json`` when it exists. Cron
jobs reference skills by name in their ``skills``/``skill`` fields; the
curator's consolidation pass rewrites those in place via
``cron.jobs.rewrite_skill_refs()``. Without capturing the pre-run state,
rolling back the skills tree would leave cron jobs pointing at the
umbrella skills even though the narrow skills they were originally
configured with have been restored. We store the whole jobs.json for
fidelity but rollback only touches the ``skills``/``skill`` fields â€” the
rest (schedule, next_run_at, enabled, prompt, etc.) is live state and
we leave it alone.
é    )ÚannotationsN)ÚdatetimeÚtimezone)ÚPath)ÚAnyÚDictÚListÚOptionalÚSetÚTuple©Úget_hermes_home©Úis_excluded_skill_path)Úformat_bytesé   ú.curator_backupsz.hubz/^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}Z(-\d{2})?$Úreturnr   c                 ó*   — t          ¦   «         dz  dz  S )NÚskillsr   r   © ó    ú:/home/ragecks/.hermes/hermes-agent/agent/curator_backup.pyÚ_backups_dirr   G   s   € ÝÑÔ˜xÑ'Ð*<Ñ<Ð<r   c                 ó$   — t          ¦   «         dz  S )Nr   r   r   r   r   Ú_skills_dirr   K   s   € ÝÑÔ˜xÑ'Ð'r   c                 ó*   — t          ¦   «         dz  dz  S )zHSource path for the live cron jobs store (``~/.hermes/cron/jobs.json``).Úcronz	jobs.jsonr   r   r   r   Ú_cron_jobs_filer   O   s   € åÑÔ˜vÑ%¨Ñ3Ð3r   zcron-jobs.jsonÚdestúDict[str, Any]c                ó8  — t          ¦   «         }dddœ}|                     ¦   «         sd|d<   |S 	 |                     d¬¦  «        }n<# t          $ r/}t                               d|¦  «         d	|› �|d<   |cY d
}~S d
}~ww xY w	 t          j        |¦  «        }t          |t          ¦  «        r=| 
                    d¦  «        }t          |t          ¦  «        rt          |¦  «        |d<   n't          |t          ¦  «        rt          |¦  «        |d<   n&# t          j        t          f$ r d|d<   d|d<   Y nw xY w	 | t          z                       |d¬¦  «         n<# t          $ r/}t                               d|¦  «         d|› �|d<   |cY d
}~S d
}~ww xY wd|d<   |S )u’  Copy the live cron jobs.json into ``dest`` as ``cron-jobs.json``.

    Returns a small dict describing what was captured so the caller can
    fold it into the manifest. Never raises â€” if the cron file is missing
    or unreadable, the return dict has ``backed_up=False`` and the reason,
    and the snapshot proceeds without cron data (the snapshot is still
    useful for rolling back skills).
    Fr   ©Ú	backed_upÚ
jobs_countzno cron/jobs.json presentÚreasonz	utf-8-sig©Úencodingz,Failed to read cron/jobs.json for backup: %szread error: NÚjobsr%   z-jobs.json was not valid JSON at snapshot timeÚparse_warningúutf-8z$Failed to write cron backup file: %szwrite error: Tr$   )r   ÚexistsÚ	read_textÚOSErrorÚloggerÚdebugÚjsonÚloadsÚ
isinstanceÚdictÚgetÚlistÚlenÚJSONDecodeErrorÚ	TypeErrorÚCRON_JOBS_FILENAMEÚ
write_text)r    ÚsrcÚinfoÚrawÚeÚparsedÚinners          r   Ú_backup_cron_jobs_intorB   W   só  € õ Ñ
Ô
€CØ).¸aÐ@Ð@€DØ�:Š:‰<Œ<ð Ø4ˆˆX‰Øˆð
ð �mŠm [ˆmÑ1Ô1ˆˆøÝð ð ð Ý�ŠÐCÀQÑGÔGÐGØ+¨Ð+Ð+ˆˆX‰Øˆˆˆˆˆˆøøøøðøøøð
PÝ”˜C‘”ˆÝ�f�dÑ#Ô#ð 	-Ø—J’J˜vÑ&Ô&ˆEÝ˜%¥Ñ&Ô&ð 0Ý%(¨¡Z¤Z��\Ñ"øÝ˜¥Ñ%Ô%ð 	-Ý!$ V¡¤ˆD�ÑøøÝÔ ¥)Ð,ð Pð Pð PØˆˆ\ÑØ Oˆˆ_ÑÐÐðPøøøðØ	Õ"Ñ	"×.Ò.¨s¸WÐ.ÑEÔEÐEÐEøÝð ð ð Ý�ŠÐ;¸QÑ?Ô?Ð?Ø,¨Ð,Ð,ˆˆX‰Øˆˆˆˆˆˆøøøøðøøøð €DˆÑØ€KsN   °A Á
B Á$A;Á5B Á;B ÂBD Ä D5Ä4D5Ä9E Å
FÅ#$FÆFÆFÚnowúOptional[datetime]Ústrc                ó  — | €t          j        t          j        ¦  «        } |                      d¬¦  «                             ¦   «         }|                     d¦  «        r
|dd…         }|                     dd¦  «        dz   S )	z@UTC ISO-ish filesystem-safe timestamp: ``2026-05-01T13-05-42Z``.Nr   )Úmicrosecondz+00:00iúÿÿÿÚ:Ú-ÚZ)r   rC   r   ÚutcÚreplaceÚ	isoformatÚendswith)rC   Úss     r   Ú_utc_idrP   Š   sq   € à
€{ÝŒl�8œ<Ñ(Ô(ˆà�Š ˆÑ"Ô"×,Ò,Ñ.Ô.€AØ‡z‚z�(ÑÔð ØˆcˆrˆcŒFˆØ�9Š9�S˜#ÑÔ Ñ$Ð$r   c                 óx  — 	 ddl m}   | ¦   «         }n4# t          $ r'}t                               d|¦  «         i cY d }~S d }~ww xY wt          |t          ¦  «        si S |                     d¦  «        pi }t          |t          ¦  «        si S |                     d¦  «        pi }t          |t          ¦  «        r|ni S )Nr   )Úload_config_readonlyz,Failed to load config for curator backup: %sÚcuratorÚbackup)Úhermes_cli.configrR   Ú	Exceptionr/   r0   r3   r4   r5   )rR   Úcfgr?   ÚcurÚbks        r   Ú_load_configrZ   •   sá   € ðØ:Ð:Ð:Ð:Ð:Ð:Ø"Ð"Ñ$Ô$ˆˆøÝð ð ð Ý�ŠÐCÀQÑGÔGÐGØˆ	ˆ	ˆ	ˆ	ˆ	ˆ	øøøøðøøøõ �c�4Ñ Ô ð Øˆ	Ø
�'Š'�)Ñ
Ô
Ð
" €CÝ�c�4Ñ Ô ð Øˆ	Ø	�Š�Ñ	Ô	Ð	 ˜b€BÝ˜B¥Ñ%Ô%Ð-ˆ2ˆ2¨2Ð-s   ‚ “
A�?¹A¿AÚboolc                 ó`   — t          t          ¦   «                              dd¦  «        ¦  «        S )uB   Default ON â€” the whole point of the backup is safety by default.ÚenabledT)r[   rZ   r5   r   r   r   Ú
is_enabledr^   ¥   s$   € å•‘”×"Ò" 9¨dÑ3Ô3Ñ4Ô4Ð4r   Úintc                 óÎ   — t          ¦   «         } 	 t          |                      dt          ¦  «        ¦  «        }n# t          t
          f$ r
 t          }Y nw xY wt          d|¦  «        S )NÚkeepé   )rZ   r_   r5   ÚDEFAULT_KEEPr9   Ú
ValueErrorÚmax)rW   Úns     r   Úget_keeprg   ª   sd   € Ý
‰.Œ.€CðÝ�—’˜¥Ñ-Ô-Ñ.Ô.ˆˆøÝ•zÐ"ð ð ð Ýˆˆˆðøøøåˆq�!‰9Œ9Ðs   �(9 ¹AÁAÚbasec                ó~   — 	 t          d„ |                      d¦  «        D ¦   «         ¦  «        S # t          $ r Y dS w xY w)Nc              3  ó8   K  — | ]}t          |¦  «        °d V — ŒdS )rb   Nr   )Ú.0Úps     r   ú	<genexpr>z%_count_skill_files.<locals>.<genexpr>¹   sD   è è € ð 
ð 
ØÕ5KÈAÑ5NÔ5Nð
Øð
ð 
ð 
ð 
ð 
ð 
r   zSKILL.mdr   )ÚsumÚrglobr.   )rh   s    r   Ú_count_skill_filesrp   ·   se   € ðÝð 
ð 
Ø—z’z *Ñ-Ô-ð
ñ 
ô 
ñ 
ô 
ð 	
øõ ð ð ð Øˆqˆqðøøøs   ‚+. ®
<»<r&   Úarchive_pathÚskills_countedÚ	cron_infoúOptional[Dict[str, Any]]ÚNonec                ó`  — | j         |t          j        t          j        ¦  «                             ¦   «         |j         |                     ¦   «         j        |dœ}|�¤t          | 	                    dd¦  «        ¦  «        t          | 	                    dd¦  «        ¦  «        dœ|d<   | 	                    d¦  «        s| 	                    dd	¦  «        |d         d<   | 	                    d
¦  «        r|d
         |d         d
<   | dz                       t          j        |dd¬¦  «        d¬¦  «         d S )N)Úidr&   Ú
created_atÚarchiveÚarchive_bytesÚskill_filesr$   Fr%   r   r#   Ú	cron_jobsr&   znot capturedr*   úmanifest.jsoné   T)ÚindentÚ	sort_keysr+   r'   )Únamer   rC   r   rK   rM   ÚstatÚst_sizer[   r5   r_   r;   r1   Údumps)r    r&   rq   rr   rs   Úmanifests         r   Ú_write_manifestr†   À   s8  € ð ŒiØÝ”l¥8¤<Ñ0Ô0×:Ò:Ñ<Ô<ØÔ$Ø%×*Ò*Ñ,Ô,Ô4Ø%ðð €Hð Ðå˜iŸmšm¨K¸Ñ?Ô?Ñ@Ô@Ý˜iŸmšm¨L¸!Ñ<Ô<Ñ=Ô=ð!
ð !
ˆ�Ñð �}Š}˜[Ñ)Ô)ð 	VØ.7¯mªm¸HÀnÑ.UÔ.UˆH�[Ô! (Ñ+Ø�=Š=˜Ñ)Ô)ð 	PØ5>¸Ô5OˆH�[Ô! /Ñ2Ø	ˆOÑ×'Ò'ÝŒ
�8 A°Ð6Ñ6Ô6Àð (ñ ô ð ð ð r   Úmanual)Úprotect_idsrˆ   úOptional[Set[str]]úOptional[Path]c               ó–  — t          ¦   «         st                               d¦  «         dS t          ¦   «         }|                     ¦   «         st                               d¦  «         dS t          ¦   «         }	 |                     dd¬¦  «         n4# t          $ r'}t                               d||¦  «         Y d}~dS d}~ww xY wt          ¦   «         }|}d}||z                       ¦   «         r$|› d|d	›�}|dz  }||z                       ¦   «         °$||z  }	 |                     dd
¬¦  «         n4# t          $ r'}t                               d||¦  «         Y d}~dS d}~ww xY w|dz  }		 t          j
        |	dd¬¦  «        5 }
t          |                     ¦   «         ¦  «        D ];}|j        t          v rŒ|
                     t!          |¦  «        |j        d¬¦  «         Œ<	 ddd¦  «         n# 1 swxY w Y   t#          |¦  «        }t%          || |	t'          |¦  «        |¬¦  «         ni# t          t          j        f$ rP}t                               d|d¬¦  «         	 t+          j        |d¬¦  «         n# t          $ r Y nw xY wY d}~dS d}~ww xY wt/          t1          ¦   «         |¬¦  «         t                               d|| ¦  «         |S )u.  Create a tar.gz snapshot of ``~/.hermes/skills/`` and prune old ones.

    Returns the snapshot directory path, or ``None`` if the snapshot was
    skipped (backup disabled, skills dir missing, or an IO error occurred â€”
    in which case we log at debug and return None so the curator never
    aborts a pass because of a backup failure).

    ``protect_ids`` is forwarded to the prune step so callers can guarantee
    specific snapshot ids survive even when they fall outside the keep
    window (rollback passes the id it is about to restore from).
    z4Curator backup disabled by config; skipping snapshotNu5   No ~/.hermes/skills/ directory â€” nothing to back upT©ÚparentsÚexist_okz#Failed to create backups dir %s: %srb   rI   Ú02dFz$Failed to create snapshot dir %s: %súskills.tar.gzzw:gzé   )Úcompresslevel)ÚarcnameÚ	recursive)rs   zCurator snapshot failed: %s©Úexc_info©Úignore_errors)ra   Úprotectz!Curator snapshot created: %s (%s))r^   r/   r0   r   r,   r   Úmkdirr.   rP   ÚtarfileÚopenÚsortedÚiterdirr�   Ú_EXCLUDE_TOP_LEVELÚaddrE   rB   r†   rp   ÚTarErrorÚshutilÚrmtreeÚ
_prune_oldrg   r=   )r&   rˆ   r   Úbackupsr?   Úbase_idÚsnap_idÚcounterr    ry   ÚtfÚentryrs   s                r   Úsnapshot_skillsr«   Ù   s�  € õ ‰<Œ<ð Ý�ŠÐKÑLÔLÐLØˆtå‰]Œ]€FØ�=Š=‰?Œ?ð Ý�ŠÐLÑMÔMÐMØˆtå‰nŒn€GðØ�Š˜d¨TˆÑ2Ô2Ð2Ð2øÝð ð ð Ý�ŠÐ:¸GÀQÑGÔGÐGØˆtˆtˆtˆtˆtøøøøðøøøõ ‰iŒi€GØ€GØ€GØ�WÑ×
$Ò
$Ñ
&Ô
&ð ØÐ,Ð,˜wÐ,Ð,Ð,ˆØ�1‰ˆð �WÑ×
$Ò
$Ñ
&Ô
&ð ð �WÑ€DðØ�
Š
˜4¨%ˆ
Ñ0Ô0Ð0Ð0øÝð ð ð Ý�ŠÐ;¸TÀ1ÑEÔEÐEØˆtˆtˆtˆtˆtøøøøðøøøð �_Ñ$€GðåŒ\˜' 6¸Ð;Ñ;Ô;ð 	G¸rÝ §¢Ñ 0Ô 0Ñ1Ô1ð Gð G�Ø”:Õ!3Ð3Ð3Øð —’•s˜5‘z”z¨5¬:À�ÑFÔFÐFÐFðGð	Gð 	Gð 	Gñ 	Gô 	Gð 	Gð 	Gð 	Gð 	Gð 	Gð 	Gøøøð 	Gð 	Gð 	Gð 	Gõ +¨4Ñ0Ô0ˆ	Ý˜˜f gÝ*¨6Ñ2Ô2Ø"+ð	-ñ 	-ô 	-ð 	-ð 	-øõ •WÔ%Ð&ð ð ð Ý�ŠÐ2°AÀˆÑEÔEÐEð	ÝŒM˜$¨dÐ3Ñ3Ô3Ð3Ð3øÝð 	ð 	ð 	ØˆDð	øøøàˆtˆtˆtˆtˆtøøøøðøøøõ •H‘J”J¨Ð4Ñ4Ô4Ð4Ý
‡K‚KÐ3°W¸fÑEÔEÐEØ€Ks‘   Á8B Â
CÂB<Â<CÄD/ Ä/
E Ä9EÅE Å)H' Æ AG+ÇH' Ç+G/Ç/H' Ç2G/Ç33H' È'JÈ=JÉI2É1JÉ2
I?É<JÉ>I?É?JÊJra   r™   ú	List[str]c                ój  — |pt          ¦   «         }t          ¦   «         }|                     ¦   «         sg S g }g }|                     ¦   «         D ]‚}|                     ¦   «         sŒ|j                             d¦  «        r|                     |¦  «         ŒGt           	                    |j        ¦  «        r|                     |j        |f¦  «         Œƒ| 
                    d„ d¬¦  «         g }|| d…         D ]q\  }}|j        |v rŒ	 t          j        |¦  «         |                     |j        ¦  «         Œ?# t          $ r&}	t                               d||	¦  «         Y d}	~	Œjd}	~	ww xY w|D ]J}	 t          j        |¦  «         Œ# t          $ r&}	t                               d||	¦  «         Y d}	~	ŒCd}	~	ww xY w|S )u•  Delete regular snapshots beyond the newest *keep*. Returns deleted
    ids. Snapshot ids in *protect* are never deleted even when they fall
    outside the keep window â€” rollback() uses this so the mandatory
    pre-rollback safety snapshot can never evict the very snapshot being
    restored. Staging dirs (``.rollback-staging-*``) are implementation
    detail and pruned independently on every call.ú.rollback-staging-c                ó   — | d         S )Nr   r   )Úts    r   ú<lambda>z_prune_old.<locals>.<lambda>?  s
   € ˜q œt€ r   T)ÚkeyÚreverseNzFailed to prune %s: %sz(Failed to clean stale staging dir %s: %s)Úsetr   r,   rž   Úis_dirr�   Ú
startswithÚappendÚ_ID_REÚmatchÚsortr¢   r£   r.   r/   r0   )
ra   r™   r¥   ÚentriesÚstale_stagingÚchildÚdeletedÚ_Úpathr?   s
             r   r¤   r¤   &  sû  € ð Ð�™œ€GÝ‰nŒn€GØ�>Š>ÑÔð Øˆ	Ø&(€GØ "€MØ—’Ñ"Ô"ð 
0ð 
0ˆØ�|Š|‰~Œ~ð 	ØØŒ:× Ò Ð!5Ñ6Ô6ð 	ð × Ò  Ñ'Ô'Ð'ØÝ�<Š<˜œ
Ñ#Ô#ð 	0Ø�NŠN˜EœJ¨Ð.Ñ/Ô/Ð/øà‡L‚L�^�^¨T€LÑ2Ô2Ð2Ø€GØ˜4˜5˜5”>ð <ð <‰ˆˆ4ØŒ9˜ÐÐØð	<ÝŒM˜$ÑÔÐØ�NŠN˜4œ9Ñ%Ô%Ð%Ð%øÝð 	<ð 	<ð 	<Ý�LŠLÐ1°4¸Ñ;Ô;Ð;Ð;Ð;Ð;Ð;Ð;øøøøð	<øøøàð Nð Nˆð	NÝŒM˜$ÑÔÐÐøÝð 	Nð 	Nð 	NÝ�LŠLÐCÀTÈ1ÑMÔMÐMÐMÐMÐMÐMÐMøøøøð	Nøøøà€Ns0   Ä.D3Ä3
E#Ä=EÅE#Å+F Æ 
F0Æ
F+Æ+F0Úsnap_dirc                óÆ   — | dz  }|                      ¦   «         si S 	 t          j        |                     d¬¦  «        ¦  «        S # t          t          j        f$ r i cY S w xY w)Nr}   r+   r'   )r,   r1   r2   r-   r.   r8   )rÁ   Úmfs     r   Ú_read_manifestrÄ   U  sp   € Ø	�OÑ	#€BØ�9Š9‰;Œ;ð Øˆ	ðÝŒz˜"Ÿ,š,°˜,Ñ8Ô8Ñ9Ô9Ð9øÝ•TÔ)Ð*ð ð ð Øˆ	ˆ	ˆ	ðøøøs   �'A ÁA ÁA úList[Dict[str, Any]]c                 ót  — t          ¦   «         } |                      ¦   «         sg S g }t          |                      ¦   «         d¬¦  «        D ]í}|                     ¦   «         sŒt
                               |j        ¦  «        sŒ7|dz                       ¦   «         sŒOt          |¦  «        }| 	                    d|j        ¦  «         | 	                    dt          |¦  «        ¦  «         d|vr8|dz  }	 |                     ¦   «         j        |d<   n# t          $ r d|d<   Y nw xY w|                     |¦  «         Œî|S )uè   Return all restorable snapshots, newest first. Only entries with a
    real ``skills.tar.gz`` tarball are listed â€” transient
    ``.rollback-staging-*`` directories created mid-rollback are
    implementation detail and not shown.T©r³   r�   rw   rÀ   rz   r   )r   r,   r�   rž   rµ   r¸   r¹   r�   rÄ   Ú
setdefaultrE   r‚   rƒ   r.   r·   )r¥   Úoutr½   rÃ   Úarcs        r   Úlist_backupsrË   _  sG  € õ
 ‰nŒn€GØ�>Š>ÑÔð Øˆ	Ø "€CÝ˜ŸšÑ)Ô)°4Ð8Ñ8Ô8ð ð ˆØ�|Š|‰~Œ~ð 	ØÝ�|Š|˜EœJÑ'Ô'ð 	ØØ˜Ñ'×/Ò/Ñ1Ô1ð 	ØÝ˜EÑ"Ô"ˆØ
�Š�d˜EœJÑ'Ô'Ð'Ø
�Š�f�c %™jœjÑ)Ô)Ð)Ø "Ð$Ð$Ø˜/Ñ)ˆCð(Ø&)§h¢h¡j¤jÔ&8��?Ñ#Ð#øÝð (ð (ð (Ø&'��?Ñ#Ð#Ð#ð(øøøà�
Š
�2‰ŒˆˆØ€Js   Ã0DÄDÄDÚ	backup_idúOptional[str]c                ó\  — t          ¦   «         }|                     ¦   «         sdS | rN|| z  }|                     ¦   «         r3t                               | ¦  «        r|dz                       ¦   «         r|S dS d„ t          |                     ¦   «         d¬¦  «        D ¦   «         }|r|d         ndS )zpReturn the path of the requested backup, or the newest one if
    *backup_id* is None. Returns None if no match.Nr�   c                ó¦   — g | ]N}|                      ¦   «         ¯t                               |j        ¦  «        ¯5|d z                       ¦   «         ¯L|‘ŒOS )r�   )rµ   r¸   r¹   r�   r,   )rk   Úcs     r   ú
<listcomp>z#_resolve_backup.<locals>.<listcomp>‹  se   € ð ð ð ØØ�8Š8‰:Œ:ðå Ÿ,š, q¤vÑ.Ô.ðà45¸Ñ4G×3OÒ3OÑ3QÔ3QðØ	ðð ð r   TrÇ   r   )r   r,   rµ   r¸   r¹   r�   rž   )rÌ   r¥   ÚtargetÚ
candidatess       r   Ú_resolve_backuprÔ   |  sË   € õ ‰nŒn€GØ�>Š>ÑÔð ØˆtØð Ø˜9Ñ$ˆà�MŠM‰OŒOð	å—’˜YÑ'Ô'ð	ð ˜/Ñ)×1Ò1Ñ3Ô3ð	ð
 ˆMØˆtðð Ý˜'Ÿ/š/Ñ+Ô+°TÐ:Ñ:Ô:ðñ ô €Jð 'Ð0ˆ:�aŒ=ˆ=¨DÐ0r   Úsnapshot_dirc           
     óè  — dg g dddœ}| t           z  }|                     ¦   «         sdt           › �|d<   |S 	 |                     d¬¦  «        }t          j        |¦  «        }n-# t
          t          j        f$ r}d	|› �|d<   |cY d}~S d}~ww xY wt          |t          ¦  «        r| 	                    d
¦  «        }nt          |t          ¦  «        r|}nd}t          |t          ¦  «        sd|d<   |S i }|D ]‰}t          |t          ¦  «        sŒ| 	                    d¦  «        }	t          |	t          ¦  «        r|	sŒE| 	                    d¦  «        | 	                    d¦  «        | 	                    d¦  «        p|	dœ||	<   ŒŠ|sd|d<   |S 	 ddlm}
m}m} n!# t           $ r}d|› �|d<   |cY d}~S d}~ww xY wd|d<   	  |¦   «         5   |
¦   «         }d}t#          ¦   «         }|D �]]}t          |t          ¦  «        sŒ| 	                    d¦  «        }	t          |	t          ¦  «        r|	sŒF|                     |	¦  «         | 	                    |	¦  «        }|€Œs| 	                    d¦  «        }| 	                    d¦  «        }| 	                    d¦  «        }| 	                    d¦  «        }||k    r||k    r|dxx         dz  cc<   Œä|€|                     dd¦  «         n||d<   |€|                     dd¦  «         n||d<   |d                              |	| 	                    d¦  «        p|	||dœ||dœdœ¦  «         d}�Œ_|                     ¦   «         D ]<\  }	}|	|vr3|d                              |	| 	                    d¦  «        p|	dœ¦  «         Œ=|r ||¦  «         ddd¦  «         n# 1 swxY w Y   n<# t,          $ r/}t.                               d|d¬¦  «         d|› �|d<   Y d}~nd}~ww xY w|S )u]  Reconcile backed-up cron skill links into the live ``cron/jobs.json``.

    We do NOT overwrite the whole cron file. Only the ``skills`` and
    ``skill`` fields are restored, and only on jobs that still exist in the
    current file (matched by ``id``). Everything else about the job â€”
    schedule, next_run_at, last_run_at, enabled, prompt, workdir, hooks â€”
    is live state that the user/scheduler has modified since the snapshot;
    overwriting it would regress unrelated cron activity.

    Rules:
    - Jobs present in backup AND live, with differing skills â†’ skills restored.
    - Jobs present in backup AND live, with matching skills â†’ no-op.
    - Jobs present in backup but gone from live (user deleted the job
      after the snapshot) â†’ skipped, noted in the return report.
    - Jobs present in live but not in backup (user created a new cron
      job after the snapshot) â†’ left untouched.

    Never raises; failures are captured in the return dict. Writes through
    ``cron.jobs`` to pick up the same lock + atomic-write path that tick()
    uses, so we don't race the scheduler.
    Fr   N)Ú	attemptedÚrestoredÚskipped_missingÚ	unchangedÚerrorzsnapshot has no rÛ   r+   r'   zfailed to load backed-up jobs: r)   z)backed-up cron-jobs.json has no jobs listrw   r   Úskillr�   )r   rÜ   r�   Tr×   )Ú	load_jobsÚ	save_jobsÚ
_jobs_lockzcron module unavailable: rÚ   rb   rØ   )r   rÜ   )Újob_idÚjob_nameÚfromÚtorÙ   )rà   rá   z"Cron skill-link restore failed: %sr•   zrestore failed mid-flight: )r:   r,   r-   r1   r2   r.   r8   r3   r4   r5   r6   rE   Ú	cron.jobsrÝ   rÞ   rß   ÚImportErrorr´   r    Úpopr·   ÚitemsrV   r/   r0   )rÕ   ÚreportÚbackup_fileÚbackup_textÚbackup_parsedr?   Úbackup_jobsÚbackup_by_idÚjobÚjidrÝ   rÞ   rß   Ú	live_jobsÚchangedÚlive_idsÚliverT   Ú
cur_skillsÚ	cur_skillÚ
bkp_skillsÚ	bkp_skills                         r   Ú_restore_cron_skill_linksrø   ’  sX  € ð. ØØØØðð €Fð Õ!3Ñ3€KØ×ÒÑÔð ØAÕ-?ÐAÐAˆˆw‰ØˆðØ!×+Ò+°WÐ+Ñ=Ô=ˆÝœ
 ;Ñ/Ô/ˆˆøÝ•TÔ)Ð*ð ð ð Ø?¸AÐ?Ð?ˆˆw‰Øˆˆˆˆˆˆøøøøðøøøõ
 �-¥Ñ&Ô&ð Ø#×'Ò'¨Ñ/Ô/ˆˆÝ	�M¥4Ñ	(Ô	(ð Ø#ˆˆàˆÝ�k¥4Ñ(Ô(ð ØEˆˆw‰Øˆð /1€LØð 

ð 

ˆÝ˜#�tÑ$Ô$ð 	ØØ�gŠg�d‰mŒmˆÝ˜#�sÑ#Ô#ð 	¨3ð 	Øà—g’g˜hÑ'Ô'Ø—W’W˜WÑ%Ô%Ø—G’G˜F‘O”OÐ* sð
ð 
ˆ�SÑÐð ð Ø"ˆˆ{ÑØˆðØ>Ð>Ð>Ð>Ð>Ð>Ð>Ð>Ð>Ð>Ð>øÝð ð ð Ø9°aÐ9Ð9ˆˆw‰Øˆˆˆˆˆˆøøøøðøøøð €Fˆ;Ñð:<ØˆZ‰\Œ\ð 6	%ð 6	%Ø!˜	™œˆIØˆGå‘u”uˆHØ!ð &ñ &�Ý! $­Ñ-Ô-ð ØØ—h’h˜t‘n”n�Ý! #¥sÑ+Ô+ð °3ð ØØ—’˜SÑ!Ô!Ð!à%×)Ò)¨#Ñ.Ô.�Ø�>Øà!ŸXšX hÑ/Ô/�
Ø ŸHšH WÑ-Ô-�	Ø#ŸZšZ¨Ñ1Ô1�
Ø"ŸJšJ wÑ/Ô/�	à Ò+Ð+°	¸YÒ0FÐ0FØ˜;Ð'Ð'Ô'¨1Ñ,Ð'Ð'Ñ'Øð Ð%Ø—H’H˜X tÑ,Ô,Ð,Ð,à%/�D˜‘NØÐ$Ø—H’H˜W dÑ+Ô+Ð+Ð+à$-�D˜‘Mà�zÔ"×)Ò)Ø!Ø &§
¢
¨6Ñ 2Ô 2Ð 9°cØ'1¸IÐFÐFØ%/¸)ÐDÐDð	+ð +ñ ô ð ð �‘ð  ,×1Ò1Ñ3Ô3ð ð ‘��VØ˜hÐ&Ð&ØÐ,Ô-×4Ò4Ø"%Ø$*§J¢J¨vÑ$6Ô$6Ð$=¸#ð6ð 6ñ ô ð øð
 ð %Ø�	˜)Ñ$Ô$Ð$ðm6	%ð 6	%ð 6	%ñ 6	%ô 6	%ð 6	%ð 6	%ð 6	%ð 6	%ð 6	%ð 6	%øøøð 6	%ð 6	%ð 6	%ð 6	%øøõn ð <ð <ð <Ý�ŠÐ9¸1ÀtˆÑLÔLÐLØ;¸Ð;Ð;ˆˆw‰ˆˆˆˆˆøøøøð<øøøð €Msx   ·*A" Á"BÁ8	BÂBÂBÆ
F Æ
F1Æ	F,Æ&F1Æ,F1Æ:
N6 ÇGN*ÎN6 Î*N.Î.N6 Î1N.Î2N6 Î6
O/Ï %O*Ï*O/ÚmovedúList[Tuple[Path, Path]]c                óÀ  — g }| D ]Ø\  }}	 |                      ¦   «         r)|                     ¦   «         st          j        |¦  «         n<|                     ¦   «         s|                     ¦   «         r|                     ¦   «          t          j        t          |¦  «        t          |¦  «        ¦  «         Œ¯# t          $ r | 	                    |j
        ¦  «         Y ŒÕw xY w|S )aª  Move staged entries back to their original paths.

    ``shutil.move`` moves *into* an existing destination directory rather than
    replacing it, so a partially-completed extract leaves debris that would
    otherwise bury the user's real skill one level deeper
    (``skills/foo/foo/``) while the tree still looks populated. Clear whatever
    the failed extract created at each original path first. The staged copy is
    authoritative, and the pre-rollback safety snapshot is the undo handle for
    the extract's own output.

    Returns the names that could not be restored, so the caller can report an
    incomplete recovery instead of claiming the state was restored.
    )rµ   Ú
is_symlinkr¢   r£   r,   ÚunlinkÚmoverE   r.   r·   r�   )rù   ÚfailedÚorigr    s       r   Ú_unstager  !  sÞ   € ð €FØð %ð %‰
ˆˆdð	%Ø�{Š{‰}Œ}ð  T§_¢_Ñ%6Ô%6ð Ý”˜dÑ#Ô#Ð#Ð#Ø—’‘”ð  $§/¢/Ñ"3Ô"3ð Ø—’‘”�ÝŒK�˜D™	œ	¥3 t¡9¤9Ñ-Ô-Ð-Ð-øÝð 	%ð 	%ð 	%Ø�MŠM˜$œ)Ñ$Ô$Ð$Ð$Ð$ð	%øøøà€Ms   ‹B(B4Â4$CÃCú Tuple[bool, str, Optional[Path]]c           
     óâ  — t          | ¦  «        }|€dd| rd| › d�ndz   dz   dfS |dz  }|                     ¦   «         sdd	|j        › d
�dfS t          ¦   «         }|                     dd¬¦  «         t          ¦   «         }|                     dd¬¦  «         	 t          d|j        › �|j        h¬¦  «         n# t          $ r}dd|› �dfcY d}~S d}~ww xY w|dt          ¦   «         › �z  }	 |                     dd¬¦  «         n# t          $ r}dd|› �dfcY d}~S d}~ww xY wg }	 t          |                     ¦   «         ¦  «        D ]a}|j        t          v rŒ||j        z  }	t          j        t          |¦  «        t          |	¦  «        ¦  «         |                     ||	f¦  «         ŒbnV# t          $ rI}t#          |¦  «         	 t          j        |d¬¦  «         n# t          $ r Y nw xY wdd|› �dfcY d}~S d}~ww xY w	 t'          j        |d¦  «        5 }
|
                     ¦   «         D ]K}|j        }|                     d¦  «        sdt/          |¦  «        j        v rt'          j        d|›�¦  «        ‚ŒL	 |
                     t          |¦  «        d¬¦  «         n2# t6          $ r% |
                     t          |¦  «        ¦  «         Y nw xY wddd¦  «         n# 1 swxY w Y   �nB# t          t&          j        f$ �r(}d„ |D ¦   «         }t          |                     ¦   «         ¦  «        D ]|}|j        t          v s	|j        |v rŒ	 |                     ¦   «         r)|                     ¦   «         st          j        |¦  «         n|                     ¦   «          Œm# t          $ r Y Œyw xY wt#          |¦  «        }|r3dd|› dd                     tA          |¦  «        ¦  «        › d|› �dfcY d}~S 	 t          j        |d¬¦  «         n# t          $ r Y nw xY wdd|› �dfcY d}~S d}~ww xY w	 t          j        |d¬¦  «         n# t          $ r Y nw xY wtC          |¦  «        }d |j        › �g}| "                    d!¦  «        �r7tG          | "                    d"¦  «        pg ¦  «        }tG          | "                    d#¦  «        pg ¦  «        }| "                    d$¦  «        r|                     d%|d$         › �¦  «         n»|d&k    r!|d&k    r| "                    d'd&¦  «        d&k    rn”g }|r|                     |› d(�¦  «         |r|                     |› d)�¦  «         | "                    d'¦  «        r|                     |d'         › d*�¦  «         |                     d+d                     |¦  «        z   ¦  «         tH           %                    d,|j        |¦  «         dd-                     |¦  «        |fS ).ao  Restore ``~/.hermes/skills/`` from a snapshot.

    Strategy:
      1. Resolve the target snapshot (explicit id or newest regular).
      2. Take a safety snapshot of the CURRENT skills tree under
         ``.curator_backups/pre-rollback-<ts>/`` so the rollback itself is
         undoable.
      3. Move all current top-level entries (except ``.curator_backups``
         and ``.hub``) into a tempdir.
      4. Extract the chosen snapshot into ``~/.hermes/skills/``.
      5. On failure during 4, move the tempdir contents back (best-effort)
         and return failure.

    Returns ``(ok, message, snapshot_path)``.
    NFzno matching backup foundz	 for id 'ú'Ú zB (use `hermes curator rollback --list` to see available snapshots)r�   z	snapshot u$    has no skills.tar.gz â€” corrupted?TrŒ   zpre-rollback to )r&   rˆ   z%pre-rollback safety snapshot failed: r®   zfailed to create staging dir: r—   z failed to stage current skills: zr:gzú/z..z!refusing to extract unsafe path: Údata)Úfilterc                ó"   — h | ]\  }}|j         ’ŒS r   )r�   )rk   r   r¿   s      r   ú	<setcomp>zrollback.<locals>.<setcomp>œ  s   € Ð7Ð7Ð7¡g d¨A˜œ	Ð7Ð7Ð7r   zsnapshot extract failed: z - could not restore z, z; staged copies kept at z*snapshot extract failed (state restored): zrestored from snapshot r×   rØ   rÙ   rÛ   u   cron links: error â€” r   rÚ   z  job(s) had skill links restoredz+ backed-up job(s) no longer exist (skipped)z already matchedzcron links: z3Curator rollback: restored from %s (cron_report=%s)z; )&rÔ   r,   r�   r   rš   r   r«   rV   rP   r.   r6   rž   rŸ   r¢   rþ   rE   r·   r  r£   r›   rœ   Ú
getmembersr¶   r   Úpartsr¡   Ú
extractallr9   rµ   rü   rý   Újoinr�   rø   r5   r7   r/   r=   )rÌ   rÒ   ry   r   r¥   r?   Ústagedrù   rª   r    r©   Úmemberr�   Ústaged_namesÚ
unrestoredÚcron_reportÚsummary_bitsÚ
restored_nÚ	skipped_nr  s                       r   Úrollbackr  <  sã  € õ  ˜YÑ'Ô'€FØ€~àØ&Ø+4Ð<Ð'˜9Ð'Ð'Ð'Ð'¸"ñ>àRñSð ð
ð 	
ð �Ñ&€GØ�>Š>ÑÔð \ØÐT 6¤;ÐTÐTÐTÐVZÐ[Ð[å‰]Œ]€FØ
‡L‚L˜¨€LÑ-Ô-Ð-Ý‰nŒn€GØ‡M‚M˜$¨€MÑ.Ô.Ð.ð
	Jõ 	Ø3 f¤kÐ3Ð3Øœ˜ð	
ñ 	
ô 	
ð 	
ð 	
øõ ð Jð Jð JØÐB¸qÐBÐBÀDÐIÐIÐIÐIÐIÐIÐIøøøøðJøøøð Ð7­G©I¬IÐ7Ð7Ñ7€FðCØ�Š˜T¨EˆÑ2Ô2Ð2Ð2øÝð Cð Cð CØÐ;¸Ð;Ð;¸TÐBÐBÐBÐBÐBÐBÐBøøøøðCøøøð &(€EðEÝ˜&Ÿ.š.Ñ*Ô*Ñ+Ô+ð 	(ð 	(ˆEØŒzÕ/Ð/Ð/ØØ˜EœJÑ&ˆDÝŒK�˜E™
œ
¥C¨¡I¤IÑ.Ô.Ð.Ø�LŠL˜% ˜Ñ'Ô'Ð'Ð'ð	(øõ ð Eð Eð Eå�‰Œˆð	ÝŒM˜&°Ð5Ñ5Ô5Ð5Ð5øÝð 	ð 	ð 	ØˆDð	øøøàÐ=¸!Ð=Ð=¸tÐDÐDÐDÐDÐDÐDÐDøøøøðEøøøð-OÝŒ\˜' 6Ñ*Ô*ð 	+¨bð Ÿ-š-™/œ/ð ð �Ø”{�Ø—?’? 3Ñ'Ô'ð ¨4µ4¸±:´:Ô3CÐ+CÐ+CÝ!Ô*ØD¸DÐDÐDñô ð ð ,Dð+Ø—’�c &™kœk°&�Ñ9Ô9Ð9Ð9øÝð +ð +ð +à—’�c &™kœkÑ*Ô*Ð*Ð*Ð*ð+øøøð	+ð 	+ð 	+ñ 	+ô 	+ð 	+ð 	+ð 	+ð 	+ð 	+ð 	+øøøð 	+ð 	+ð 	+ð 	+ùøõ •WÔ%Ð&ð Oñ Oð Oð 8Ð7°Ð7Ñ7Ô7ˆÝ˜&Ÿ.š.Ñ*Ô*Ñ+Ô+ð 		ð 		ˆEØŒzÕ/Ð/Ð/°5´:ÀÐ3MÐ3MØðØ—<’<‘>”>ð #¨%×*:Ò*:Ñ*<Ô*<ð #Ý”M %Ñ(Ô(Ð(Ð(à—L’L‘N”N�NøøÝð ð ð Ø�ðøøøå˜e‘_”_ˆ
Øð 	ð ðS¨Að Sð SØ—9’9�V JÑ/Ô/Ñ0Ô0ðSð SØJPðSð Sàð	ð ð ð ð ð ð ð	ÝŒM˜&°Ð5Ñ5Ô5Ð5Ð5øÝð 	ð 	ð 	ØˆDð	øøøàÐGÀAÐGÐGÈÐNÐNÐNÐNÐNÐNÐNøøøøð;OøøøðBÝŒ�f¨DÐ1Ñ1Ô1Ð1Ð1øÝð ð ð Øˆðøøøõ ,¨FÑ3Ô3€Kà;¨f¬kÐ;Ð;Ð<€LØ‡‚�{Ñ#Ô#ñ CÝ˜Ÿš¨Ñ4Ô4Ð:¸Ñ;Ô;ˆ
Ý˜ŸšÐ(9Ñ:Ô:Ð@¸bÑAÔAˆ	Ø�?Š?˜7Ñ#Ô#ð 	CØ×ÒÐ O¸ÀWÔ9MÐ OÐ OÑPÔPÐPÐPØ˜1Š_ˆ_ ¨a¢ °K·O²OÀKÐQRÑ4SÔ4SÐWXÒ4XÐ4XààˆEØð NØ—’ 
ÐLÐLÐLÑMÔMÐMØð XØ—’ 	ÐVÐVÐVÑWÔWÐWØ�Š˜{Ñ+Ô+ð LØ—’ ¨KÔ 8ÐJÐJÐJÑKÔKÐKØ×Ò °·²¸5Ñ1AÔ1AÑ AÑBÔBÐBå
‡K‚KÐEØ”˜[ñ*ô *ð *à�$—)’)˜LÑ)Ô)¨6Ð2Ð2sI  ÂB7 Â7
CÃCÃCÃCÃ+D Ä
DÄDÄDÄDÄ%BF) Æ)
G<Æ3G7ÇGÇG7Ç
G'Ç$G7Ç&G'Ç'
G7Ç1G<Ç7G<È K' ÈA!KÉ7$JÊKÊ,KËKË
KËKËK' ËKËK' Ë!KË"K' Ë'P&Ë>AP!ÍANÎP!Î
N%Î"P!Î$N%Î%AP!Ï&P&Ï-PÐP!Ð
PÐP!ÐPÐ
P!ÐP&Ð!P&Ð*Q Ñ
QÑQc                 ó  — t          ¦   «         } | sdS dd›ddd›ddd›dd	d
›�g}|                     dt          |d         ¦  «        z  ¦  «         | D ]š}|                     |                     dd¦  «        d›d|                     dd¦  «        pdd d…         d›d|                     dd¦  «        d›dt	          t          |                     dd¦  «        ¦  «        ¦  «        d
›�¦  «         Œ›d                     |¦  «        S )NzNo curator snapshots yet.rw   z<24z  r&   z<40r   z>6Úsizez>8u   â”€r   ú?é(   r{   rz   Ú
)rË   r·   r7   r5   r   r_   r  )ÚrowsÚlinesÚrs      r   Úsummarize_backupsr   â  sH  € Ý‰>Œ>€DØð +Ø*Ð*ØÐFÐFÐF˜HÐFÐFÐF¨HÐFÐFÐF¸6ÐFÐFÐFÐG€EØ	‡L‚L��˜U 1œX™œÑ&Ñ'Ô'Ð'Øð 
ð 
ˆØ�ŠØ�uŠu�T˜#‰ŒÐ&ð Að AØ—’�h˜sÑ#Ô#Ð* s¨C¨R¨CÔ0Ð8ðAð Aà�uŠu�] AÑ&Ô&Ð-ðAð Aõ �C §¢ o°qÑ 9Ô 9Ñ:Ô:Ñ;Ô;Ð@ðAð Añ	
ô 	
ð 	
ð 	
ð �9Š9�UÑÔÐr   )r   r   )r    r   r   r!   )N)rC   rD   r   rE   )r   r!   )r   r[   )r   r_   )rh   r   r   r_   )r    r   r&   rE   rq   r   rr   r_   rs   rt   r   ru   )r‡   )r&   rE   rˆ   r‰   r   rŠ   )ra   r_   r™   r‰   r   r¬   )rÁ   r   r   r!   )r   rÅ   )rÌ   rÍ   r   rŠ   )rÕ   r   r   r!   )rù   rú   r   r¬   )rÌ   rÍ   r   r  )r   rE   )4Ú__doc__Ú
__future__r   r1   ÚloggingÚrer¢   r›   r   r   Úpathlibr   Útypingr   r   r	   r
   r   r   Úhermes_constantsr   Úagent.skill_utilsr   Úhermes_cli.sizefmtr   Ú	getLoggerÚ__name__r/   rc   rŸ   Úcompiler¸   r   r   r   r:   rB   rP   rZ   r^   rg   rp   r†   r«   r¤   rÄ   rË   rÔ   rø   r  r  r   r   r   r   ú<module>r-     sé  ðð%ð %ðN #Ð "Ð "Ð "Ð "Ð "à €€€Ø €€€Ø 	€	€	€	Ø €€€Ø €€€Ø 'Ð 'Ð 'Ð 'Ð 'Ð 'Ð 'Ð 'Ø Ð Ð Ð Ð Ð Ø 8Ð 8Ð 8Ð 8Ð 8Ð 8Ð 8Ð 8Ð 8Ð 8Ð 8Ð 8Ð 8Ð 8Ð 8Ð 8à ,Ð ,Ð ,Ð ,Ð ,Ð ,Ø 4Ð 4Ð 4Ð 4Ð 4Ð 4Ø +Ð +Ð +Ð +Ð +Ð +à	ˆÔ	˜8Ñ	$Ô	$€ð €ð
 )¨&Ð1Ð ð
 
ˆŒÐFÑ	GÔ	G€ð=ð =ð =ð =ð(ð (ð (ð (ð4ð 4ð 4ð 4ð
 &Ð ð0ð 0ð 0ð 0ðf%ð %ð %ð %ð %ð.ð .ð .ð .ð 5ð 5ð 5ð 5ð
ð ð ð ðð ð ð ð ;?ðð ð ð ð ð2JÐQUð Jð Jð Jð Jð Jð JðZ(ð (ð (ð (ð (ð^ð ð ð ðð ð ð ð:1ð 1ð 1ð 1ð,Kð Kð Kð Kð^ð ð ð ð6^3ð ^3ð ^3ð ^3ð ^3ðLð ð ð ð ð r   