Skip to content

Latest commit

 

History

9 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

driftline

Code intelligence engine that maps repository structure, dependencies, and relationships into a graph for developers and AI agents.

A live, local map of what your AI coding agents are doing the plan they declared, the files they actually touched, and every place those two stop agreeing.

Quickstart · Dashboard · PLAN.md · Extending

driftline replaying an agent session

MIT · zero runtime dependencies · nothing leaves your machine

Quickstart

cd your-repo
npx driftline init     # one-time: local agent hooks + a starter PLAN.md
npx driftline --open   # http://127.0.0.1:4173

That's the whole install. There is no npm install step and no config file driftline has zero runtime dependencies, because SQLite comes from node:sqlite, the watcher from fs.watch, and the server from node:http. Requires Node 22.5+.

Want to see it without wiring anything up? Clone this repo and run it on itself — PLAN.md here describes driftline, so you get a real map in five seconds:

git clone https://github.com/justusa11/driftline && cd driftline
node bin/driftline.mjs --open

What you get

A map that draws itself Components and dependencies come from PLAN.md. Topological columns, crossing-reduced rows, click any card to focus its neighbourhood.
Observed activity, attributed Every write is matched back to the component that declared those files. Cards light up as their files move.
Drift A component says [x] done, but its files kept changing afterwards. Badged on the card, counted in the legend.
Stalled work A component says [~] working, but nothing has moved in ten minutes. The answer to "is it stuck, or is it thinking?"
Plan coverage The glob matcher run backwards: what percentage of observed writes does your plan actually claim? With the unmapped directories listed and paste-ready stubs for them.
Replay Scrub or play the entire session back. The map, the feed, the file heat and every timestamp rewind together — including the plan itself, so components that hadn't been declared yet are genuinely absent.

Why this exists

Watching an agent work, you lose the thread fast. It reports success on a task while quietly editing three files nobody asked it to touch; it marks something done and keeps changing it; it goes quiet and you can't tell whether it's stuck or thinking. The plan and the work drift apart, and nothing shows you the gap.

driftline holds the two side by side: what was declared (PLAN.md) and what was observed (every write, every tool call). The interesting part is never either one on its own — it's where they disagree.

Credit where it's due: the core idea — declared plan vs. observed filesystem activity, as a live local map comes from sodiumsun/agenttrail. driftline is a from-scratch rebuild with a different set of architectural trade-offs, listed below.

Architecture

bin/driftline.mjs      CLI entry: `init` and `--open`
lib/server.mjs         HTTP + SSE daemon, binds 127.0.0.1 only
lib/store.mjs           node:sqlite event log + PLAN.md version history
lib/watcher.mjs         fs.watch observer + the shared ignore filter
lib/planParser.mjs      Hand-written grammar for PLAN.md (throws with line numbers)
lib/adapters.mjs        Canonical event schema + per-agent translators
lib/install.mjs         `init`: hooks + starter plan, additive/idempotent
public/index.html       Single-file dashboard: file tree + map + live feed
test/planParser.test.mjs

What changed from the original, and why

  • Persistent SQLite event log instead of in-memory fading state. Uses node:sqlite (built into Node 22+, no npm install required) so the zero-dependency philosophy survives. You get restart-proof history — and everything below is that decision being spent.

  • Replay, and it's honest about the past. The log also keeps a copy of PLAN.md every time its content changes (plan_versions, deduped by hash). So scrubbing the timeline doesn't project today's plan onto yesterday's activity: components that hadn't been declared yet are genuinely missing, and tasks show the status they had at that instant. Press play and watch a component get added mid-session.

  • "Is it stuck?" as a first-class state. A component declaring [~] working whose files: haven't moved in ten minutes is marked stalled 26m — amber card, amber accent, counted in the legend. The declared status and the observed activity are both already there; nothing else seems to bother comparing them.

  • Coverage: the plan checked against reality. Run the glob matcher backwards over observed writes and the ones no component claims are, by definition, work nobody declared. The Coverage tab puts a number on it — "52% of the 23 observed writes landed in files a component declares" — lists the unmapped directories, and hands you a paste-ready component stub for each.

  • Sessions instead of an undifferentiated stream. session_id was on every row and used for nothing. Rolled up, it answers "what happened while I was at lunch". The fs watcher can't attribute a write to an agent, so observed writes are folded into whichever agent session was live at that moment rather than pretending to be a session of their own.

  • A real grammar for PLAN.md, not regex scanning. lib/planParser.mjs is a small tokenizer/parser with explicit rules. Malformed input throws a PlanParseError with a line number instead of silently mis-rendering the map. It's unit tested (test/planParser.test.mjs) — the convention itself is now a contract, not tribal knowledge.

  • Adapter interface for agents. lib/adapters.mjs defines one canonical event shape; each agent gets a registerAdapter(name, translate) function. Claude Code's hook payloads are translated in ~30 lines. Adding Codex, Cursor's own hook format (if/when it exposes one), or a custom in-house agent means writing one adapter file — the daemon, store, and UI never change.

  • Deterministic layered layout instead of hand-placed nodes. Columns come from topological depth on needs:; row order inside each column comes from a barycenter sweep, the same crossing-reduction idea a Sugiyama layout uses, in about thirty lines and with no D3/dagre dependency. Past ~15 components that ordering pass is most of the difference between a readable graph and a hairball.

  • The cards are DOM; only the wires are SVG. Components are absolutely positioned <div>s inside one transformed viewport, with an SVG layer behind them for edges. Progress bars, expandable task lists, hover states and text ellipsis are then just CSS, and the edge layer stays cheap enough to redraw on every drag frame.

  • Observed writes are attributed back to declared components. Each component's files: globs are compiled once, so a write to backend/twin/rc.py lights up the card that claimed it. If a component says [x] done and its files keep changing afterwards, it gets a drift badge — that disagreement between declared and observed is the reason the tool exists, so the dashboard names it instead of leaving it for you to notice.

  • Still local-only, still no telemetry. Binds 127.0.0.1 explicitly (never 0.0.0.0). Optional --token support is the natural next step if you ever run this on a shared machine.

The PLAN.md convention (unchanged from upstream, still the good part)

## Capture the audio {#capture}
tech: coreaudio tap + ring buffer
files: [src/audio/**]
- [x] Grab the mic feed {#capture-mic}
- [~] Keep the last 30 seconds ready {#capture-ring}

## decisions
- 2026-08-21: dropped redis for summaries; in-process queue instead

[ ] todo · [x] done · [~] working · [!] blocked needs: draws a dependency arrow · links: draws a dashed connection

Extending

Add an agent adapter — in lib/adapters.mjs:

registerAdapter('my-agent', (rawPayload) => ({
  ts: Date.now(),
  agent: 'my-agent',
  sessionId: rawPayload.session,
  kind: 'tool_end',
  tool: rawPayload.action,
  file: rawPayload.path,
}));

Then add a route in lib/server.mjs that POSTs incoming payloads through it.

Run the tests:

npm test

Ignoring noisy paths

.venv*, venv*, __pycache__, .pytest_cache, .mypy_cache, .tox, site-packages, htmlcov, coverage, and *.pyc/*.pyo are ignored by default alongside the usual node_modules/.git/dist/build set. Patterns support * and ? (within a path segment), so the *.log the starter file suggests actually works and the watcher and the dashboard's file tree share one filter, so the tree can never show you a path the feed would never mention. driftline init also drops a .driftlineignore file in the repo root one path per line, # comments allowed for anything project-specific still showing up in Live Activity. Already ran init before this existed? Re-run it — it's additive and won't touch your PLAN.md.

The dashboard

Three panes over one event log. Picking something in any one of them filters the other two:

  • Files the repo tree, coloured by how recently the event log saw each path change. Click a file or directory to scope the activity feed to it and highlight the components that declared it. Touched narrows the tree to just the paths that have actually moved.

  • Map PLAN.md as a dependency graph. Click a card to focus it: its wires light up, everything unrelated recedes, and the right pane becomes a detail view (tasks, dependencies, dependents, declared globs, and the activity matching those globs). The N tasks button expands the task list inline on the card.

  • Activity the observed stream, with repeated hits on one file coalesced into a single ×n run instead of forty identical rows. Filter by kind, search by path/tool/agent, and click a #component tag to jump to that card. A 60-minute sparkline sits above it.

  • Coverage how much observed work the plan actually claims, and which directories it doesn't. Click an unmapped area to scope the feed to it, or stub to copy a component for it.

  • Timeline the scrubber along the bottom. Density of events across the window, agent sessions as bands underneath, and a tick wherever PLAN.md itself changed. Drag it and the whole dashboard — map, feed, file heat, coverage, and every relative timestamp — rewinds to that instant. Space plays it back; Return to live (or Esc) rejoins the present.

The status legend doubles as a live tally and a filter — including drift (done, but the files kept moving) and stalled (working, but nothing has moved in a while), which only appear when something is actually in that state.

Keyboard: F fit · + - 0 zoom · T files · A activity · / search · Esc clear the current focus. Drag a card to place it by hand; card positions and the pan/zoom persist per repo, and Re-arrange discards them and re-runs the auto-layout.

The header pill answers "is anything actually happening": Working while events arrive, Watching plus how long it's been quiet once they stop, and reconnecting… if the daemon goes away.

The demo animation

The GIF at the top isn't a screen recording. npm run demo regenerates it from scratch:

  1. a fixed fixture (tools/demo-fixture.mjs) is written straight into a throwaway store with chosen timestamps — the same 38 minutes of history every run, including two PLAN.md revisions
  2. the recorder drives the replay playhead itself, frame by frame, so the animation is a function of frame index rather than of how fast the machine can screenshot
  3. frames are encoded to GIF in-process (tools/lib/), so this needs no ffmpeg and no npm packages — same zero-dependency rule as the product

Change the UI, run it again, get the same demo with the new look. The encoder is unit-tested by round-tripping its own output back through an independent decoder (test/gif.test.mjs).

Not yet built (natural next steps)

  • --token flag wired through to the dashboard's EventSource for shared-machine use
  • Codex/Cursor adapters (they don't yet expose hook systems as rich as Claude Code's, so today they're covered only by the fs watcher)
  • Replay currently loads its window of events in one request. That's fine for a day of work and would want paging for a month of it.
  • Coverage counts writes, not lines. Weighting by diff size would sharpen it, but no adapter reports that yet.

License

MIT

About

The map your agent said it was building next to the one it actually built

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages