An audit trail answers one question from a stranger: why does this line exist? Below is a single agent run, recorded end to end, including the moment a gate refused its first attempt. Click any step to see the record it produced, and why each field is in the schema.
Nothing here comes from a real run: every name, hash, and timestamp is a stand-in. The schema in the right-hand column is the part to steal.
charter_sha. Without it, nobody can say which rules were in force, because the charter moves thirty times a year. It also joins every run to the eval history. Trace a quality regression back to the exact rule edit that preceded it.
billing/refund.py sha 6c1a… read
billing/order.py sha 91be… read
billing/money.py sha 0f4d… read
tests/test_refund.py sha aa20… read
CHARTER.md sha a91f… read
.claude/rules/money.md sha 77c9… read
Store path plus content hash plus the repo sha. Never the file body. Bodies bloat the trail, duplicate your source of truth, and turn a log store into a place secrets live forever. The hash proves what the agent read, and the repo already has the text.
CHOSEN extend refund_for_order(), branch on order.status
REJECTED new refund_cancelled_order() entry point
reason: duplicates the discount reconciliation
block; charter §consistency
The rejected alternative is the highest-value row in the trail, and almost no system records it. That row is what makes review possible six months later, and an engineer needs it as evidence of judgment. Capture it at plan time, and the run produces that evidence instead of an engineer remembering it.
Recording only the result makes concurrent edits unresolvable. With the pre-image hash you can prove the agent edited the file it thought it was editing. That failure appears the moment two runs touch one repo.
✗ money.integer-cents
billing/refund.py:48
- refund_total = order.total * (1 - discount_pct / 100)
^^^^^^^^^^^^^^^^^
float division on a money path. Charter §money:
"money is integer cents everywhere; percentages resolve
via Decimal, then round half-up to cents."
A gate that blocks without a record looks, from the outside, exactly like a gate that never fired. Six months later nobody can tell whether that rule is load-bearing or dead weight. Deleting dead rules is R4's job, and the record of every firing is how R4 finds them.
Record every firing, including the ones that pass. The pass count tells you a rule still earns its place. Its block count tells you the rule is doing work today.
agent received the gate output verbatim and revised:
+ pct = Decimal(discount_pct) / Decimal(100)
+ refund_total = round_half_up(order.total_cents * (1 - pct))
Two attempts, then a human. An unbounded correction loop burns budget and talks itself into satisfying the gate, not the problem. Record the budget in the trail, so "why did this escalate" has an answer that is not somebody's memory.
Four rules ran, and the trail records all four. A trail that records only failures cannot answer the most useful maintenance question. Which of our rules has not blocked anything in twelve months?
"412 passed" is the non-claim. The line that matters is the revert check: proof the new test can fail. Without it the trail records a green pipeline, which has never meant the code is correct.
A trail nobody can find is a trail nobody uses. One line in the PR body, pointing at this view, is enough. That link turns the store from compliance furniture into something a reviewer opens when a diff looks odd.
| field | why it is there |
|---|---|
| run_id | Joins everything. Generate before the first token. |
| parent_run_id | Subagents. Without it a fan-out is unreconstructable. |
| initiated_by | A human is accountable for every run. Name them. |
| model | Exact id. "the model" is not a value. |
| charter_sha | Which rules were in force. The most-forgotten field. |
| repo_sha | What the world looked like going in. |
| prompt_sha | Hash, not text. Proves sameness without storing it. |
| context_refs | path + content hash. Never bodies. |
| tool_calls[] | name, args (redacted), result hash, ms. |
| gates[] | rule_id, verdict, matched location. Passes included. |
| alternatives[] | Which option the agent rejected, and why. The rarest, most valuable. |
| retry_index | With the budget, so escalations explain themselves. |
| pre_sha / post_sha | Both. One of them is useless alone. |
| outcome | pr number, merge sha, or the reason it stopped. |
Raw prompts with customer data, and tool arguments carrying credentials or personal data. Never whole file bodies, and never full model outputs. Hash or redact at the point of capture, not in a cleanup job later. An audit store is append-only by design, which means a mistake in it is permanent by design.
Different retention per tier is what keeps the store affordable: 30 days for tool detail, forever for the step summary.
Pick a merged change from three months ago at random. Using only the trail, answer: why does this line look like this? If you name the rejected alternative, the rule that constrained it, and the accountable human, you have an audit trail. If you can only see what happened and in what order, you have logs.
Timestamps, tool names, token counts. Answers "what happened". Cannot answer "why this and not the other thing".
Gates recorded but not which charter produced them. You know a rule fired; you cannot reconstruct the ruleset it belonged to.
Alternatives, gate verdicts including passes, charter sha, accountable human. Reconstructable by a stranger.
Do not build a store. Append one JSON line per run to a file, with eight fields:
{"run_id","initiated_by","model","charter_sha",
"repo_sha","gates","alternatives","outcome"}
That is an afternoon, it fits in the repo, and it passes the replay test. Everything on this page is what you add after the cheap version has proved somebody actually opens it.
Teams build the dashboard first and the schema second. The view is beautiful, and the data under it cannot answer the only question that matters. The schema is the product, and this page is only a way of looking at it.