| name | trail |
| description | Record decisions as commit-message trailers — Commitment (Type hypothesis|decision|constraint|goal) with Scope/Caused/Predicts/Alternatives/Resolves/Outcome; Hypothesis:/Decision: are grandfathered aliases — so reasoning reconstructs from git log (no LLM) and renders as a black-and-white decision graph plus tempo timeline in ~/.anu/trail/<repo>. `trail active --scope <path>` surfaces the commitments governing a file; `trail check`/`trail gate` flag edits to governed scope that don't honor them; trail swarm merges per-worktree trails into a disagreement matrix. Use when working as a long-running agent; whenever a commit embodies a hypothesis, a chosen direction, or the resolution of an earlier one; before editing code a commitment governs; or when the user runs `trail` or asks to see or record the decision trail. |
trail: record the reasoning behind each commit
Research forks into hypotheses; edits are the work between forks. At a genuine
fork, write what you are betting on and why in the commit that enacts it. The
record lives in commit-message trailers, so it travels with every clone and
push, and the graph rebuilds from git log with no tool required. A human
reads the fork instead of the diff; an agent resuming later sees what was
already tried, and why.
The vocabulary
Trailers go in the last paragraph of a commit message, one Key: value per
line (standard git-trailer shape; they coexist with Co-Authored-By: etc.):
| Trailer | Meaning |
|---|
Why: | The rationale. The floor for any non-obvious commit: explain the reasoning. |
Hypothesis: <slug> | Opens a testable claim. The commit subject is its title; the slug is its stable handle. |
Predicts: <observable> | What you expect to see if the hypothesis holds. Pairs with Hypothesis:. |
Alternatives: <a> | <b> | The paths you considered and did not take (pipe-separated). The roads-not-taken map. |
Resolves: <slug> | Closes a hypothesis. Goes on a later, forward commit; never amend the original. |
Outcome: | confirmed | refuted | inconclusive | abandoned. The verdict. Pairs with Resolves:. |
Evidence: <pointer> | What backs the outcome: a metric, a file, a commit. Pairs with Resolves:. |
Decision: <slug> | A chosen direction that is a design call rather than a testable hypothesis. A node, no outcome lifecycle. |
Supersedes: <slug> | This node replaces an earlier one: a pivot. |
Goal: <question> | The research question the whole trail serves. Put it on an early commit (latest wins); it anchors the progress view. |
A plain explained commit is just a good subject + Why:. A hypothesis adds
Hypothesis:/Predicts:/Alternatives:. A resolution is its own commit with
Resolves:/Outcome:/Evidence:.
Store decisions as commit trailers, not a ledger file
Parse the trail from git log rather than maintaining a side file that
can drift from the commits it describes.
Hypothesis: trailers-beat-ledger
Predicts: the whole graph reconstructs from git log alone
Alternatives: committed ledger file | separate git-notes ref
Why: keeps it basic git: zero extra state, travels with every clone
Render the decision graph from git log
Resolves: trailers-beat-ledger
Outcome: confirmed
Evidence: trail rebuilt this branch's full graph from git log
Why: the parser needs no state beyond the commits themselves
Commitments: scope, binding, and the gate
Commitment: <slug> with a Type: is the unified primitive. Hypothesis: and
Decision: are grandfathered aliases — legacy trails keep working untouched.
What a new Commitment: adds is that it governs files and must touch reality:
| Trailer | Meaning |
|---|
Commitment: <slug> | Opens a commitment (the unified node). |
Type: | hypothesis (a testable bet) | decision/constraint (a standing rule) | goal | assumption. |
Scope: <path|glob>[, …] | The files this commitment governs (dir/**, dir/, exact path, *.ext). |
Caused: <commit|path>[, …] | What it produced — must overlap the diff of the commit that creates it. |
Invalidated-By: <observable> | The condition that would prove it wrong. |
Honors: <slug>[, …] | This commit obeys an active commitment governing what it touched. |
Challenges: <slug> | This commit intends to overturn one (pair it with a superseding commitment + evidence). |
Hostile to prose (the per-type bar). A new Commitment: below its type's
bar is demoted to a note and does not govern:
hypothesis → Claim + (Predicts | Invalidated-By)
decision/constraint → Claim + Scope + Caused
assumption → Claim + Scope; goal → Claim
Even fully bound, much of a commitment stays trust: the bar can't prove the
stated reason was the operative one. It only forces the narration to touch
reality — to name the diff it caused and the scope it governs.
The loop: before you edit governed code
- Before working an area, run
trail active --scope <path> — the unresolved
commitments governing it. (It is silent where nothing is governed.)
- Classify your planned change against each:
Honors:, Challenges:, or
Supersedes:, in the commit that makes the change.
- To overturn one, never edit it away silently — emit a new commitment that
Supersedes:/Resolves: it, with Evidence:.
trail gate install adds an opt-in commit-msg hook that flags any commit
touching governed scope with no declared relation. It defaults to flag (warn,
never block); ANU_TRAIL_GATE_MODE=reject blocks instead. trail check --range <a>..<b> runs the same pass over a range, e.g. in review.
When to record (and when not to)
Record at genuine forks: choosing what to test, picking an approach over
named alternatives, accepting or rejecting a result, pivoting. Do not put a
hypothesis on a typo fix; over-recording is its own slop. The floor for
substantive work is Why:; reserve Hypothesis: for claims that could turn out
false.
Resolutions are forward-only
Never git commit --amend a hypothesis to bolt on its outcome. Make a new
commit that Resolves: it. The record of what you believed at the time must
stay true; a later truth is a later commit. This is also what keeps the trail
append-only and safe to push.
Exploring an alternative
The alternatives are already written on the fork commit, so when a path fails
you know what else to try. To actually try one, branch from that fork into a
worktree so your current path is undisturbed, then open a new hypothesis there:
gwa try-ledger <fork-sha>
Going back is plain git. The trail's only job is to make the fork findable and
remember the bet you didn't place.
Viewing
trail renders the graph to ~/.anu/trail/<repo>/index.html and opens it,
parsing git log, no LLM in the loop. trail open re-opens the last
render; trail watch is a live pane that re-renders as you commit, so you watch
the research unfold. The page shows the same trailer data three ways:
- decision graph: ancestry, what led to what, and the roads not taken.
- tempo timeline: time, when each hypothesis opened, how long it stayed
open, what is still running. Commits in each phase show as ticks; an open
hypothesis that has stayed open far longer than your hypotheses usually take to
resolve is flagged ⚠ stuck (age, not commit count: raw counts get polluted by
unrelated work that interleaves on a shared branch).
- awaiting evaluation + goal progress: open hypotheses (no
Resolves:
yet) surface up top, where a human verdict is the current bottleneck; if a
Goal: is set, a bar shows how many hypotheses are resolved against it.
Pushing publishes the reasoning; gp carries the trail with the code.
Across a swarm
When a swarm runs an agent per worktree (swarm wt, or worktrees via gwa),
each agent keeps its own trail on its own branch. trail swarm merges them into
one agent × hypothesis matrix: rows are hypotheses, columns are agents, each
cell the agent's verdict. The signal it surfaces is contradiction: where two
agents reach opposing verdicts on the same hypothesis (one confirms, another
refutes). Those rows sort to the top and are flagged ⚠: a disagreement between
agents is exactly what a human should adjudicate. Agents are discovered from
git worktree list; a hypothesis is "the same" across agents when its slug
matches, so use stable slugs.