Evidence buffer¶
Audience: users and operators inspecting or troubleshooting captured verification evidence.
<resolved-state-dir>/.evidence-buffer/ is a transient, append-only directory used by the
shell-free anvil hook dispatch capture-evidence hook to record command output
between the moment a verification command runs and the moment anvil submit
packages that output into a durable evidence.submitted event. The legacy
capture-evidence.sh wrapper delegates to the same capture command when it is
available.
Only a fixed set of verification commands is captured: the hook matches on
pytest, ruff check, mypy, npm test, cargo test, and bun test
(bin/src/anvil/cli/hooks.py's _VERIFICATION_PATTERNS; the legacy wrapper
mirrors it). Any other bash
command exits the hook silently and is never written to a buffer file — it
is not just "not a verification command," it leaves no trace at all. A
future phase may move this list to config.
Documented as part of closing tech-debt-backlog CL-15 (originally flagged in PR #41).
Format¶
Each file is JSON: one record per line in append-only *.json files, keyed
by the active claim ID. The hook writes one file per claim:
.anvil/.evidence-buffer/
├── 4F2A.json # claim 4F2A's captured commands
├── 7B91.json # claim 7B91's captured commands
└── orphan.json # commands without an exact active claim/owner/session pin
Each line in a file is one JSON object:
{
"kind": "command",
"timestamp": "2026-05-25T14:23:00+00:00",
"command": "pytest tests/ -v",
"exit_code": 0,
"output_sha256": "9f3c...a1",
"stdout_excerpt": "...up to 4000 chars...",
"stderr_excerpt": "...up to 4000 chars...",
"actor": "agent-x",
"claim_id": "4F2A",
"attribution": {
"schema_version": 1,
"project_id": "P1",
"claim_id": "4F2A",
"generation": 1,
"claimed_by": "agent-x",
"task_id": "T012",
"task_revision": "<64 hex>",
"prd_id": "default",
"prd_revision": 1,
"repository_id": "<64 hex>",
"claim_start_sha": "<40 or 64 hex>"
},
"semantic_digest": "<64 hex>"
}
output_sha256 is the SHA-256 of the full (untruncated) stdout+stderr. The
shell-free dispatcher computes it while reading the full payload; the legacy
wrapper computes it before transporting bounded excerpts to the CLI. It lets the
CommandProof attest to output that was never fully persisted. kind is
written by the hook. The submit-side reconciler (_read_command_proofs in
packet_apply.py) reads at most 16 proofs and 1 MiB, then validates the exact
claim_id, attribution, and
domain-separated semantic_digest before importing the line. It silently
skips malformed, cross-claim, tampered, and historical unattributed records
rather than failing submit.
stdout_excerpt and stderr_excerpt are truncated to 4000 characters each
to keep buffer files small and JSONL-friendly. Truncated outputs are still
useful for the sentinel — full output should be saved separately if the
agent's flow needs the long form.
Lifecycle¶
| Step | Who | Effect |
|---|---|---|
1. Agent runs pytest (or other verification command) |
Bash tool | PostToolUse hook fires |
2. The dispatcher calls anvil hook capture-evidence directly (the legacy wrapper shells to it) |
Hook | With exact ANVIL_CLAIM_ID, ANVIL_ACTOR, and session ownership, one attributed JSON line is appended to <claim-id>.json; Git claims use their immutable claim context, while non-Git claims bind the current task/PRD snapshot and omit repository fields. Otherwise a descriptive line goes to orphan.json. |
3. Agent runs anvil submit T012 --commands "pytest" --files-changed ... |
CLI | Reads <claim-id>.json, parses each well-formed line into a CommandProof, and embeds them in the evidence.submitted event's proofs field |
4. submit --output-file provided directly |
CLI | The buffer is bypassed; up to 8000 characters become a descriptive output excerpt. This never creates a typed proof or satisfies required_proofs. |
5. submit --command-proof-file ARTIFACT provided |
CLI | The bounded claim-bound artifact is validated against the explicit active claim, actor, generation, task/PRD revision, repository, cwd, command, timestamps, exit code, and output digest before it is imported as a typed proof. Repeat the flag for a batch. |
Submit is read-only with respect to the buffer: it turns the transient
buffer into the durable evidence.submitted JSONL event but does not
delete, truncate, or rotate the buffer file afterward. <claim-id>.json
persists on disk exactly as it was — submitted lines and all — until the
user manually removes it (or the whole .evidence-buffer/ directory).
submit also auto-releases the active claim (the CLI's human-readable output
prints Claim ID: <claim-id> (auto-released)), so there is no such thing as
a second submit reading the same claim's buffer file — a task with no
active claim fails submit outright (no active claim found for task '...'.
Run \anvil claim ...` first., exit 1). Re-claiming the task afterward mints
a brand-new claim ID, so any further hook-captured commands land in a new
External claim-bound command-proof artifact¶
External and subagent runners can satisfy an exact typed command requirement without hook instrumentation by constructing one canonical JSON envelope:
{"envelope_id":"RUN-1","payload":{"schema_version":1,"project_id":"P1","claim_id":"C123","generation":1,"claimed_by":"agent-a","task_id":"T001","task_revision":"<64 hex>","prd_id":"default","prd_revision":1,"repository_id":"<64 hex>","claim_start_sha":"<40 or 64 hex>","cwd_relative":".","cwd_identity":"<64 hex>","command_base64":"cHl0ZXN0IC1x","started_at":"2026-08-08T18:00:00Z","ended_at":"2026-08-08T18:00:01Z","exit_code":0,"output_base64":"MSBwYXNzZWQK","output_sha256":"a92b7fdcb45e1d22fc2af4c80adc6e7fc1389ff8a694010cf5e6ff0b5ffbf1f6"}}
The root contains exactly envelope_id, payload, and optional issuer.
The payload contains exactly the fields shown. Take the claim/task/PRD and
repository values from the explicit claim response and current project status;
do not infer an owner or use another active claim. command_base64 is standard
canonical base64 of the exact UTF-8 command bytes and must decode to one exact
task required_proofs command and one exact submitted --commands value.
output_base64 is the exact reported combined output and output_sha256 is
lowercase SHA-256 over those decoded bytes. cwd_relative is . or a canonical
repository-relative POSIX path. Compute cwd_identity with the public
anvil.claims.command_proof_artifact.claim_command_cwd_identity helper, passing
the repository root, repository_id, and cwd_relative. The helper verifies
the contained directory without following links/reparse points and binds its
stable filesystem identity; do not reproduce its hash preimage independently.
Serialize with anvil.state.hashing.canonical_json_bytes: sorted keys, UTF-8,
no BOM, whitespace, trailing newline, duplicate keys, floats, or noncanonical
base64. Times use canonical UTC ...Z spelling and must fall between claim
creation and both verification time and lease expiry. Limits are 262,144 bytes
per canonical envelope, 16,384 decoded command bytes, 131,072 decoded output
bytes, 16 artifacts per batch, and 1 MiB aggregate canonical bytes for the
persisted ClaimCommandProof models. Adapters also prebound aggregate raw
envelope/base64 input before loading; the verifier and state handler enforce the
authoritative persisted-model cap. The semantic digest uses domain
anvil.command-proof.v1\0 over the typed payload
projection excluding cwd_relative. The verifier-proven cwd_identity
remains in that projection, so alternate display paths for the same working
directory cannot mint distinct semantic evidence. A configured issuer still
signs the full canonical payload bytes, including cwd_relative.
An optional configured issuer has exact shape
{"algorithm":"ed25519","signer_id":"<16 hex>","public_key":"<64 hex>","signature":"<128 hex>"}.
The signature covers the canonical payload bytes. The public key or fingerprint
must be present in ANVIL_TRUST_LIST or ~/.anvil/trust.txt. Anvil revalidates
that current trust membership and the signature both during the live append and
during event-log replay. Missing or unreadable trust configuration, or a change
that removes the signing public key or fingerprint, fails closed and aborts the
append or replay. Back up and restore the effective trust-list file
with project state, and preserve the issuer public key or fingerprint for as
long as signed proof events must remain replayable. Unsigned proof events stored
as claim_owner_self_attested do not depend on this external trust list during
replay; they remain self-attestations, not independent execution.
Import canonical JSON files with repeated --command-proof-file flags. MCP
clients base64-encode each entire canonical envelope and pass the resulting
strings in command_proof_artifacts_base64 together with cwd. Every artifact
is prevalidated before the one durable evidence.submitted append, so one bad
item imports nothing and leaves the claim active.
orphan.json accumulation¶
When a command runs without an exact active claim, owner, and session pin,
the record goes to orphan.json. The work packet's structured
update_protocol.continuation.hook_environment supplies ANVIL_CLAIM_ID and
ANVIL_ACTOR; both must be carried into the process that invokes tools. The
claim ID selects a candidate but does not authenticate it: persisted ownership
and any persisted session discriminator must also match. Orphaning commonly
happens when:
- An agent runs verification commands before claiming a task.
- An agent runs commands after the claim has been released or has gone stale.
- The packet's hook environment was not applied, is stale, or names a different owner/session.
orphan.json is currently never auto-cleaned. It accumulates indefinitely
until the user deletes it manually. Resolve the state directory first; do not
assume it lives in the repository:
anvil_state_dir="$(anvil status --path-only)"
rm -- "$anvil_state_dir/.evidence-buffer/orphan.json"
PowerShell equivalent:
$anvilStateDir = anvil status --path-only
Remove-Item -LiteralPath (Join-Path $anvilStateDir '.evidence-buffer\orphan.json')
This is a known limitation. submit --output-file can preserve an orphan
record as a descriptive excerpt, but it cannot turn that record into a
typed command proof or satisfy required_proofs. To satisfy a typed command
requirement, rerun the command while the explicit claim is active or import a
valid claim-bound artifact with submit --command-proof-file. A future
anvil evidence prune command could rotate orphan.json on a TTL
basis; tracked separately.
Sentinel interaction¶
The sentinel agent reads the per-claim buffer files when validating evidence completeness. After
submit runs, the sentinel sees the durable evidence.submitted event in
state.db and events.jsonl — the buffer file itself is still there on
disk, unchanged, but the sentinel's evidence-completeness checks work off the
durable event, not the buffer.
Cleanup policy¶
| Trigger | What happens |
|---|---|
anvil submit T012 succeeds |
<claim-id>.json for T012's claim is read and embedded in evidence.submitted; the file itself is not deleted |
anvil release T012 |
Buffer file for the released claim is not auto-deleted; it remains on disk indefinitely until a manual clean. Because buffer files are keyed by claim ID and claim IDs are never reused, no future submit reads it again — re-claiming the task writes to a new <new-claim-id>.json instead |
anvil init --force |
The entire .evidence-buffer/ directory is preserved (it's user data) |
| Process crash mid-write | Append-only JSONL means a torn line is the worst case; subsequent reads skip malformed lines |
When to manually clean¶
- After a hard reset of project state, after resolving and inspecting the exact
<resolved-state-dir>/.evidence-buffer/target. - After resolving an orphan-accumulation issue (e.g., a stuck claim was force-released and never resubmitted).
- Before sharing a project state snapshot — the buffer is transient and not part of the canonical audit log.
See also¶
bin/src/anvil/cli/hooks.py— the active dispatcher and CLI subcommand that write the buffer.hooks/capture-evidence.sh— the retained legacy wrapper for the same subcommand.bin/src/anvil/cli/packet_apply.py::submit— the read side that reconciles the buffer intoevidence.submitted.docs/hooks-reference.md— the broader hook lifecycle.