
    KiJK                        U d Z ddlZddlZddlZddlZddlZddlmZ ddlm	Z	 ddl
mZmZmZmZ  ej        e          Z e	            dz  Zg dZ ed ed	 e ej        d
d                                        Zeed<   dZdedefdZdededefdZedfdee         dedededeee                  defdZ dededee         fdZ!dedefdZ" G d d          Z#dee         dedefdZ$dS )uL  
Checkpoint Manager — Transparent filesystem snapshots via shadow git repos.

Creates automatic snapshots of working directories before file-mutating
operations (write_file, patch), triggered once per conversation turn.
Provides rollback to any previous checkpoint.

This is NOT a tool — the LLM never sees it.  It's transparent infrastructure
controlled by the ``checkpoints`` config flag or ``--checkpoints`` CLI flag.

Architecture:
    ~/.hermes/checkpoints/{sha256(abs_dir)[:16]}/   — shadow git repo
        HEAD, refs/, objects/                        — standard git internals
        HERMES_WORKDIR                               — original dir path
        info/exclude                                 — default excludes

The shadow repo uses GIT_DIR + GIT_WORK_TREE so no git state leaks
into the user's project directory.
    N)Path)get_hermes_home)DictListOptionalSetcheckpoints)znode_modules/zdist/zbuild/z.envz.env.*z
.env.localz.env.*.localz__pycache__/z*.pycz*.pyoz	.DS_Storez*.logz.cache/z.next/z.nuxt/z	coverage/z.pytest_cache/z.venv/zvenv/z.git/
   <   HERMES_CHECKPOINT_TIMEOUT30_GIT_TIMEOUTiP  working_dirreturnc                     t          t          |                                                     }t          j        |                                                                          dd         }t          |z  S )z6Deterministic shadow repo path: sha256(abs_path)[:16].N   )strr   resolvehashlibsha256encode	hexdigestCHECKPOINT_BASE)r   abs_pathdir_hashs      5/home/ubuntu/hermes-agent/tools/checkpoint_manager.py_shadow_repo_pathr   H   s\    4$$,,..//H~hoo//00::<<SbSAHX%%    shadow_repoc                 L   t           j                                        }t          |           |d<   t          t	          |                                                    |d<   |                    dd           |                    dd           |                    dd           |S )z5Build env dict that redirects git to the shadow repo.GIT_DIRGIT_WORK_TREEGIT_INDEX_FILENGIT_NAMESPACE GIT_ALTERNATE_OBJECT_DIRECTORIES)osenvironcopyr   r   r   pop)r   r   envs      r   _git_envr+   O   s    
*//

C%%C	NtK0088::;;CGGd###GGOT"""GG.555Jr   argstimeoutallowed_returncodesc                    t          ||          }dgt          |           z   }|pt                      }	 t          j        |dd||t          t          |                                                              }|j        dk    }|j	        
                                }	|j        
                                }
|s>|j        |vr5t                              dd                    |          |j        |
           ||	|
fS # t          j        $ r? d| dd                    |           }t                              |d	           d
d|fcY S t           $ r4 t                              dd                    |          d	           Y dS t"          $ rM}t                              dd                    |          |d	           d
dt          |          fcY d}~S d}~ww xY w)a4  Run a git command against the shadow repo.  Returns (ok, stdout, stderr).

    ``allowed_returncodes`` suppresses error logging for known/expected non-zero
    exits while preserving the normal ``ok = (returncode == 0)`` contract.
    Example: ``git diff --cached --quiet`` returns 1 when changes exist.
    gitT)capture_outputtextr-   r*   cwdr   z(Git command failed: %s (rc=%d) stderr=%s zgit timed out after zs: )exc_infoF zGit executable not found: %s)Fr6   zgit not foundz#Unexpected git error running %s: %sN)r+   listset
subprocessrunr   r   r   
returncodestdoutstripstderrloggererrorjoinTimeoutExpiredFileNotFoundError	Exception)r,   r   r   r-   r.   r*   cmdresultokr<   r>   msgexcs                r   _run_gitrJ   Z   s    ;
,
,C'DJJ
C-6#D%%--//00
 
 
 !#$$&&$$&& 	f'/BBBLL:v0&   66!!$   @W@@#@@S4(((b#~ * * *3SXXc]]TRRR))) # # #:CHHSMM3Y]^^^b#c(("""""""#s,   CC= =AG
9G	GAGGGc                 V   | dz                                   rdS |                     dd           t          dg| |          \  }}}|sd| S t          g d| |           t          g d| |           | d	z  }|                    d
           |dz                      d                    t
                    dz   d           | dz                      t          t          |                                                    dz   d           t          
                    d| |           dS )z@Initialise shadow repo if needed.  Returns error string or None.HEADNT)parentsexist_okinitzShadow repo init failed: )configz
user.emailzhermes@local)rP   z	user.namezHermes Checkpointinfo)rN   exclude
zutf-8)encodingHERMES_WORKDIRz(Initialised checkpoint repo at %s for %s)existsmkdirrJ   
write_textrA   DEFAULT_EXCLUDESr   r   r   r?   debug)r   r   rG   _errinfo_dirs         r   _init_shadow_repor^      sZ   f$$&& tdT2226(K==JB3 103000555{KPPP999;TTTV#HNNDN!!!	%%		"##d*W &    ##//D%%''((4/' 0    LL;[+VVV4r   pathc                     d}	 t          |                               d          D ]}|dz  }|t          k    r|c S n# t          t          f$ r Y nw xY w|S )z;Quick file count estimate (stops early if over _MAX_FILES).r   *   )r   rglob
_MAX_FILESPermissionErrorOSError)r_   countr[   s      r   _dir_file_countrh      s    Ed!!#&& 	 	AQJEz!! "	 W%   Ls   7> > AAc            	           e Zd ZdZddedefdZdd	ZddededefdZ	dede
e         fdZedededdfd            ZdededefdZddedededefdZdedefdZdededefdZdededdfdZdS )CheckpointManagera  Manages automatic filesystem checkpoints.

    Designed to be owned by AIAgent.  Call ``new_turn()`` at the start of
    each conversation turn and ``ensure_checkpoint(dir, reason)`` before
    any file-mutating tool call.  The manager deduplicates so at most one
    snapshot is taken per directory per turn.

    Parameters
    ----------
    enabled : bool
        Master switch (from config / CLI flag).
    max_snapshots : int
        Keep at most this many checkpoints per directory.
    F2   enabledmax_snapshotsc                 V    || _         || _        t                      | _        d | _        d S N)rl   rm   r8   _checkpointed_dirs_git_available)selfrl   rm   s      r   __init__zCheckpointManager.__init__   s+    *,/EE.2r   r   Nc                 8    | j                                          dS )zAReset per-turn dedup.  Call at the start of each agent iteration.N)rp   clear)rr   s    r   new_turnzCheckpointManager.new_turn   s    %%'''''r   autor   reasonc                 h   | j         sdS | j        <t          j        d          du| _        | j        st                              d           | j        sdS t          t          |                                                    }|dt          t          j	                              fv rt                              d|           dS || j
        v rdS | j
                            |           	 |                     ||          S # t          $ r&}t                              d|           Y d}~dS d}~ww xY w)u   Take a checkpoint if enabled and not already done this turn.

        Returns True if a checkpoint was taken, False otherwise.
        Never raises — all errors are silently logged.
        FNr0   z#Checkpoints disabled: git not found/z,Checkpoint skipped: directory too broad (%s)z!Checkpoint failed (non-fatal): %s)rl   rq   shutilwhichr?   rZ   r   r   r   homerp   add_takerD   )rr   r   rx   abs_dires        r   ensure_checkpointz#CheckpointManager.ensure_checkpoint   sE    | 	5 &"(,u"5"5T"AD& DBCCC" 	5d;''//1122 sC	,,---LLGQQQ5 d---5##G,,,	::gv... 	 	 	LL<a@@@55555	s   +D 
D1D,,D1c           	         t          t          |                                                    }t          |          }|dz                                  sg S t          dddt          | j                  g||          \  }}}|r|sg S g }|                                D ]}|                    dd          }	t          |	          dk    r}|	d         |	d	         |	d
         |	d         dddd}
t          dd|	d          d|	d         g||ddh          \  }}}|r|r| 
                    ||
           |                    |
           |S )zList available checkpoints for a directory.

        Returns a list of dicts with keys: hash, short_hash, timestamp, reason,
        files_changed, insertions, deletions.  Most recent first.
        rL   logz--format=%H|%h|%aI|%sz-n|      r   rb      )hash
short_hash	timestamprx   files_changed
insertions	deletionsdiffz--shortstatz~1      r.   )r   r   r   r   rV   rJ   rm   
splitlinessplitlen_parse_shortstatappend)rr   r   r   shadowrG   r<   r[   resultslinepartsentrystat_okstat_outs                r   list_checkpointsz"CheckpointManager.list_checkpoints   s    d;''//1122"7++'')) 	I +T3t7I3J3JKG
 
FA
  	 	I%%'' 	& 	&DJJsA&&E5zzQ!!H"'(!&q#Ah%&"#!"  (0]uQxOOOU1XFG),c
( ( ($1
  ;x ;))(E:::u%%%r   	stat_liner   c                    ddl }|                    d|           }|r%t          |                    d                    |d<   |                    d|           }|r%t          |                    d                    |d<   |                    d|           }|r't          |                    d                    |d	<   dS dS )
z-Parse git --shortstat output into entry dict.r   Nz
(\d+) filerb   r   z(\d+) insertionr   z(\d+) deletionr   )researchintgroup)r   r   r   ms       r   r   z"CheckpointManager._parse_shortstat'  s     				IImY// 	5%(__E/"II()44 	2"%aggajj//E,II'33 	1!$QWWQZZE+	1 	1r   commit_hashc                    t          t          |                                                    }t          |          }|dz                                  sdddS t          dd|g||          \  }}}|s	dd| ddS t          d	d
g||t          dz             t          dd|dg||          \  }}	}t          d|ddg||          \  }
}}t          g d||           |s|
sdddS d|r|	nd|
r|nddS )zShow diff between a checkpoint and the current working tree.

        Returns dict with success, diff text, and stat summary.
        rL   F'No checkpoints exist for this directorysuccessr@   cat-file-tCheckpoint '' not foundr~   -Ar   r-   r   z--stat--cachedz
--no-color)resetrL   --quietzCould not generate diffTr6   )r   statr   )r   r   r   r   rV   rJ   r   )rr   r   r   r   r   rG   r[   r\   ok_statr   ok_diffdiff_outs               r   r   zCheckpointManager.diff5  s   
 d;''//1122"7++'')) 	Z$/XYYY {+VW
 

As  	X$/Vk/V/V/VWWW 	%9IJJJJ  (X{J7G 
  
1  ([*l;G 
  
1 	---vw??? 	Jw 	J$/HIII  '/HHR '/HHR
 
 	
r   	file_pathc                    t          t          |                                                    }t          |          }|dz                                  sdddS t          dd|g||          \  }}}|sdd| d|pd	d
S |                     |d|d	d          d           |r|nd}	t          d|d|	g||t          dz            \  }}
}|sdd| |pd	d
S t          ddd|g||          \  }}}|r|nd}d|d	d         ||d}|r||d<   |S )u  Restore files to a checkpoint state.

        Uses ``git checkout <hash> -- .`` (or a specific file) which restores
        tracked files without moving HEAD — safe and reversible.

        Parameters
        ----------
        file_path : str, optional
            If provided, restore only this file instead of the entire directory.

        Returns dict with success/error info.
        rL   Fr   r   r   r   r   r   N)r   r@   rZ   z$pre-rollback snapshot (restoring to    ).checkoutz--r   r   zRestore failed: r   z--format=%sz-1unknownT)r   restored_torx   	directoryfile)r   r   r   r   rV   rJ   r   r   )rr   r   r   r   r   r   rG   r[   r\   restore_targetr<   ok2
reason_outrx   rF   s                  r   restorezCheckpointManager.restoreb  s    d;''//1122"7++'')) 	Z$/XYYY {+VW
 

As  	n$/Vk/V/V/Vadalhlmmm 	

7U;rPQr?UUUVVV '08S"dN;G\A%5
 
 
FC
  	_$/G#/G/GRUR]Y]^^^ &M45vw
 
Z  #1	 &rr? 	
 
  	'&F6Nr   c                 :   t          |                                          }|                                r|}n|j        }h d}|j        k    r<t	          fd|D                       rt                    S j        j        k    <t          |          S )a  Resolve a file path to its working directory for checkpointing.

        Walks up from the file's parent to find a reasonable project root
        (directory containing .git, pyproject.toml, package.json, etc.).
        Falls back to the file's parent directory.
        >	   .hg.gitgo.modpom.xml
Cargo.tomlpackage.jsonpyproject.tomlGemfileMakefilec              3   F   K   | ]}|z                                   V  d S ro   )rV   ).0r   checks     r   	<genexpr>z=CheckpointManager.get_working_dir_for_path.<locals>.<genexpr>  s3      99AEAI%%''999999r   )r   r   is_dirparentanyr   )rr   r   r_   	candidatemarkersr   s        @r   get_working_dir_for_pathz*CheckpointManager.get_working_dir_for_path  s     I&&((;;== 	$IIIG G Gu|##999999999 "5zz!LE u|## 9~~r   c                    t          |          }t          ||          }|rt                              d|           dS t	          |          t
          k    r#t                              dt
          |           dS t          ddg||t          dz            \  }}}|st                              d|           dS t          g d	||d
h          \  }}}|rt                              d|           dS t          dd|dg||t          dz            \  }}}|st                              d|           dS t                              d||           |                     ||           dS )z*Take a snapshot.  Returns True on success.zCheckpoint init failed: %sFz#Checkpoint skipped: >%d files in %sr~   r   r   r   zCheckpoint git-add failed: %s)r   r   r   rb   r   z$Checkpoint skipped: no changes in %scommitz-mz--allow-empty-messagezCheckpoint commit failed: %szCheckpoint taken in %s: %sT)	r   r^   r?   rZ   rh   rd   rJ   r   _prune)	rr   r   rx   r   r\   rG   r[   r   r   s	            r   r   zCheckpointManager._take  s   ";//  44 	LL5s;;;5 ;''*44LL>
KXXX5 DM6;q8H
 
 

As  	LL8#>>>5  (+++!"	 
  
  
1  	LL?MMM5 tV%<=K)9
 
 

As  	LL7===51;GGG 	FK(((tr   r   c                 
   t          g d||          \  }}}|sdS 	 t          |          }n# t          $ r Y dS w xY w|| j        k    rdS t          g d||          \  }}}t                              d|| j                   dS )z:Keep only the last max_snapshots commits via orphan reset.)rev-listz--countrL   N)r   z	--reverserL   z--skip=0z--max-count=1z)Checkpoint repo has %d commits (limit %d))rJ   r   
ValueErrorrm   r?   rZ   )rr   r   r   rG   r<   r[   rg   cutoff_hashs           r   r   zCheckpointManager._prune  s     +++[+
 
FA  	F	KKEE 	 	 	FF	 D&&&F &  
 
K 	@%I[\\\\\s   - 
;;)Frk   )r   N)rw   ro   )__name__
__module____qualname____doc__boolr   rs   rv   r   r   r   r   r   staticmethodr   r   r   r   r   r   r    r   r   rj   rj      s        3 3 3S 3 3 3 3( ( ( (" "S "# "4 " " " "H*C *DJ * * * *X 1C 1 1 1 1 1 \1+
 +
# +
$ +
 +
 +
 +
Z5 53 5S 5S 5TX 5 5 5 5n# #    :1 1c 1d 1 1 1 1f]$ ]S ]T ] ] ] ] ] ]r   rj   r   c                 "   | sd| S d| dg}t          | d          D ]\  }}|d         }d|v r}|                    d          d                             d          d                             d	          d         d
d         }|d                             d          d         }| d| }|                    dd          }|                    dd          }|                    dd          }	|rd| d|dk    rdnd d| d|	 d	}
nd}
|                    d| d|d          d| d|d          |
 	           |                    d           |                    d           |                    d           d                    |          S )z+Format checkpoint list for display to user.zNo checkpoints found for u   📸 Checkpoints for z:
rb   r   T+r   -N   r4   r   r   r   z  (z filesr6   z, +z/-r   z  z. r   rx   z4
  /rollback <N>             restore to checkpoint Nz>  /rollback diff <N>        preview changes since checkpoint NzC  /rollback <N> <file>      restore a single file from checkpoint NrS   )	enumerater   getr   rA   )r	   r   linesicptsdatefilesinsdeler   s              r   format_checkpoint_listr     s    7696663Y3334E;** O O2_"99#q!'',,Q/55c::1=bqbABk?((--a0D2B **ff\1%%vvk1%% 	QQQEQJJSSBQQ3QQ$QQQDDDM!MMr,/MM2MMHMtMMNNNN	LLHIII	LLQRRR	LLVWWW99Ur   )%r   r   loggingr&   r{   r9   pathlibr   hermes_constantsr   typingr   r   r   r   	getLoggerr   r?   r   rY   maxminr   getenvr   __annotations__rd   r   r   dictr+   tuplerJ   r^   rh   rj   r   r   r   r   <module>r     s    (   				            , , , , , , , , , , , , , , , , , ,		8	$	$ "/##m3   0 CCCCC		2Mt(T(T$U$UVVWWc W W W 
&3 &4 & & & &$ S T      .2+# +#
s)+#+# +# 	+#
 "#c(++# +# +# +# +#\4 c hsm    8
# 
# 
 
 
 
"P] P] P] P] P] P] P] P]f
T
 s s      r   