Skip to main content

Time-Travel Codebase

Reason across a repository's entire evolution — not just its current state. TimeTravelAgent wraps a git repo behind a GitBackend, parses commit history into an AST-based EvolutionIndex (symbol- and file-level change entries with #NNN PR linkage), flags stale abstractions with a DriftDetector, and turns diff timelines into readable markdown with a DiffNarrativeGenerator. You can scope any query to a point in time with agent.as_of(date), ask natural-language questions about how code got the way it is, and get an LLM (or heuristic) narrative back.

Every git subprocess call is offloaded off the event loop with asyncio.to_thread, so the public IO methods stay coroutines and never block your async application.

Import:

from synapsekit.timetravel import (
TimeTravelAgent,
GitBackend,
EvolutionIndex,
EvolutionEntry,
DriftDetector,
DriftCandidate,
DiffNarrativeGenerator,
CommitInfo,
AsOf,
)

Only the standard git CLI is required at runtime — the backend shells out to git. An LLM is optional: without one, narratives fall back to a structured heuristic summary.


Quickstart

import asyncio
from synapsekit.timetravel import TimeTravelAgent

async def main():
# Point at any git repo. An LLM is optional — omit the key and you get
# the deterministic heuristic narrative instead.
agent = TimeTravelAgent(repo=".", model="gpt-4o-mini", api_key="sk-...")

# 1. Natural-language question about how the code evolved
narrative = await agent.query("how has AgentRegistry changed over time?")
print(narrative)

# 2. Chronological timeline for a file or symbol (oldest → newest)
for entry in await agent.timeline("AgentRegistry"):
pr = f" (#{entry.pr_number})" if entry.pr_number else ""
print(f"{entry.commit.date:%Y-%m-%d} [{entry.commit.hash[:7]}]{pr} "
f"{entry.change_type} {entry.symbol or entry.file_path}")

# 3. Which abstractions have drifted from their original justification?
for cand in await agent.detect_drift(min_age_days=0):
print(f"{cand.symbol}: confidence={cand.confidence:.2f} "
f"usage {cand.original_usage_count}{cand.current_usage_count}")
print(" →", cand.recommendation)

asyncio.run(main())

TimeTravelAgent(repo, *, llm=None, model="gpt-4o-mini", api_key="", provider=None, world_model=None, memory=None) resolves repo to an absolute path and wires up a GitBackend, EvolutionIndex, and DriftDetector for you. If LLM construction fails (no key, unknown provider), the agent silently sets llm=None and every narrative uses the heuristic path — so the quickstart above runs even with api_key="".


Asking questions

await agent.query(question) builds (or reuses) the evolution index, matches the question's terms against indexed files and symbols, deduplicates, and hands the resulting EvolutionEntry list to the narrative generator. The whole blocking git/index pipeline runs via asyncio.to_thread, so query never blocks the loop.

answer = await agent.query("why was the retry logic added to the HTTP client?")

If a world_model (WorldModelRAG) or memory (LivingMemory) is attached, their bitemporal-graph context and matching patch rationales are appended under a Historical Memory & Knowledge Graph Context section.


Point-in-time scoping with as_of(date)

agent.as_of(date) returns an AsOf context anchored to the commit at or before date. date accepts a datetime or an ISO / YYYY-MM-DD string; a date predating all history resolves to the earliest commit (not HEAD).

from datetime import datetime, UTC

as_of = agent.as_of("2026-01-15")
print(as_of.commit) # commit hash in effect on that date

# Query the codebase *as it was* on that date
snapshot = await as_of.query("how did authentication work?")

# Timeline and drift, both scoped up to that date
history = await as_of.timeline("AuthManager")
drift = await as_of.detect_drift("AuthManager")

AsOf.query returns a markdown report headed # Codebase State As Of <date> (<commit>) — listing the tracked-file count, previews of files relevant to the question at that commit, and a narrative built from the timeline up to that date.


The evolution index

EvolutionIndex.build(...) walks commit history via GitBackend.log and, for each .py file, parses the file content at that commit with Python's ast module to attach the class / function / async-function names that changed. Each change becomes an EvolutionEntry:

FieldTypeDescription
file_pathstrFile that changed
symbolstr | NoneClass / function name (Python files) or None
commitCommitInfohash, author, date, subject, body, files_changed
diff_snippetstrFirst ~15 added/removed lines of the diff
change_type"added" | "modified" | "deleted" | "renamed"Inferred change kind
lines_added / lines_removedintDiff line counts
pr_numberint | NoneParsed from a #NNN reference in the commit subject or body

You can drive the index directly without the agent:

from synapsekit.timetravel import GitBackend, EvolutionIndex

index = EvolutionIndex(GitBackend("."))
index.build() # optionally: paths=[...], since=..., until=..., max_count=...
entries = index.query("AgentRegistry") # match by symbol or file substring
timeline = index.timeline("agent.py") # same, sorted oldest → newest

Detecting drift

DriftDetector.detect(...) groups indexed entries by symbol and, for each, compares how many times the symbol is referenced across the repo at its first commit versus at HEAD (GitBackend.list_files + regex counting per Python file). It scores a confidence from heuristics — few or zero remaining callers, usage dropping below its original count, rationale words like temporary / workaround / interim / deprecated / todo, and age over 180 days — and emits a DriftCandidate when confidence clears the threshold (or when you name a specific symbol).

candidates = await agent.detect_drift(symbol=None, min_age_days=30)
for c in candidates:
print(c.symbol, c.confidence, c.original_rationale)
print(c.recommendation)

Each DriftCandidate carries symbol, file_path, original_rationale (extracted from the introducing commit's subject + body), current_usage_count, original_usage_count, first_introduced, last_modified, confidence, and a human-readable recommendation. Results are sorted by descending confidence.


Generating evolution narratives

DiffNarrativeGenerator.generate(entries, query, llm=None) produces the markdown timeline. With an LLM it streams a developer-facing summary (how the code evolved, why, any architectural shift); without one — or on any LLM error — it falls back to _heuristic_narrative, a structured markdown block with a timeframe header, a per-commit Evolution Timeline (date, short hash, #PR, symbol, subject, author, diff preview), and a Key Changes & Rationale rollup.

from synapsekit.timetravel import DiffNarrativeGenerator, EvolutionIndex, GitBackend

index = EvolutionIndex(GitBackend("."))
entries = index.query("AgentRegistry")

gen = DiffNarrativeGenerator() # no LLM → heuristic markdown
print(await gen.generate(entries, "AgentRegistry evolution"))

The git backend

GitBackend(repo_path) is a thin async-safe wrapper over the git CLI. It is what makes everything above point-in-time aware:

  • log(path=None, follow=True, since=None, until=None, max_count=None) -> list[CommitInfo]
  • diff(commit_a, commit_b="HEAD", path=None) -> str
  • show(commit, path) -> str / file_at(commit, path) -> str (alias) — file content at a commit
  • find_commit_at(date) -> str — commit hash at or before a date (earliest commit for pre-history dates)
  • blame(path, commit=None) -> list[dict] — structured line ownership
  • list_files(commit=None) -> list[str] — tracked files at a commit or HEAD

The backend's methods are synchronous; the agent wraps them in asyncio.to_thread from its coroutine methods so nothing blocks the event loop.


API reference

TimeTravelAgent(repo=".", *, llm=None, model="gpt-4o-mini", api_key="", provider=None, world_model=None, memory=None)

MethodDescription
as_of(date) -> AsOfPoint-in-time context anchored to the commit at/before date
async query(question) -> strNL question about the repo's evolution → markdown narrative
async timeline(file_or_symbol, since=None, until=None) -> list[EvolutionEntry]Chronological change timeline
async detect_drift(symbol=None, min_age_days=0, as_of_date=None) -> list[DriftCandidate]Flag drifted abstractions

AsOf

Dataclass with agent, date, commit. Methods: async query(question), async detect_drift(symbol), async timeline(file_or_symbol) — each scoped to date.

CommitInfo

Frozen dataclass: hash, author, date, subject, body, files_changed.


See also