Concepts
Span
A span is a named, declared coupling — a group of anchors coupled by nothing a schema, type, test, or build/generator step enforces — plus a why naming what those anchors form. A span is identified by a kebab-case name (segments separated by /, each segment starting with a letter or digit — e.g. checkout/min-amount).
Anchor
An anchor is a single location a span tracks. There are two forms:
- Whole-file anchor: a bare path, e.g.
web/checkout.tsx. Tracks the entire file's content. - Line-range anchor:
path#Lstart-Lend, e.g.api/charge.ts#L4-L6. Lines are 1-indexed and the range is inclusive —L4-L6covers lines 4, 5, and 6.
A span holds one or more anchors, mixing whole-file and line-range forms freely.
The why
The why is compact, durable, decision-relevant context shared by the exact anchors. Use one or two complete present-tense clauses for the relationship and any decisive nonlocal authority, invariant, permitted difference, lifecycle state, evidence gate, or focused conditional verification. Labels are optional but must introduce complete clauses. Write it with git span why <name> "...".
Inherit the why only while it remains true. Omit generic work orders and CLI procedure. A satisfied gate authorizes its transition; revise or retire the why, reconcile or retire superseded anchors, and require scoped drift to be zero.
The .span/ directory
Span data lives in a .span/ directory at the repo root (or wherever the span root resolves to), committed alongside your code with ordinary git commands: git add .span && git commit. It is managed through the CLI — git span add, remove, why, delete — not hand-edited. The files under .span/ use an internal storage format, not the TOML-style view git span show renders; treat that internal format as private to the tool.
Drift
An anchor has drifted when the code it points at no longer matches what was recorded. git span drift reports drift with one of these statuses:
- Changed — the anchored content differs from what was recorded, in place.
- Moved — the anchored content was found at a different location (a shifted line range).
- Resolved, pending commit — a
--fixrun already rewrote this anchor in the worktree, but the rewrite isn't committed yet. This is a distinct, non-drift state: it's tallied with fresh anchors and doesn't affectdrift's exit code. Immediately after the--fixrun that produced it, it's tagged "— auto-updated"; on laterdriftruns, until you commit, it shows as "resolved, pending commit" with no suffix.
A span with any drifted anchor has its entire anchor list printed, not just the drifted ones — anchors with no drift appear with no status suffix, alongside their drifted siblings.
git span drift exits 0 on a clean scan and 1 when it finds drift (or an internal integrity violation, which --no-exit-code cannot mask). Reconciling drift starts with a decision, not a command: use the why and demonstrated intent to identify authority, with git span history and the drift-attributing commit as provenance, then conform the other side before re-anchoring with git span add. Without a confirmed contrary authority, a doc that lags deliberate committed code follows the code. Re-anchoring records the current content as the new baseline without any semantic check, so re-anchoring over a live disagreement hides the drift signal without resolving it. git span drift --fix handles the mechanically safe cases (Moved and whitespace-equivalent Changed anchors), without committing; see the reconcile guide for the walkthrough and its reliability caveats.
Span command output is ground truth. Don't re-derive a span's state by reading raw git log, git show, or the files under .span/ directly — use git span show, list, drift, and history, which account for staging, worktree state, and internal formatting that raw git output doesn't reflect.
Automation lifecycle
Beyond manual use, git-span is meant to run unattended at a few points in a repo's lifecycle:
- CI: run
git span driftand fail the build on a non-zero exit — see CI & git integration for the wiring. A plain localgit commithook works the same way for a workflow with no agent hooks at all. - Merge driver:
.span/files can conflict like any other tracked file during a merge; git-span registers a merge driver that resolves what's structurally derivable and leaves genuine same-anchor divergence forgit span drift --fixto finish. - Agent touch hook: a
PostToolUsehook (Claude Code and Codex) fires after supported file-affecting tool calls, healing pure positional drift inline and surfacing genuine semantic drift as context — it never blocks the tool call. - Agent commit advisor: a
PreToolUsehook on the shell tool holdsgit commit/git pushonce per distinct debt state when the changeset carries real span debt — semantic drift, or a changed file no span covers — so the report gets read; an identical retry proceeds. It reports rather than enforces: a scan that fails to complete holds nothing, warning that span debt wasn't verified and letting the command through.
See Agent integration for the touch hook and advisor in detail, CI & git integration for CI and the merge driver, and the reference page for the storage model and exit codes.