DEV Community

Charlie Hu
Charlie Hu

Posted on

Weekend Build Log: A Session Snapshot Trail for AI Coding — What I Built, Cut, and Kept

Reviewing AI-generated code fails most often at the boundary between turns, not between files. The merged diff hides the order in which edits happened, and the order is where intent breaks. This weekend build produced a small CLI that snapshots the working tree every few seconds, groups changes into turns, and writes a per-turn review note. The tool is deliberately small, the notes cost almost nothing in tokens, and the whole experiment stayed on free infrastructure.

The problem behind the build

AI coding tools turned every developer into a reviewer. Part of the current developer discussion asks what the human should do while the model does the editing. A previous post on this account covered a merge gate for AI-generated code. This build is the trail behind that gate: a record of what the AI touched, in what order, and where the human should look first.

The gap is simple. A combined diff shows the final state, not the sequence of edits. A one-line config change made early can be visually buried under a large formatting shift from a later turn. Reviewers either trust the model or rebuild the timeline by hand. Neither is a good option.

The shape of the tool

The tool is called snaplog. It polls git status instead of watching filesystem events. Polling is cross-platform, and human-paced AI edits do not need millisecond precision.

  • Every 5 seconds it reads the changed-file list and diff statistics for the current branch.
  • When no change appears for 90 seconds, the current turn is closed.
  • Each turn records start time, end time, changed files, and added/deleted line counts.
  • On Ctrl+C the session renders a Markdown report with one section per turn.

Turn detection is a heuristic. Long silent pauses split one turn into two, and two fast bursts can merge. For triage, that is acceptable.

The core script

snaplog uses only the Python standard library and git:

#!/usr/bin/env python3
"""snaplog — build a review trail for an AI-assisted coding session.

Start it, let the AI edit, press Ctrl+C to stop.
A Markdown report grouped into edit turns is written to session-report.md.
Only the Python standard library and git are required.
"""
import argparse
import json
import subprocess
import time
from datetime import datetime
from pathlib import Path

INTERVAL = 5   # seconds between git status polls
IDLE_GAP = 90  # seconds without edits -> the current turn ends


def git(*args: str) -> str:
    out = subprocess.run(["git", *args], capture_output=True, text=True)
    return out.stdout.strip()


def current_changes() -> tuple[list[str], dict[str, tuple[int, int]]]:
    status = git("status", "--porcelain")
    files = sorted(line[3:] for line in status.splitlines() if line.strip())
    stats: dict[str, tuple[int, int]] = {}
    for line in git("diff", "--numstat", "HEAD").splitlines():
        added, deleted, path = line.split("\t", 2)
        stats[path] = (int(added), int(deleted))
    return files, stats


def add_summary(turn: dict, command: str) -> None:
    result = subprocess.run(
        command, shell=True, input=json.dumps(turn),
        capture_output=True, text=True,
    )
    turn["summary"] = result.stdout.strip()


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument(
        "--summarizer",
        help="command that reads a turn JSON on stdin and prints review notes",
    )
    args = parser.parse_args()

    turns = []
    current = None
    last_change = time.monotonic()

    try:
        while True:
            files, stats = current_changes()
            now = time.monotonic()
            if files:
                if current is None:
                    current = {
                        "start": datetime.now().isoformat(timespec="seconds"),
                        "files": [],
                        "stats": {},
                    }
                current["files"] = sorted(set(current["files"]) | set(files))
                current["stats"].update(stats)
                last_change = now
            elif current and now - last_change > IDLE_GAP:
                current["end"] = datetime.now().isoformat(timespec="seconds")
                if args.summarizer:
                    add_summary(current, args.summarizer)
                turns.append(current)
                current = None
            time.sleep(INTERVAL)
    except KeyboardInterrupt:
        if current:
            current["end"] = datetime.now().isoformat(timespec="seconds")
            if args.summarizer:
                add_summary(current, args.summarizer)
            turns.append(current)

    lines = ["# Session review trail\n"]
    for index, turn in enumerate(turns, 1):
        lines.append(f"## Turn {index}{turn['start']}{turn['end']}")
        for path, (added, deleted) in sorted(turn["stats"].items()):
            lines.append(f"- `{path}` (+{added}/-{deleted})")
        if turn.get("summary"):
            lines.append(f"> {turn['summary']}")
        lines.append("")
    Path("session-report.md").write_text("\n".join(lines))
    print(f"wrote session-report.md with {len(turns)} turns")


if __name__ == "__main__":
    main()
Enter fullscreen mode Exit fullscreen mode

The summarizer is a separate seam. It reads a turn JSON object from stdin and prints a short review note:

#!/usr/bin/env python3
"""Adapter for snaplog --summarizer.

Reads a turn JSON object from stdin and prints a short review note.
Expects an OpenAI-compatible chat-completions endpoint at SNAPLOG_API_URL.
This weekend build pointed SNAPLOG_API_URL at MonkeyCode's free model access.
The response contract is the only assumption here.
"""
import json
import os
import sys
from urllib import request

turn = json.load(sys.stdin)
file_lines = [
    f"- {path} (+{added}/-{deleted})"
    for path, (added, deleted) in sorted(turn["stats"].items())[:20]
]
prompt = (
    "Summarize one AI-assisted coding turn for a human reviewer. "
    "Write two bullets: what likely changed, and what deserves a close look.\n"
    + "\n".join(file_lines)
)

payload = {
    "model": os.environ.get("SNAPLOG_MODEL", "default"),
    "messages": [{"role": "user", "content": prompt}],
}
headers = {"Content-Type": "application/json"}
if key := os.environ.get("SNAPLOG_API_KEY"):
    headers["Authorization"] = f"Bearer {key}"

req = request.Request(
    os.environ["SNAPLOG_API_URL"],
    data=json.dumps(payload).encode(),
    headers=headers,
)
with request.urlopen(req) as resp:
    data = json.load(resp)
print(data["choices"][0]["message"]["content"])
Enter fullscreen mode Exit fullscreen mode

Where the free pieces fit

The summarizer step is a seam. In this build, each closed turn was sent to MonkeyCode's free model access, and the returned notes were inserted under the matching turn. Disclosure: This article was prepared as part of MonkeyCode's product outreach.

MonkeyCode is an open-source project. Its current offering includes free model access (10 million tokens per the project's documentation at the time of writing) and a free server option. Both terms can change, so the README is the source of truth before anyone relies on the allowance for more than a weekend.

The free server option ran the second half of the demo. The Markdown report renders to one static HTML page, and one long-running process serves the directory. That workload fits any free tier:

python -m http.server 8080 --directory ./report_out
Enter fullscreen mode Exit fullscreen mode

A collaborator opens the port and sees the whole session trail in a browser. Nothing needs to be installed on the reviewer's machine.

The working demo

The command that produced the report:

SNAPLOG_API_URL=<chat-completions-endpoint> \
SNAPLOG_API_KEY=<key-if-required> \
python snaplog.py --summarizer "python summarize.py"
Enter fullscreen mode Exit fullscreen mode

After the AI finishes, Ctrl+C stops the loop. The file session-report.md appears with one section per edit turn, in this format (illustrative output, not a real session):

# Session review trail

## Turn 1 — 14:02:11 → 14:05:33
- `src/parser.py` (+34/-10)
- `tests/test_parser.py` (+18/-0)

> Turn 1 replaced the hand-rolled tokenizer loop with a table-driven
> parser. Check the empty-input edge case: the new branch returns
> early before setting `self.pos`.
Enter fullscreen mode Exit fullscreen mode

The adapter expects an OpenAI-compatible chat-completions response shape. A different gateway contract means swapping the last three lines of summarize.py.

What got cut

Scope control was the point of the weekend.

  1. Filesystem watchers — dropped. They add platform-specific code and permissions. Polling every 5 seconds is enough.
  2. Full-diff summarization — dropped. Sending whole diffs to a model burns tokens fast. The adapter sends only filenames and line counts, capped at the first 20 files per turn.
  3. CI integration and dashboards — dropped. Version one is a local CLI plus one static HTML page.
  4. Token budgeting — dropped. The hard input cap and the idle heuristic are the budget.

What the trail changed

In the test session, the review started from the report instead of from an empty diff. The per-turn notes pointed at the risky file first, and the turn order exposed an early config edit that the final diff made easy to miss. The effect is structural: the human reads a session map before reading code.

Limitations

  • Git-only. If the AI writes to files outside the working tree, the trail misses them. Untracked files also appear in the file list without line stats.
  • Heuristic turns. Silent thinking time splits a turn; overlapping bursts merge.
  • No security layer. Filenames and line counts leave the machine. Pointing the adapter at code that cannot be shared is a bad idea.
  • Triage, not audit. This is not a replacement for reading the diff, and it is not an immutable audit trail.

Who should skip this

Teams that need signed, compliant audit trails should use a real ALM tool. Developers whose review process already catches intent changes will see friction without payoff. Anyone who prefers to re-derive session history by hand can keep doing that.

Closing

The useful result is a receipt. A reviewer gets a map of the session before opening the diff, and the map costs a small prompt per turn instead of an hour of archaeology. If this pattern fits a weekend workflow, MonkeyCode's free model allowance and free server option are a low-risk place to test it — current terms are in the project README, and they can change, so verify before starting. The script and the habit are both free.

Top comments (0)