git-span
Guides

Reconcile drifted spans

Under an agent session, most positional drift never reaches this guide — the touch hook heals a pure line-shift the moment the edit lands, and the commit advisor re-runs a scoped drift --fix before classifying a changeset. What follows is for what neither can heal automatically: genuine semantic drift (content that no longer matches what a span asserts), a held command's checklist, or reconciling outside a hooked session entirely (a human-authored commit, CI, or a harness without the hooks wired up). See Agent integration for the hook mechanics.

Run the scan

Run git span drift to check every span in the repo for drift:

git span drift

With no drift, it prints a summary line (0 drift across N spans (K anchors checked)) and exits 0. When drift is found, it exits 1 and lists each drifted span with its anchors:

## checkout-flow
- api/charge.ts#L3-L5 — changed in the working tree
- web/checkout.tsx#L4-L6 — moved to web/checkout.tsx#L6-L8

Checkout submit flow that carries a charge attempt from browser to server.

A drifted span's listing includes every anchor on that span, not just the drifted ones — an anchor with no drift is printed with no status suffix — and closes with the span's why. Read each finding before acting: changed in the working tree means the anchored content itself differs; moved to <path>#L<start>-<end> means the same content shifted to a new range. A whole-file anchor only ever reports changed — any edit to the file, including adding or removing lines, counts as a content change.

Auto-resolve what's safe

Run git span drift --fix to re-anchor what's mechanically safe:

git span drift --fix

--fix re-anchors Moved anchors — the same content at a shifted line range or a renamed path — and whitespace-equivalent Changed anchors, cases where the anchored content was reformatted (indentation, whitespace) but is otherwise identical. The equivalence is judged on the whole anchor: if the range mixes a reformat with even one non-whitespace edit, the anchor counts as meaning-altering and --fix leaves all of it alone. Those anchors report resolved, pending commit — auto-updated on the --fix run itself, and resolved, pending commit on later drift runs until you commit the .span/ rewrite. That status is not drift — it's tallied with fresh anchors and does not affect the exit code.

--fix never touches meaning-altering Changed drift — content that differs beyond whitespace is left for you to resolve by hand, and re-running --fix again does not help. --fix only works with --format human, and it never commits: it just rewrites .span/ in the worktree.

After running --fix, re-run git span drift and treat anything still listed as drift as needing manual re-anchoring. See Re-anchor after an edit for the git span add / git span remove steps.

Resolve what's left by hand

For each anchor still drifting after --fix, decide which side of the coupling is authoritative, then act:

  • The anchors still agree. The edit changed the content without breaking the coupling. Re-anchor with git span add: if the content changed in place, re-add the same address — add resolves the existing anchor in place ("Added 0 anchors and resolved 1 in place"); if the range moved, swap the old anchor for the new one with git span replace. Follow Re-anchor after an edit.
  • The anchors now disagree. Conform the side that a confirmed authority or satisfied gate decides, then validate any code change. Without a confirmed contrary authority, docs follow deliberate committed code. After a gate transition, revise or retire the why and every superseded anchor. Surface ambiguous authority instead of recording a contradiction as the new baseline, and require scoped zero drift either way.
  • Coupling is gone. Remove just that anchor with git span remove, or delete the whole span with git span delete if none of its anchors are still meaningful.

Each git span add (and git span why write) ends with a scoped post-write check over the touched span, so the mutation output itself carries the reconciliation verdict: the requested-address lines (added / resolved in place / unchanged) state only the local fact, then the span-wide line asserts the whole-span state — 0 drift across ..., N anchors drifted — ..., state indeterminate (index changed during check), or state unverified. The exit code matches drift's contract: 0 = clean, 1 = drift remains or the check errored, 2 = indeterminate (index changed during check) — retryable, so re-run the command. Old anchors the mutation leaves drifted are printed with their canonical address and a runnable git span remove next action. Read the mutation's span-wide line before committing; the trailing git span drift <name> stays the final confirmation.

Before deciding, recover the original intent:

git span why checkout-flow
git span history checkout-flow

why shows the current decision and may name an authority. history walks the span's commit-by-commit timeline, including each anchor's recorded content and live drift. Use both to establish intent and provenance; a commit explains a change but does not override an explicit, still-valid authority.

Commit the result

Review what --fix and your manual re-anchors changed, then stage and commit — the .span/ rewrite together with the source edits that caused the drift, usually in the same commit:

git diff .span/
git add .span <changed source files> && git commit

An anchor that --fix resolved keeps showing resolved, pending commit until both sides are committed — committing .span/ alone while the drifted source content stays uncommitted leaves the status in place.

If a commit was denied

Under a hooked agent session, git commit/git push can come back denied instead of landing, with a checklist describing exactly the debt above. Resolve it the same way — the checklist names each drifted span or uncovered file — then retry the identical command. Both denial kinds are consider-once: semantic-drift and uncovered-writes denials each deny once per distinct debt state, and an identical retry proceeds. Editing a span's anchors changes the findings and earns one fresh deny. See Agent integration for the full denial-message shapes.

Merge conflicts in .span/

A merge can leave a span file with conflict markers. git span drift reports the span itself with a — conflict suffix in human output (CONFLICT in the machine formats), and git span show refuses to render a span in that state.

git span drift --fix resolves what it can structurally — splitting the conflict markers into ours/theirs and merging anchors that don't genuinely disagree — but only when every source file the span references is itself free of conflict markers (the clean-source precondition). It also resolves a renamed or deleted source path automatically: if an anchor's path is missing on one side and exactly one anchor on the other side shares its exact line range at a different, readable path, --fix drops the dead anchor and keeps the live one, re-pointed and re-hashed against the worktree — no manual step needed. Three cases stay manual:

  • If the span's why text diverged between the two sides with no common merge base, --fix writes the anchors clean but leaves the why's conflict markers as residue, and the file stays unmerged for you to sort out by hand.
  • Any anchor whose source file still contains live <<<<<<< markers is reported as a conflict and left untouched — resolve the source file first, then re-run --fix.
  • A renamed or deleted source path with no readable same-line-range counterpart on the other side, or more than one, is reported as a conflict — the warning names the unreadable path (and, when ambiguous, the candidate paths it could not choose between).

After a --fix during a merge, always re-run git span drift — a --fix exit code of 0 does not guarantee the span came out fully clean. Also re-check git span why <name>: a merge resolution can leave the why empty even when both sides carried the same text; if it's blank, re-set it with git span why <name> "...". Git still expects you to finish the merge with git commit.