git-span

Reference

Span names

A span name is one or more kebab-case segments separated by /: <slug>, <category>/<slug>, or <category>/<subcategory>/<identifier-slug>. Each segment may contain only lowercase a-z, 0-9, and -, and must start with a letter or digit — no uppercase, no leading dot, no underscores.

$ git span add BadName web/f.ts
error: invalid name: `BadName` segment `BadName` must start with a-z or 0-9 (kebab-case segments separated by `/` (e.g. `<slug>`, `<category>/<slug>`, or `<category>/<subcategory>/<identifier-slug>`); lowercase a-z, 0-9, and `-`; each segment must start with a letter or digit)

A number of names are reserved because they collide with subcommands or other tokens, including add, remove, commit, why, restore, revert, delete, move, drift, tree, fetch, push, doctor, log, config, list, help, pre-commit, advice, rewrite, and hooks. This list is not exhaustive.

Anchors

An anchor is either:

  • A whole-file anchor: a bare <path>.
  • A line-range anchor: <path>#L<start>-L<end>, 1-indexed and inclusive.

A line-range anchor whose end exceeds the file's current line count is rejected:

$ git span add wsspan web/f.ts#L100-L200
error: invalid anchor: end=200 exceeds file line count (7)

The range must be well-formed, with start ≤ end. A reversed range like #L6-L4 fails anchor parsing with a misleading complaint about a missing L prefix — see Re-anchor after an edit.

Before writing any #L<start>-L<end> range by hand, run wc -l <path> and check the end line against the file's current length. To cover an entire file, prefer the bare-path form over a full-file line range — it stays valid as the file grows.

The .span/ directory

Treat .span/ as internal. Inspect span state through git span show, list, drift, and history — never by reading files under .span/ directly or reconstructing state from git log/git show. Those commands are the source of truth for what's current; the raw files are an implementation detail that can change.

At a high level, the span root holds one file per span, named after its slug — a nested name like category/slug creates a subdirectory. Each span file holds one <path>#L<start>-L<end> <algorithm>:<hash> line per anchor, a blank line, then the why as trailing prose.

A handful of helper files live alongside the span files:

  • .gitattributes — forces LF line endings for span files.
  • .gitignore — ignores generated runtime artifacts (log files) so they don't get tracked. Still written by ensure_span_dir; the current in-session hooks (see Agent integration) keep their session state outside .span/, so the file is harmless to keep around.
  • .hookignore — path-scoped suppression rules for the touch hook's context injection.
  • .advisorignore — user-owned (nothing auto-creates it); each non-comment line is a gitignore-style path pattern, same grammar as .hookignore, excluding matching paths from the commit advisor's uncovered-writes check.

git span show <name> (and the bare git span <name> form) renders a friendlier TOML-style display — name, message, [[anchors]] blocks, a [config] block — but that's a display transform, not the on-disk format.

Exit codes

CommandExit codeMeaning
Any subcommand0Success
drift0No drift found, or --no-exit-code passed while drift is present
drift1Drift found (Changed/Moved anchors), or an interior-anchor integrity violation
drift2Resolver index_changed verdict — the scan could not determine drift state; retryable
add / why (write mode)0Write succeeded and the post-write check completed and found no actionable drift
add / why (write mode)1Write succeeded but actionable drift remains, or the post-write check errored — the output says which (remains lines vs state unverified; JSON DRIFT vs UNKNOWN + reason)
add / why (write mode)2Post-write check indeterminate (index changed during check) — retryable
add / why / etc.1Operational or validation error (invalid name, out-of-range anchor, and similar); why --format json in read mode is also rejected fail-closed with exit 1
Any command2A clap usage error — bad flag, missing required argument
merge-driver0Fully resolved
merge-driver1Partial resolution (residue markers written)

The interior-anchor integrity case is never maskable by drift --no-exit-code — it always exits 1, even when no anchor drifted.

Published schemas

Every subcommand that accepts --format json publishes its output family as a JSON Schema at a stable URL, derived from the same Rust types that serialize the CLI output:

FamilyURLSubcommands
mutationhttps://git-span.com/schemas/cli/v1/mutation.jsonadd, why
resolvehttps://git-span.com/schemas/cli/v1/resolve.jsonresolve
contexthttps://git-span.com/schemas/cli/v1/context.jsoncontext
historyhttps://git-span.com/schemas/cli/v1/history.jsonhistory
drifthttps://git-span.com/schemas/cli/v1/drift.jsondrift

Each schema's $id is its URL, and the URL is the version: v1 is immutable. A committed sha256-manifest.txt beside the schemas pins every published file's digest, and CI recomputes and compares it — the manifest is never regenerated, so published bytes cannot drift. A breaking change to a family's shape lands at a v2 URL; the v1 bytes stay byte-for-byte what they were. The frozen bytes freeze the key set with them: a validator cached against a v1 schema stays correct for every output that family produces, because a round-trip test validates real CLI stdout against the committed schemas — a schema the binary violates fails CI, not a consumer's validator.

replace and tree also accept --format json, but their output is unversioned and unpublished: publishing would require adding a schema_version key to it, a change to an existing output shape. Their JSON remains an internal interface.

The schemas and the command reference are generated from the CLI itself (yarn build:schemas in packages/git-span); a staleness gate fails CI when a committed artifact falls behind the generator.

Configuration

Span root directory. Resolved with this precedence:

  1. --span-dir <path> CLI flag
  2. GIT_SPAN_DIR environment variable
  3. git config git-span.dir
  4. Default: .span

Other environment variables and config:

SettingEffect
GIT_SPAN_PERFEnables --perf timing output to stderr without passing the flag (1/true/yes/on, case-insensitive)
GIT_SPAN_CACHE=0Disables the on-disk resolver cache entirely
GIT_SPAN_RENAME_BUDGETOverrides the rename-detection budget used by history/move tracking. Default 1000

The resolver cache lives in the git common directory and is safe to delete.

See Command reference for per-subcommand flags and behavior.