Session note schema
Stored under refs/notes/gitvow/<session-id> (gitvow 0.1 used the single ref refs/notes/sessions). First line gitvow-session, then JSON:
| Field | Type | Meaning |
|---|---|---|
session_id |
string | the session id the agent assigns to the conversation |
step |
int | commit number within the session |
committed_at |
string | local time the note was written |
assistant_turns_so_far |
int | assistant messages in the transcript at commit time |
tool_calls_so_far |
int | tool invocations at commit time |
tools_used |
string[] | distinct tool names |
last_stated_plan |
string | last assistant text before the commit, redacted, at most 600 characters; read from Claude Code, Codex and Gemini CLI transcripts, empty for Cursor |
files_in_commit |
string[] | git show --stat lines, at most 50 |
files_written_by_agent_this_session |
string[] | files in the commit that the agent edited or wrote via tools in this session |
attribution |
object | see below |
usage |
object | tokens by kind, models seen, estimated_cost_usd, pricing table used; see What each session cost |
subagents |
object | count and tool_calls of side conversations in the session |
snapshot |
string or null | the last snapshot ref taken before this commit, e.g. refs/gitvow/snapshots/<session>/7 |
decisions |
object[] | the findings this commit carried and how they were answered; see below |
intent |
object or null | schema 8: what the task was for, in the person's words: text (redacted, at most 200 characters), by, source (stated from gitvow intent, prompt from the first message of the session), set_at, session_id, step; see State the intent |
schema |
int | note schema version, currently 9 |
transcript |
string | always "kept local; see ledger" |
redaction |
string | statement of what redaction ran |
attribution
| Field | Type | Meaning |
|---|---|---|
files_in_commit |
int | files changed by the commit |
touched_by_agent |
int | of those, files the agent wrote or edited this session |
lines_added_in_commit |
int | sum of added lines over all files |
lines_changed_by_human_after_agent |
int | lines added or removed by a person after the agent's last write, over agent-touched files |
agent_share |
float or null | share of added lines that match the agent's version; null when the commit adds no lines or no agent-written blob was recorded |
files[] |
object[] | per file: path, agent_wrote, lines_added_in_commit, human_lines_added, human_lines_removed, agent_blob, committed_blob |
decisions[]
| Field | Type | Meaning |
|---|---|---|
finding |
string | the finding as it appears in the trailer, e.g. route /v1/x in src/api.py |
kind |
string | edit, route, route-removal, gate-file |
subject |
string | the path or route |
path |
string | the file the finding came from |
reason |
string | the rule's reason or the provider's summary |
evidence |
string[] | provider evidence, redacted, at most 8 lines |
raised |
int | how many tool calls raised the same finding in the session |
answer |
string | accepted, declined, referred, open, or observed (schema 7: an observe-tier rule recorded it and nobody was asked) |
by |
string or null | committer identity used in the trailer: email local part or name |
authority |
string | policy (named in decisions.authorities), commit-access (no list configured), none |
scope |
string or null | environment or branch the answer is limited to. A scoped answer is an exception, so it never counts towards a proposed rule |
to |
string or null | with answer: referred, who the question should go to; null when the person did not name anyone (schema 6) |
note |
string or null | the person's reason, redacted |
decided_at |
string or null | local time the answer was recorded |
human_turns_after_card |
int or null | user messages in the transcript between the card and the answer; 0 means the agent answered without a person speaking |
proposed |
string or null | what the record proposed on the card from earlier decisions on the same finding: accept, decline or null |
intent_covered |
bool | schema 8: the finding's subject shares a word with the session's stated intent (a term overlap, computed at card time; never an answer) |
owner |
object or null | schema 9: who owned the finding's path when it was raised: owners (as written in the ownership file, or the policy's authorities), source (<file>@<commit> or policy.authorities), pattern (the matching rule), unowned. Null for a finding that names no path, such as a command. See Who owns this path? |
unowned |
bool | schema 9: the path had no owner from any source |
edits_outside_repository (schema 7)
An object keyed by the name of another checkout the agent edited during the session, each with count and up to 20 paths relative to that checkout. Empty when every edit stayed inside this repository. The gate evaluated those edits; this repository's note is the only record of them.
The schema is additive. New fields may appear; existing fields keep their meaning. Consumers should ignore unknown fields. Schema 9 added owner and unowned to decisions[]; schema 8 added intent and decisions[].intent_covered; schema 7 added observed as an answer and the edits_outside_repository object; schema 6 added to to decisions[]; a note written by an older gitvow reads the same as it always did.
The claim note
Stored under refs/notes/gitvow/claims on the empty commit gitvow claims confirm|reject writes. First line gitvow-claim, then JSON (schema 1): claim_id, verdict (confirmed or rejected), by, authority (speaker when the confirmer is the person who said it, else policy, commit-access or none), decided_at, text (as recorded), original_text (as extracted, verbatim), edited, paths, reach (repo), speaker, kind, source (a pointer: kind, file, message index; never the source content), said_at, classification (the extractor's opinion), reason. The trailer carries the id, the person, the bound paths and the first 120 characters; the note carries the rest. Person-reach preferences are never written to git; they live in ~/.gitvow/claims/preferences.jsonl on the speaker's own machine.
The outcome note
gitvow outcomes (0.31) grades each decision-bearing commit by what happened to it and writes the grade under refs/notes/gitvow/outcomes: its own ref, appended beside the decision, never rewriting the session note. First line gitvow-outcome, then JSON:
| Field | Type | Meaning |
|---|---|---|
schema |
int | outcome note schema, currently 2 |
graded_at |
string | UTC time the grade was written |
window_days |
int | the revert window the grade used (outcomes.revert_window_days) |
verdict.kind |
string | merged (a pull request merged it), closed (a pull request closed unmerged), open, direct (reachable from a production branch with no pull request), unknown |
verdict.source |
string | github when the forge answered, git when history alone spoke |
verdict.pull_request, verdict.url |
int, string or null | the pull request that carried the commit |
verdict.landed_ts, verdict.landed_at |
int, string or null | when it landed: the merge time, or the commit time for a direct landing |
revert |
object or null | sha of the reverting commit, after_seconds since landing, in_window |
rework |
object or null | schema 2: the earliest later commit by another author that rewrote lines this commit added, inside rework_window_days: sha, after_seconds, path, by_other |
rework_window_days |
int | schema 2: the rework window the grade used |
decisions[] |
object[] | per decision on the commit: finding, answer, outcome (held, not_held, reworked, overridden, pending) |
See Did the decision hold? for the grading table.
The rule-decision note
Accepting or rejecting a proposed rule is itself a decision, so it is recorded the way decisions are. gitvow rules accept|reject writes an empty commit carrying the trailer and attaches a note under refs/notes/gitvow/rules — its own ref, because a rule decision is not a session. First line gitvow-rule-decision, then JSON:
| Field | Type | Meaning |
|---|---|---|
schema |
int | rule-decision note schema, currently 1 |
verdict |
string | accepted or rejected |
by |
string | the authority who decided |
authority |
string | policy or commit-access; never none, because a verdict by an unnamed person is refused rather than recorded |
reason |
string or null | the person's reason, redacted |
decided_at |
string | local time the verdict was recorded |
threshold |
int | decisions.rule_threshold in force when the verdict was made |
rules[] |
object[] | one per proposal in this verdict: finding, kind, answer, count, first, last, by[], evidence[], exceptions, exception_scopes[], expires |
rules[].evidence[] is the run the proposal rested on, oldest first, at most 8 entries of date, by, answer and sha. It is stored at the moment of the verdict so the record says not only that a rule was accepted but what was in front of the person who accepted it.
Trailers
Gitvow-Session: <session id>
Gitvow-Step: <int>
Gitvow-Accepted: <finding> by <person>[ scope=<scope>][: <reason>]
Gitvow-Declined: <finding> by <person>[ scope=<scope>][: <reason>]
Gitvow-Referred: <finding> by <person>[ to=<person-or-team>][: <reason>]
Gitvow-Open: <finding>
Gitvow-Observed: <finding>
Gitvow-Revisits: <commit>
Gitvow-Rule-Accepted: <finding> by <person>[ answer=<accepted|declined>][: <reason>]
Gitvow-Rule-Rejected: <finding> by <person>[ answer=<accepted|declined>][: <reason>]
Gitvow-Claim-Confirmed: <claim-id> by <person>[ paths=<p1>,<p2>][: <verbatim text, first 120 chars>]
Gitvow-Claim-Rejected: <claim-id> by <person>[: <reason>]
prepare-commit-msg when .git/gitvow-session.json holds a session id and a pending_commit timestamp younger than five minutes, which the PreToolUse gate sets when the agent runs git commit and PostToolUse clears afterwards. Idempotent: a message that already has Gitvow-Session: is left alone. A note is written only when the commit at HEAD carries the trailer.
Gitvow-Accepted, Gitvow-Declined and Gitvow-Referred are written for every finding decided with gitvow decide before the commit, on agent and human commits alike. Gitvow-Open is written for each finding nobody decided, on the agent's commit once the card has been shown and on a person's commit directly (policy decisions.mode open; in strict neither commit happens until every finding is decided). Gitvow-Observed is written for each finding an observe-tier rule recorded; it is neither debt nor an answer and never reaches the card. An immediate confirm nobody answered writes nothing: the command never ran. The post-commit hook clears the findings the commit carried. Gitvow-Revisits marks an empty commit written by gitvow revisit; its decision trailers answer the named commit's findings again. Gitvow-Rule-Accepted and Gitvow-Rule-Rejected mark an empty commit written by gitvow rules accept|reject; answer= says which run the verdict was about, so a later contradicting decision does not inherit it. See Decisions.
The grammar grows by new trailer names, never by new tail tokens on a name that has already shipped. Gitvow-Referred and its to= arrived together in 0.16: an older gitvow does not match the new name at all, so it reads the commit exactly as it would have before, which is the safe direction. Adding a token to a trailer that already exists is not safe, and the reason is in the pattern above — the finding is matched lazily and every suffix is optional, so an older parser that does not know the token backtracks it into the finding. Gitvow-Accepted: edit foo.go class=k7 by nikhil would parse on 0.15 as a decision about edit foo.go class=k7, silently forking the identity of every decision that carried it, with nothing anywhere reporting an error. Treat the token set of a released trailer as closed: a new fact belongs in a new trailer name, or in the note, which is versioned and genuinely additive. Within that constraint every part after the finding is optional, so a commit written by any earlier gitvow parses exactly as it did when it was made, and no existing trailer name has changed meaning. Gitvow-Rule-Accepted is deliberately not a Gitvow-Accepted: accepting a rule is not an answer to a finding on that commit, and a parser looking for decisions must not pick it up.