@@ -1918,6 +1918,9 @@ def _init_autonomy(self, autonomy: Any, verification_hooks: Optional[List[Any]]
19181918 self .autonomy_config = {}
19191919 self ._autonomy_trigger = None
19201920 self ._doom_loop_tracker = None
1921+ self ._file_snapshot = None
1922+ self ._snapshot_stack = []
1923+ self ._redo_stack = []
19211924 return
19221925
19231926 self .autonomy_enabled = True
@@ -1942,6 +1945,9 @@ def _init_autonomy(self, autonomy: Any, verification_hooks: Optional[List[Any]]
19421945 self .autonomy_config = {}
19431946 self ._autonomy_trigger = None
19441947 self ._doom_loop_tracker = None
1948+ self ._file_snapshot = None
1949+ self ._snapshot_stack = []
1950+ self ._redo_stack = []
19451951 return
19461952
19471953 # Preserve ALL AutonomyConfig fields in the dict (G14 fix: no lossy extraction)
@@ -1954,6 +1960,8 @@ def _init_autonomy(self, autonomy: Any, verification_hooks: Optional[List[Any]]
19541960 "observe" : config .observe ,
19551961 "completion_promise" : config .completion_promise ,
19561962 "clear_context" : config .clear_context ,
1963+ "track_changes" : config .effective_track_changes ,
1964+ "snapshot_dir" : config .snapshot_dir ,
19571965 }
19581966 # Also preserve any extra user-provided keys from dict input
19591967 if isinstance (autonomy , dict ):
@@ -1967,6 +1975,21 @@ def _init_autonomy(self, autonomy: Any, verification_hooks: Optional[List[Any]]
19671975 self ._autonomy_trigger = AutonomyTrigger ()
19681976 self ._doom_loop_tracker = DoomLoopTracker (threshold = config .doom_loop_threshold )
19691977
1978+ # Initialize FileSnapshot for filesystem tracking (lazy import)
1979+ self ._file_snapshot = None
1980+ self ._snapshot_stack = [] # Stack of snapshot hashes for undo/redo
1981+ self ._redo_stack = [] # Redo stack
1982+ if config .effective_track_changes :
1983+ try :
1984+ from ..snapshot import FileSnapshot
1985+ import os
1986+ self ._file_snapshot = FileSnapshot (
1987+ project_path = os .getcwd (),
1988+ snapshot_dir = config .snapshot_dir ,
1989+ )
1990+ except Exception as e :
1991+ logger .debug (f"FileSnapshot init failed (git may not be available): { e } " )
1992+
19701993 # Wire ObservabilityHooks when observe=True (G-UNUSED-2 fix)
19711994 if config .observe :
19721995 from ..escalation .observability import ObservabilityHooks
@@ -1977,6 +2000,93 @@ def _init_autonomy(self, autonomy: Any, verification_hooks: Optional[List[Any]]
19772000 # Wire level → approval bridge (G3 fix)
19782001 self ._bridge_autonomy_level (config .level )
19792002
2003+ # ================================================================
2004+ # Filesystem tracking convenience methods (powered by FileSnapshot)
2005+ # ================================================================
2006+
2007+ def undo (self ) -> bool :
2008+ """Undo the last set of file changes.
2009+
2010+ Restores files to the state before the last autonomous iteration.
2011+ Requires ``autonomy=AutonomyConfig(track_changes=True)``.
2012+
2013+ Returns:
2014+ True if undo was successful, False if nothing to undo.
2015+
2016+ Example::
2017+
2018+ agent = Agent(autonomy="full_auto")
2019+ result = agent.start("Refactor utils.py")
2020+ agent.undo() # Restore original files
2021+ """
2022+ if self ._file_snapshot is None or not self ._snapshot_stack :
2023+ return False
2024+ try :
2025+ target_hash = self ._snapshot_stack .pop ()
2026+ # Get current hash before restore (for redo)
2027+ current_hash = self ._file_snapshot .get_current_hash ()
2028+ if current_hash :
2029+ self ._redo_stack .append (current_hash )
2030+ self ._file_snapshot .restore (target_hash )
2031+ return True
2032+ except Exception as e :
2033+ logger .debug (f"Undo failed: { e } " )
2034+ return False
2035+
2036+ def redo (self ) -> bool :
2037+ """Redo a previously undone set of file changes.
2038+
2039+ Re-applies file changes that were reverted by :meth:`undo`.
2040+
2041+ Returns:
2042+ True if redo was successful, False if nothing to redo.
2043+ """
2044+ if self ._file_snapshot is None or not self ._redo_stack :
2045+ return False
2046+ try :
2047+ target_hash = self ._redo_stack .pop ()
2048+ current_hash = self ._file_snapshot .get_current_hash ()
2049+ if current_hash :
2050+ self ._snapshot_stack .append (current_hash )
2051+ self ._file_snapshot .restore (target_hash )
2052+ return True
2053+ except Exception as e :
2054+ logger .debug (f"Redo failed: { e } " )
2055+ return False
2056+
2057+ def diff (self , from_hash : Optional [str ] = None ):
2058+ """Get file diffs from autonomous execution.
2059+
2060+ Returns a list of :class:`FileDiff` objects showing what files
2061+ were modified, with additions/deletions counts.
2062+
2063+ Args:
2064+ from_hash: Base commit hash to diff from. If None, uses the
2065+ first snapshot (pre-autonomous state).
2066+
2067+ Returns:
2068+ List of FileDiff objects, or empty list if tracking not enabled.
2069+
2070+ Example::
2071+
2072+ agent = Agent(autonomy="full_auto")
2073+ result = agent.start("Refactor utils.py")
2074+ for d in agent.diff():
2075+ print(f"{d.status}: {d.path} (+{d.additions}/-{d.deletions})")
2076+ """
2077+ if self ._file_snapshot is None :
2078+ return []
2079+ try :
2080+ base = from_hash
2081+ if base is None and self ._snapshot_stack :
2082+ base = self ._snapshot_stack [0 ]
2083+ if base is None :
2084+ return []
2085+ return self ._file_snapshot .diff (base )
2086+ except Exception as e :
2087+ logger .debug (f"Diff failed: { e } " )
2088+ return []
2089+
19802090 def analyze_prompt (self , prompt : str ) -> set :
19812091 """Analyze prompt for autonomy signals.
19822092
@@ -2159,6 +2269,15 @@ def run_autonomous(
21592269 "Create agent with autonomy=True or autonomy={...}"
21602270 )
21612271
2272+ # Take initial snapshot before autonomous execution starts
2273+ if self ._file_snapshot is not None :
2274+ try :
2275+ snap_info = self ._file_snapshot .track (message = "pre-autonomous" )
2276+ self ._snapshot_stack .append (snap_info .commit_hash )
2277+ self ._redo_stack .clear ()
2278+ except Exception as e :
2279+ logger .debug (f"Pre-autonomous snapshot failed: { e } " )
2280+
21622281 start_time = time_module .time ()
21632282 started_at = datetime .now (timezone .utc ).isoformat ().replace ("+00:00" , "Z" )
21642283 iterations = 0
0 commit comments