Skip to content

Context And Capture: Closing The Loop

dent8’s wedge is multiple coding agents (and a human) sharing one verified fact base about one repository — the stale-CLAUDE.md problem. That only works if the store actually touches an agent’s session on both ends:

  • Inject at session start: dent8 context emits the currently-believed facts as a markdown context pack (or --output json).
  • Capture at session end: dent8 capture flushes structured fact proposals back through the same firewall path as every other write.
  • Arbitrate without setup homework: dent8 authority defaults seeds the natural trust ordering for a repository — human > CI > agent — so authority arbitration works before anyone invents a trust taxonomy.

There is no daemon and no new write path here. context is a read over the same durable store as explain/facts list; capture dispatches every proposal to the same op_* firewall layer as the interactive CLI and the MCP tools.

Terminal window
dent8 context # markdown, ready for CLAUDE.md/AGENTS.md-style injection
dent8 context --output json # machine-readable
dent8 context --kind repo # scope by subject kind/key/predicate, like `facts list`
dent8 context --include-stale # also show believed-but-stale facts, annotated
dent8 context --record-retrieval [--purpose session-start]
# also record a fact.retrieved audit event per emitted fact

Semantics, in belief terms:

  • Only currently-believed facts appear. A superseded, retracted, or expired fact is history, not context.
  • Stale and not-yet-valid facts are omitted by default (and counted in a trailer), or annotated inline with --include-stale — injected context must not smuggle in a fact past its validity window unmarked.
  • A contested fact is flagged, never silently picked: the line carries a [contested — N rival value(s)] marker, and the JSON status is contested instead of ok whenever the pack carries a live dispute.
  • Every fact carries provenance: authority, the asserting source, and its dent8://{kind}/{key}/{predicate} receipt reference — so a generated block pasted into a native memory file remains verifiable with dent8 native reconcile and dent8 explain.
  • Internal diagnostic:* streams are hidden unless --include-diagnostics is passed, matching facts list.
  • --record-retrieval closes the read-audit loop: every fact the pack emits gains a fact.retrieved audit event on its stream, stamped with --purpose (default context-pack). The audit is recorded before the pack is emitted and all-or-nothing, so an emitted pack is never un-audited; the markdown itself stays a pure context block, and --output json reports recorded_retrievals. Identity resolves like unattributed capture — the active signed grant’s source when configured, else source:agent at low — through the normal write boundary. Audit events never change lifecycle, value, or authority; dent8 replay shows them with their purpose.

dent8 capture — structured fact proposals

Section titled “dent8 capture — structured fact proposals”
Terminal window
dent8 capture # read JSON-lines proposals from stdin
dent8 capture proposals.jsonl # ... or from a file
dent8 capture proposals.jsonl --consume # ... and truncate the queue afterwards
dent8 capture proposals.jsonl --consume --keep-failed
# ... keeping rejected/malformed lines for retry

Each input line is one proposal:

{"subject": "repo:dent8", "predicate": "cli_binary", "value": "dent8"}
{"op": "supersede", "subject": "repo:dent8", "predicate": "repo.database", "value": "postgres", "authority": "high", "source": "source:human"}
{"op": "used_in_decision", "subject": "repo:dent8", "predicate": "repo.database", "decision": "chose the sqlx driver"}
  • op is assert (the default), supersede, reinforce, contradict, retract, expire, or used_in_decision; value is required for the first three and forbidden for the rest.
  • used_in_decision closes the report half of the read-audit loop: it records a fact.used_in_decision audit event (with the required decision field instead of a value) on the believed fact(s) of the subject+predicate — how an agent reports which facts informed a decision, through the queue it already writes. Audit events never change lifecycle, value, or authority, and — like dissent — are not authority-gated in the fold, so a low-authority agent can report using a high-authority fact; the write-boundary gate (source ceiling, grant scope, signed identity) still applies.
  • Authority and source resolve per line: the proposal’s own fields, then the --authority/--source flags, then the active signed grant (DENT8_GRANT), then the agent tier of the default profilesource:agent at low. Unattributed agent capture enters at the bottom of the trust ordering instead of failing or minting authority.
  • Every line is attempted; one bad proposal does not drop the rest of a session’s capture. Unknown fields and unknown ops are rejected as malformed (a typo must fail loudly, not silently misattribute a write).
  • Exit codes: 2 if any line was malformed, 1 if the firewall rejected any proposal — a safety signal worth surfacing in hook logs, not a crash — else 0.
  • --consume truncates the queue after processing; with --keep-failed, rejected and malformed lines are written back (accepted lines are still removed), so a failed proposal survives in the file for inspection/retry instead of only in hook logs. Note a kept line is retried verbatim on the next flush — fix or remove it, or it will fail again.
  • Predicate policy still applies: a registry predicate with an authority floor (for example repo.test_command at medium) rejects an agent-tier proposal, exactly as it rejects the same write from dent8 assert.

Capture writes through the local op layer, so it follows DENT8_LOG / DENT8_STORE_URL like every other command; it does not route through DENT8_DAEMON_SOCKET.

dent8 authority defaults — the shipped trust profile

Section titled “dent8 authority defaults — the shipped trust profile”
Terminal window
dent8 authority defaults

seeds the source→authority ceiling registry with:

source max authority natural role
source:human high the repository owner’s corrections
source:ci medium machine-verified observations
source:agent low agent inference and session capture

It is merge-only: an existing grant for one of those sources is kept, never downgraded (dent8 authority add still replaces explicitly). canonical stays reserved for explicit policy, because contradicting a canonical fact hard-alarms. Once seeded, the registry is deny-by-default for unlisted sources, like any other registry (STATUS.md §authority).

With the profile in place the arbitration you want falls out of the fold: an agent cannot supersede a human-asserted fact, CI can supersede agent guesses but not human decisions, and a human correction beats both.

examples/agent-hooks/claude-code/settings.sample.json ships the full wiring:

  • SessionStart runs dent8 context. A Claude Code SessionStart hook’s stdout is added to the session context, so the believed facts are injected at session start with no file to keep fresh.
  • SessionEnd runs dent8 capture .dent8/proposals.jsonl --consume. During the session the agent appends proposal lines to .dent8/proposals.jsonl (instruct it to in CLAUDE.md, or let it use the dent8 MCP tools directly for immediate writes); the hook flushes the queue through the firewall when the session ends and truncates it so the next firing does not replay it. A missing queue file is an empty session, not an error.

A minimal CLAUDE.md instruction for the queue-file variant:

When you learn a durable project fact (database, test command, convention), append one
JSON line to .dent8/proposals.jsonl:
{"subject": "repo:<name>", "predicate": "<predicate>", "value": "<value>"}
Do not edit the "Project facts (dent8)" block by hand — it is generated by `dent8 context`.

For agents with MCP configured, the MCP assert/supersede tools remain the immediate path; the proposals queue is the zero-MCP fallback that still ends at the same firewall. Non-hook setups can run the same two commands as session bookends (e.g. a wrapper script that prepends dent8 context output and pipes collected proposals to dent8 capture).

Seeded against a fresh store. The two above-agent facts (source:human @ high and source:ci @ medium) each need a signed identity — v0.8.0 rejects any write above the agent tier without one — so provision both up front (the issuer key is the one dent8 init created, default ~/.config/dent8/issuer.key). The agent’s proposals stay at the low tier and need no identity:

Terminal window
$ dent8 init --source source:human # trust root + issuer key + source:human identity
$ dent8 identity agent-keygen source:ci --out .dent8/identities/source_ci.key
$ dent8 identity grant-issue source:ci --public-key .dent8/identities/source_ci.key.pub \
--max medium --issuer owner --issuer-key ~/.config/dent8/issuer.key \
--out .dent8/grants/source_ci.grant.json
$ dent8 identity repair-env --source source:ci
$ dent8 authority defaults
seeded the default authority profile (human > CI > agent) in .dent8/authority.json:
kept source:human max=high
kept source:ci max=medium
kept source:agent max=low
the registry is now deny-by-default: unlisted sources are blocked until granted with `dent8 authority add`.

Load the store and ceiling for the shell — but not a source grant, so the agent’s unattributed proposals below land at source:agent @ low. Each signed fact is written in a subshell that loads only that source’s identity:

Terminal window
$ export DENT8_LOG=.dent8/memory.jsonl DENT8_AUTHORITY=.dent8/authority.json DENT8_REQUIRE_AUTHORITY=1
$ ( set -a; . .dent8/identity-human.env; set +a
dent8 assert repo:dent8 repo.database "postgres (operational) and embedded sqlite; file dev log by default" --authority high --source source:human )
$ ( set -a; . .dent8/identity-ci.env; set +a
dent8 assert repo:dent8 repo.test_command "cargo test --workspace" --authority medium --source source:ci )
$ echo '{"subject": "repo:dent8", "predicate": "cli_binary", "value": "dent8"}' | dent8 capture
line 1: ACCEPTED repo:dent8 cli_binary = "dent8" (authority=low)
captured 1 proposal(s): 1 accepted, 0 contested, 0 rejected, 0 invalid

The generated context pack:

## Project facts (dent8)
<!-- generated by `dent8 context` at 1783523090324; regenerate instead of hand-editing -->
Currently-believed facts from the dent8 memory firewall. Each carries its authority and source; verify one with `dent8 explain <subject> <predicate>`.
### repo:dent8
- `cli_binary` = "dent8" (authority: low, source: source:agent, ref: dent8://repo/dent8/cli_binary)
- `repo.database` = "postgres (operational) and embedded sqlite; file dev log by default" (authority: high, source: source:human, ref: dent8://repo/dent8/repo.database)
- `repo.test_command` = "cargo test --workspace" (authority: medium, source: source:ci, ref: dent8://repo/dent8/repo.test_command)

And the loop’s integrity half — an unattributed agent proposal cannot displace the human-asserted fact:

Terminal window
$ echo '{"op": "supersede", "subject": "repo:dent8", "predicate": "repo.database", "value": "mysql"}' | dent8 capture
line 1: REJECTED: firewall rejected the write: insufficient authority: low may not override or remove an incumbent of high
captured 1 proposal(s): 0 accepted, 0 contested, 1 rejected, 0 invalid
$ echo $?
1

MCP resources/read auto-records a fact.retrieved audit event (purpose mcp:resources/read) on every successful read from a write-capable connection — the same audit event as context --record-retrieval, so an agent that pulls a fact through the resource URI leaves a replayable trail without an extra tool call. Identity resolves like unattributed capture (active grant when present, else source:agent at low). Opt out with DENT8_MCP_RECORD_RETRIEVAL=0. Unauthenticated (read-only) daemon connections still return the receipt but skip the audit write. The agent-report half remains a capture proposal ("op": "used_in_decision").

  • Not a daemon: both commands are one-shot processes over the existing store.
  • Not natural-language ingestion: capture takes structured proposals only. Inferring facts from prose remains out of scope, like native scan/native reconcile.
  • Not automatic CLI read auditing: dent8 context records fact.retrieved only when --record-retrieval is passed (a whole pack is bulk retrieval; the flag keeps it intentional). MCP resources/read is the opposite default — each read is already an explicit retrieval, so it is audited unless opted out.