Rendered from docs/spec/15-the-recorder-contract.md in the Headwater corpus. Every document on this half of the site is typed by the taxonomy the descriptor names: corpus.json.

The recorder contract

Spec 5 names a component that no part of this repository holds. A recorder drives a probe session, observes it from outside, and writes a transcript of what it saw. It is the one part of the measurement layer that reaches the network. So it is not a crate of this engine, and no verb here writes a transcript.

A person who writes a recorder reads this part. Everything below is a contract that engine/crates/probe/src/intake.rs enforces, and engine/crates/probe/tests/contract.rs holds this text to the constants that carry the four closed key sets. A key added to the engine and not to the table below fails that test.

A recorder is a driver and a transform, and the channel between them is the condition

HW-DR-0059 admits a second shape of recorder. A driver starts the session and reads what the harness emits. A transform filters that stream into a transcript. The driver reaches the network and the transform reaches nothing, so the sentence above describes the driver.

A harness already tags each block of its own session log by type, and the model states no tag on any block. So a filter that keeps the tool calls and their results reads no account the model wrote of its own process. That filter is an outside observation on one condition. The log has to reach the transform through a channel that the model has no handle on.

A file on a filesystem the session reaches is not such a channel. On the host that runs this repository, every process of one user can write the live log of a session. A Bash call of that session is one of those processes. The standard output of the harness process is such a channel, because the session holds no handle on the parent's pipe. claude -p --output-format stream-json, read by the driver from that stream, is the channel the first recorder reads. A recorder that reads the log from disk states nothing about who wrote a line of it. The transcript it produces is a self-report that no key set can detect.

Both parts sit outside this engine. The transform is the second half of one component, and it stands on the path from a live session to a transcript. That is the position the paragraph above keeps out of the engine.

The values a session log omits, and the step that derives each one

A harness log carries the calls and not the rest of the contract below. Each value the log omits is derived after the session by a named step. The list is prose rather than a table, because the four tables of this part are the four closed key sets and engine/crates/probe/tests/contract.rs counts them.

  • result on a call is the content digest of the document, in the form headwater probe plan prints, and never the bytes returned.
  • path on a produced artifact comes first from the log's own write calls, and then from any path the driver names. The next section states the rule.
  • cites on a produced artifact is every identifier of this corpus that appears in the artifact.
  • findings on a produced artifact is every rule that reported over the artifact. Both are computed the same way for every probe, and neither reads a probe.
  • The six identity members are lock, tree, selection, read_set, seed and harness, copied from the plan.
  • served_version and cost_cents are derived from the provider metadata of the run, because a usage record carries token counts and not cents.

A session reads its workspace and nothing else of the host

A probe measures what the workspace gives a session. A session that can read outside its workspace measures the host instead. In the 2026-09-30 batch, 51 of 658 sessions named a path outside the workspace. 16 of 658 named a checkout of this repository. So the driver confines each session, and it refuses a session that it cannot confine.

What the session can read

tools/probe/probe-record.sh runs the harness under bwrap. The file system of the session holds these items and no other:

  • /usr and /etc, read-only, with /bin, /lib and their siblings as the host has them.
  • A private /proc, /dev and /tmp.
  • The workspace, read-write, at its own path.
  • The harness binary, read-only, at a path of its own.
  • The log directory of the run, read-write, because the intent hook writes there.
  • A configuration directory of the session, read-write.

So the session cannot read a copy of this repository, another tree of its batch, or the home directory of the host. The driver also clears the environment, so no token of the host reaches the session.

The configuration the session runs under

The configuration directory starts empty. The driver copies the credentials of the host into it, and it removes that copy when the session ends. HOME and CLAUDE_CONFIG_DIR both name this directory. So no user CLAUDE.md, skill, plugin, setting or auto-memory of the host loads.

The driver also passes --setting-sources project,local. The present arm still loads the hooks and skills of the workspace, because they are what a campaign measures. Every arm runs in the permission mode dontAsk, with one list of allowed tools, and with WebSearch and WebFetch denied.

The batches of 2026-09-28 and 2026-09-30 ran under bypassPermissions and the configuration of the host. A rate from a batch under this contract does not compare with a rate from those batches. Each transcript states this in its prose.

The init line is the check

The first system line of the session log, with subtype init, states what the session loaded. The driver refuses the transcript with exit 12 when that line shows one of these items:

  • The permission mode bypassPermissions.
  • A plugin whose path is not builtin.
  • An MCP server that the .mcp.json of the workspace does not declare.
  • A skill whose name is a skill directory of the host, or a skill that carries a plugin namespace.
  • A memory path outside the configuration directory.

A log with no init line is refused too, because it does not state what loaded. This refusal comes after the session, so that session has spent. The transcript also counts the paths outside the workspace that the session named, because a session cannot see a refusal from outside.

What a host must provide

A recording host must provide these three items:

  • bwrap, with user namespaces that an unprivileged user can create. Without them, the driver exits 12 before any harness call, so nothing is spent. No variable turns the confinement off.
  • Credentials for the harness, in .credentials.json of the user configuration of the host.
  • A batch directory outside $HOME. tools/probe/campaign.sh refuses an output directory under it.

The channel that stays open

The confinement shares the network of the host, because the session needs the provider API. So a Bash call can still reach the public repository with curl, gh api or git clone. Only an egress proxy that allows one host, or a network namespace with one route, closes this channel. That is a provision of the host, and no recorder of this repository has it yet.

The prompt is the task section, and the answer is the final line

Two values cross the boundary between a probe document and a session. The driver sends one in and it reads one out. This part fixes both, and tools/probe/probe-record.sh is the recorder of this repository that implements them.

The prompt is the body of the probe's ## Task section, word for word. The driver copies that body and sends it as the whole of the session's first turn. No other section of the probe document reaches the session, because the session runs in a sealed workspace. tools/probe/seal.sh removes the probe shelves from the workspace, and every file that names the probe. A file outside docs/ stays only if folds: in .headwater/probe.yml declares it. A fold names the probe by path or title and states no answer. The seal also removes each answer key that .headwater/probe.yml declares for the probe: a document that an earlier session wrote in answer to the task, and every line elsewhere that names that document. The driver refuses a workspace in which a file still names the probe or an answer key. Before #1229 both arms kept every probe, and one session read its own probe file and then answered. An expectation, a worked example and a paragraph of rationale are for the person who reads the probe. So a paragraph of commentary under the ## Task heading is prompt rather than commentary. It reaches the model, it changes what headwater route scores, and the probe then measures a task that nobody wrote.

The answer is the final line of the final message, compared as a set of words. The driver takes the last line of the final message that is not empty. It trims the space around that line, strips one trailing period, and folds the case. It then splits the line into words at each run of space, and it splits each declared answer in the same way. The answer is a value only where the line holds the words of one answer that the probe declares under answers, in any order. An extra word or a missing word gives no value. The driver writes the declared form of that answer. Every other message records answer: null. The recorder reads the whole set and never the expected values. So a wrong word of the set is recorded as that word, and the grader then finds it wrong. A message that states the reasoning and then puts the word on a line of its own carries that word. A message whose last line is a sentence carries no answer, and so does a message that sets the word in Markdown emphasis. The driver writes the word and nothing else, so the transcript holds no prose. A driver that searched for the word inside a sentence would read the session rather than observe it, and spec 5 puts that reading outside a recorder. So a probe with a closed answer set states the output form in its own task text.

The whole message was the rule until #980. The pilot of 2026-09-28 found that 7 of 27 sessions wrote one sentence of reasoning and then the word on its own line. The arms did this at different rates, so the rule graded the format and moved the rates unevenly. The owner ruled on #980 the same day that the final line is the answer.

The line was compared as one value until #1384. The status probe declares answers of two words, such as current HW-DR-0052. In the pilot of 2026-09-29, the present arm gave the answer in 10 of 10 sessions and the absent arm in 8 of 10. One absent-arm session ended with HW-DR-0052 current, as #1384 records, and that line states the same two facts in the other order. The rule of one value recorded no answer for it, so the rule graded the order of the words and not their content. The set rule recovers that session, and the absent arm then reads 9 of 10. The other miss ended with 0052 current HW-DR-0052, which has an extra word, and it stays no answer.

The engine fixes six members of the identity, and the recorder supplies the rest

headwater probe plan composes a selection and prints the six members of the run identity that exist before any session starts. They are the lock, the corpus tree, the selection, the read set, the seed and the harness version. The plan also prints the probes selected, the task of each one, and the documents each one examines. It prints the read set that the digest covers, one line for each document.

The recorder copies those six into the transcript without change. It supplies the four that belong to the run: the model, the served version, the wall-clock time and the realized cost. It supplies the tier and the arm, which the plan states and which a reader of the transcript alone would otherwise have to guess.

A model name is not a pin. The recorder writes the served version where the provider exposes one, and it writes the name as a name where no version is exposed.

A transcript is two fenced blocks under two headings

The probe_transcript kind requires a Run identity section and an Events section. Under each heading the recorder writes one fenced yaml block. Prose around the blocks is what a person wrote about the run, and no verb of this engine reads a word of it.

The blocks carry four closed key sets. A key outside a set refuses the whole file. That refusal enforces the rule that a transcript holds no model prose. An omission that nothing tests is a request rather than a rule. A transcript with a reasoning key beside the tool calls returns the self-report through the field the rule forbids.

The run identity, which carries twelve keys

key what the recorder writes
model the model the session ran against
served_version the served version, or the name where none is exposed
tree the corpus tree digest the plan printed
lock the taxonomy lock digest the plan printed
selection the selection digest the plan printed
read_set the read-set digest the plan printed
seed the rotation seed the caller stated
harness the harness version the plan printed
tier regression, campaign or documentation
arm present, absent, or a component arm: no-hook, no-skills, no-claude-md or mcp
at the wall-clock time of the run
cost_cents the realized cost, as a whole number of cents

Every one of the twelve is required. An identity with a member missing is a measurement that nobody can locate again. A run that recorded no cost leaves the cost of the instrument to a guess.

One event, which carries five keys

key what the recorder writes
probe the identifier of the probe this session ran
session a name for this session, distinct within the probe
calls the ordered tool calls, or no key at all
produced the artifacts the session produced, or no key at all
answer the final answer, null, or no key at all

probe and session are required of every event. An event that names a probe this corpus does not classify is dropped with a reason, and the reason is printed beside the rate. The last three keys carry the three-state rule below.

One tool call, which carries three keys

key what the recorder writes
tool the name of the tool the session called
argument the argument, which for a read is the path
result the identity of what the call returned, which for a read is the content digest of the document

A result identity and never a result. The bytes a tool returned are the corpus. A transcript that held them is a second copy of the tree it already names by digest.

The identity of a read is the content digest of the document, in the form that headwater probe plan prints. headwater probe stale compares that identity against the digest this corpus holds, so an identity of another form decides nothing.

One produced artifact, which carries four keys

key what the recorder writes
path where the artifact landed
result the identity of the artifact, and never the bytes
cites every identifier of this corpus that appears in the artifact
findings every rule that reported over the artifact, or no key at all

The log names the artifacts before the driver does. Every call of Edit, Write, MultiEdit or NotebookEdit carries its path in its structured input. The transform adds each of those paths to produced once, and it reads result, cites and findings off the file itself. A driver still names an artifact with --produced where the log carries no structured path to point at. A path the driver names and a path the log names are one entry.

The session writes in a copy of the corpus and not in the corpus. So the driver names that copy as the workspace, and the transform reads each artifact from it. A path inside the workspace is written relative to it. headwater check runs over the workspace, which is the tree that holds the artifact. A path outside the workspace has no findings key, because no check read that file.

A write through Bash stays invisible to this step. A redirect, a sed -i or a heredoc names no path in its input, so the transform cannot find the file. That write still needs --produced. A read through Bash is different. The grader splits the command of the call into shell words. An opened or not_opened predicate then reads a word that names the document, whatever command the word goes to. So ls and rm of the document count as reads of it. The target of an output redirect and an argument of tee are writes, and they do not count. The body of a heredoc is text and not a command, so a path in it does not count either. The grader expands nothing. A path that a variable, a glob or a command substitution inside double quotes makes is not visible to it.

An empty list and an absent key are two facts, and a grader is wrong without both

Four keys carry this distinction: calls, produced and answer on an event, and findings on a produced artifact.

calls: [] says the recorder watched the session and saw no tool call. An event with no calls key says that nothing watched. findings: [] says the artifact was checked and no rule reported. An artifact with no findings key says that nothing checked it.

A recorder that wrote an empty list for a thing it did not observe reports a verdict about itself as a verdict about the corpus. The direction of the error is the reason the distinction is here. Every one of those readings returns a pass, so a recorder that collapses the two turns a run that observed nothing into a clean result.

Two produced keys carry a derivation, and the boundary keeps the recorder out of the grader

cites and findings are computed rather than observed, and the two forms this contract permits are narrow.

A recorder computes each key the same way for every probe, and it reads no probe to do it. cites is every identifier of this corpus that appears in the artifact. findings is every rule that reported over the artifact. Neither reads an expectation.

A recorder that reads the probe is a grader with no fixture set and no version. That covers a recorder which writes only the identifiers one probe named, and one which writes only the rule an oracle names. Any value whose computation needs the probe belongs to headwater probe grade, which is a pure function of the transcript, the expectations and its own version.

What the engine confirms, and what it records without confirming

headwater probe record confirms five things and evaluates no expectation.

  1. The taxonomy. The lock that the transcript names is compared with the lock of this tree. A transcript that names another lock is refused whole only where this tree composes no read set for its probes. Where this tree composes one, the transcript is graded, and a read set that differs from the recorded one marks the verdicts (#1338).
  2. The identity is complete. Every one of the twelve keys is present.
  3. Membership. Every probe an event names is a classified probe of this corpus.
  4. No prose. Every key of every block is a member of one of the four sets above.
  5. A realized cost. cost_cents is a whole number of cents.

A moved lock is refused only where it can reach a verdict. The grader reads the lock through nothing but the probe documents and the documents that they examine. An answered or an opened expectation is declared in a probe document. A patched verdict reads the findings that the recorder stored when the session ran, and no rule runs again at intake. So a lock move that leaves the read set of the probes alone changes no verdict, and the transcript is read over this tree. Before #1292, every lock move refused the transcript, and a taxonomy change to a kind that no probe reads voided a whole campaign batch.

A moved read set marks the verdicts and keeps them. The read set is paths and bytes, and no lock move changes the bytes of a document. So a read set that moved is always a document edit. Until #1338, the first confirmation refused that edit when the lock had also moved, and it kept the verdicts when the lock had not. So an unrelated package publish decided whether an edit dropped the evidence. On the day #1338 was filed, 24 of 26 committed results carried no verdict for that reason. Now the transcript is graded whether or not the lock moved. The result carries one sentence that says the read set moved. That sentence also says that each verdict grades what the session did against the expectations this tree declares now. Where a probe document changed, those expectations are not the ones the session ran under. A lock move stays refused only where this tree composes no read set. That is the case where no selection is in hand, or where the recorded selection is not a part of it.

Present is not confirmed, and the difference is what the rest of this section states. Of the six members the plan fixed, this verb compares one to refuse, and it compares the read set only to mark the verdicts. headwater generate compares a second and headwater probe stale compares a third. The other three are not compared at all, and each of the three has a reason of its own.

The selection digest is compared, and the comparison is reported rather than refused. headwater generate writes the comparison into the probe result. The digest covers the identifiers of the probes selected and nothing else. So it holds still when the prose of a probe is edited. It moves when a probe is added, removed or renamed. That is the one change that makes a recorded run cover a population this corpus does not declare. A refusal there replaces a graded rate with a notice on the day somebody adds a probe. So the result names both digests, and the reader decides.

The read_set digest is compared by a verb, and no exit status carries the answer. The digest covers every probe of the selection and every document one of them examines, by path and content. So it moves when a document a session was pointed at changes. It holds still when any other document of this corpus changes. headwater probe stale recomposes it over the tree in front of the reader and reports which recorded results the change voided. That verb exits 0 on every answer it reaches. A probe result that goes stale is a fact about a measurement. An exit status that carried it would put the behavior of a model on a build.

A comparison against the tree in front of a reader does not go into a probe result. generate --check holds every committed projection to its own bytes. A result that compared a recorded digest against the current tree would change its own bytes. It would change them on every edit to a document the selection points at. The gate would then ask for a regeneration, and the staleness of a measurement would stop a merge through a derived document. A regeneration would also write something false, because it would claim the run was taken over a state that the run never met. So the result names the recorded digest, and headwater probe stale compares it. The mark that a moved read set leaves is not such a comparison (#1338). It says only that the read set moved, and it names no digest that the tree composes. So it changes the bytes once, on the first edit that moves the read set, and no later edit changes them again. generate --check then asks for one regeneration, the run names the result under results graded over a moved read set, and the regeneration keeps every verdict. The selection digest is the one comparison a result carries. It earns the place because it moves only when somebody adds, removes or renames a probe.

The tree digest is recorded and never compared. It covers every classified document of the corpus, so it moves on any edit to any document. A result that reported it would need a fresh commit after every prose change, and generate --check would ask for one on every pull request. A statement that nobody can leave standing is not a statement. The read set above is the narrow instrument that this reasoning defers to. It covers the documents that a whole-tree digest cannot separate from the rest of the corpus.

A refusal is reported by the run and not only by the file it writes. The refusal text is what headwater generate derives for a refused transcript. So generate --check regenerates it faithfully, and a corpus can carry a result with no verdict through every gate it has. One did. So the run prints a refused transcripts section that names the transcript, the confirmation that refused it and the reason.

The run fails where the corpus says that a reader may rely on the refused transcript, and nowhere else. The state facet declares a role on every value it admits. A live role says that a reader may rely on the document. That is the one state which holds the refusal. An initial role says that the recording is unfinished. A terminal- role says that the corpus keeps the recording as a record and relies on it no more. Both of those states report the refusal and leave the run green. The remedy for a refusal is a fresh recording rather than an edit. A gate that a contributor cannot clear is a gate that gets removed. Every other reading holds the refusal. Three of them land there: a document with no state, a value the vocabulary does not admit, and a role the engine cannot fold. Each one says that nothing has stated where the recording stands. HW-DR-0062 is the ruling, and it carries the cost this reading measures.

A refusal names every document that reads it, and the naming gates nothing. The state a recording stands in decides whether a refusal fails the run. It says nothing about the documents that cite the recording, or the result derived from it, because those are other documents. A retirement leaves every sentence in them where it was. So headwater generate reads the links that the graph build bound. It also names every document of this corpus that links the refused transcript or its result. The run prints that list, and the result carries it where a reader of a committed file meets it. Nothing fails on it. The remedy is a rewrite of a sentence that a person reads, and a gate over prose is a gate that no run can clear. The bytes of the result do move: a document that starts citing a refused result changes the list, and generate --check then asks for a regeneration.

The seed and the harness are provenance. A seed is a number the caller stated, and this corpus holds nothing to compare it against. A harness version is the version of the engine that planned the run. A comparison against the version that reads the run refuses every transcript on the first release.

Nothing here separates a recorded transcript from a typed one

Every value in a transcript is a value that a person can type. The lock digest is printed by headwater probe plan. So is the selection digest, and so is the read-set digest. The events are lines of YAML. No signature, no key and no witness is part of this contract. An integrity artifact that travels with the file it describes states internal consistency rather than identity.

The distinction between a recorded artifact and a written one lives in provenance.warrant. No taxonomy declares that block. warrant.value.not_permitted reads the warrant against the closed set, and no check can tell a recorded artifact from a written one. A shelf index prints the warrant of every document it covers, and the shelf that holds a transcript has no index. HW-OBL-0030 holds that gap. This contract is the sharpest instance of it. The warrant is the only field that separates the two artifacts, and it is the field with no shape.

What the design does buy is narrower and it is worth stating exactly. A rate is not a value that anybody types. headwater generate derives it from the events, and generate --check holds the committed result to the derivation. So a forged rate needs a forged event log. That is a claim about which documents a session opened, rather than a number in a summary. A reader who doubts a result reads the transcript and counts events. Every satisfied verdict names the event it came from.

How long a result stays citable, and why that period is not a number this schema produces

A published rate is a claim that a reader can re-derive. headwater generate writes a probe result from three committed inputs and from nothing else. They are the transcript, the expectations the probes of this corpus declare, and the version of the grader. The result states that in its own first paragraph. So the retention window of a transcript is the period in which a reader can still fetch those three. HW-DR-0008 commits the transcript and states nothing about how long it stays.

Storage does not bound the window, and the reason is a choice this contract already made. A tool call records the identity of what it returned and never the bytes. A produced artifact does the same. The bytes a tool returned are the corpus, which the transcript already names by tree digest. So a transcript of a session that read forty documents is forty digests rather than forty documents.

The schema fixes the keys and not their number, so no byte figure follows from it. The four tables above close what a key may be. They do not bound calls, which is an ordered list whose length is the behavior of the model. They do not bound cites and findings, which the artifact bounds rather than the session. A figure multiplied out of the key counts, the probe count and the session count sizes the identity block alone. It undercounts every transcript that carries a produced artifact. The session counts are derived and the byte size is not. headwater probe plan reads the probes from the tree, and .headwater/probe.yml declares the arms and the repetitions of each tier. Every plan that counts its sessions prints the product. A plan that refuses before that count prints its refusal and no product. The first committed transcript is what turns a byte size from a guess into a measurement. The one this corpus holds is the measurement to read it off.

Two of the three inputs never expire, and the third is the whole window. The transcript and the probes are documents of this corpus. The history of the repository holds them for as long as the repository stands, and no verb of this engine deletes one. The grader version is a string rather than an artifact. To re-derive a result at it, a reader needs the source of an engine release that carries that grader version. The reader also needs a toolchain that builds that source. So the window is the period in which somebody can build the named grader, and nothing in this part sets it.

A change to grading stales every committed result until somebody regenerates it, and a release does not. The grader version is the grader's own version and not the engine version. A change that alters a verdict or the rendered grade moves it. After that change, each committed result is a function of a version that is not the current one. A release that leaves the grading code as it was leaves the grader version as it was, so the release moves no committed result. The result names the version that graded it. A report over a set of results says how many versions the set spans rather than one average across them. Two results graded by two versions are two instruments.

headwater probe stale names no window, and it answers a different question. It recomposes the read set over the tree in front of the reader and reports which recorded results a change to the corpus voided. Age expires a result and an edit voids one. They are two facts, and neither one substitutes for the other. A window is a policy of the repository that runs the probes, and .headwater/probe.yml is where such a policy already lives.

This corpus declares its probes on one shelf and holds refused transcripts alone

docs/probes/ is the shelf that declares them, and headwater probe plan prints the count it reads from the tree. No count is written here, because a hand-kept count of a shelf drifts as the shelf grows. docs/probe-runs/ holds the recordings, and headwater generate writes a result beside each one. A recording went stale in each of the two ways this part describes. The commit that landed the first one moved the lock in the same commit, and a later change moved the lock under the second. Until #1338, the first confirmation refused both of those and most of the later transcripts on that shelf. Each of those results carried no verdict. Since #1338, a result whose read set moved carries its verdicts and the mark.

The reason the refusal stands is this part, read from the other end. A recorder observes a session from outside it, and tools/probe/probe-record.sh is the process in this repository that does. An agent that works here and writes a file about the documents it opened produces the self-report that spec 5 refuses. No check here tells that file from a recorded one. So the channel the log arrived by is what a reader trusts. What the corpus is owed is a recording taken against the lock of the tree that reads it, rather than a recorder. HW-OBL-0124 carries that debt.