Skip to content

Workspace Read Contract

Tools that show a workspace read it through one contract (#2536, D15 of #2524). behold, hud and agents all read the same JSON documents from the same commands, and an MCP tool wraps those documents rather than defining its own. The contract is made of the declaration format and a JSON Schema for each command’s output. One closed list of reason codes and a chant floor for each contract version complete it.

Contractchant floorDeclarationOutput schemas
10.81.0schema 1ls, graph, check, status, records, records-since, intent, intent-record, composites, points, changes, patch, work-history, agent, runs, wip, change-set, plan-summary, pr-report, run-statement and ci-last-green, each at v1

Every document carries contract, the version it follows, and $schema, its schema’s $id. Most also carry chant, the version of the chant that wrote it. A reader that knows contract 1 refuses a document with any other contract value. Given a chant older than the floor, the reader can say so and name the version to install, since that chant doesn’t write the contract at all.

Within a version, fields are only added, and readers ignore fields they don’t know. A new reason code, a removed field or a changed meaning is a new contract version. Codes can still be added to version 1 until 0.81.0 is released.

Each schema ships in @intentius/chant at src/workspace/<command>.schema.json, and the package exports it as @intentius/chant/workspace/<command>.schema.json.

CommandPrints the document withSchema $id
chant workspace ls--jsonhttps://intentius.io/chant/schemas/workspace/ls/v1/ls.schema.json
chant workspace graphalwayshttps://intentius.io/chant/schemas/workspace/graph/v1/graph.schema.json
chant workspace check--format jsonhttps://intentius.io/chant/schemas/workspace/check/v1/check.schema.json
chant workspace status--jsonhttps://intentius.io/chant/schemas/workspace/status/v1/status.schema.json
chant workspace records--jsonhttps://intentius.io/chant/schemas/workspace/records/v1/records.schema.json
chant workspace records --since--jsonhttps://intentius.io/chant/schemas/workspace/records-since/v1/records-since.schema.json
chant workspace graph --intent--jsonhttps://intentius.io/chant/schemas/workspace/intent/v1/intent.schema.json
chant workspace graph --intent --record--jsonhttps://intentius.io/chant/schemas/workspace/intent-record/v1/intent-record.schema.json
chant workspace graph --compositesalwayshttps://intentius.io/chant/schemas/workspace/composites/v1/composites.schema.json
chant workspace check --changes--jsonhttps://intentius.io/chant/schemas/workspace/changes/v1/changes.schema.json
chant workspace patch--jsonhttps://intentius.io/chant/schemas/workspace/patch/v1/patch.schema.json
chant workspace points--jsonhttps://intentius.io/chant/schemas/workspace/points/v1/points.schema.json
the workEvidence Op activityits result, or the refusal it fails withhttps://intentius.io/chant/schemas/workspace/work-evidence/v1/work-evidence.schema.json
the change-set document, from the composeChangeSet Op activityits documenthttps://intentius.io/chant/schemas/workspace/change-set/v1/change-set.schema.json
chant change-set summary--format jsonhttps://intentius.io/chant/schemas/workspace/plan-summary/v1/plan-summary.schema.json
chant components pr-plan and pr-applypr-plan.json and pr-apply.json under --output, or --jsonhttps://intentius.io/chant/schemas/workspace/pr-report/v1/pr-report.schema.json
chant workspace work history--jsonhttps://intentius.io/chant/schemas/workspace/work-history/v1/work-history.schema.json
chant workspace agent--jsonhttps://intentius.io/chant/schemas/workspace/agent/v1/agent.schema.json
chant workspace runs--jsonhttps://intentius.io/chant/schemas/workspace/runs/v1/runs.schema.json
chant workspace runs statement and runs verifyalways for statement, --json for verifyhttps://intentius.io/chant/schemas/workspace/run-statement/v1/run-statement.schema.json
chant workspace wip--jsonhttps://intentius.io/chant/schemas/workspace/wip/v1/wip.schema.json
chant workspace signers--jsonhttps://intentius.io/chant/schemas/workspace/signers/v1/signers.schema.json
chant workspace evidence verify--jsonhttps://intentius.io/chant/schemas/workspace/evidence/v1/evidence.schema.json
chant ci last-green--jsonhttps://intentius.io/chant/schemas/workspace/ci-last-green/v1/ci-last-green.schema.json

Each schema is a oneOf of a result and a failure. The records schema has a third branch for the set it prints without --kind (#2680). The set holds a result or a failure for each record kind the declaration names. A failure has error: { code, message }, plus location for the commands that read the declaration, and exits 1. A result may still hold entries that couldn’t be read, each with a reason: { code, message }. Only check fails on those (ws-020). In check, a declaration that can’t be read is the finding WSP001 with the same code, not a failure.

A workspace is identified by its declared name and a git revision (ws-016). Every result names both. The name is workspace.name, and at is the full commit id, or null for the working tree. workspace.root is the workspace root relative to the git root, so a nested workspace is told apart from the outer one.

ls, graph, check and records take --at <rev> and read from the local git object store. They need no checkout, no clean working tree and no network.

CommandWhat --at reads
lsthe declaration, and the member and group directories, at the revision
checkthe declaration and the lineage lock at the revision, and runs the declaration and kind checks (WSP001 to WSP011) on that tree. The ledger, pipeline and generated-file checks read the checkout, so they are left out
recordsthe records at the revision, and the files their pins name, as committed at the revision. For a session kind, its subject records at the revision too
records --sincethe records at --since and at --at, or in the working tree without --at, compared by id
graphthe declaration at the revision, and each member’s graph from its source at the revision. With --kind, the records and the files they pin or constrain at the revision too
graph --intentthe declaration, the region, the records and the files they pin at the revision, and the history reachable from it
graph --intent --recordthe declaration and the records at the revision, and the history of each path and member the record constrains, reachable from it

A member’s graph comes from running its chant config, so graph --at exports the workspace root’s tree at the revision to a temporary directory with git archive, links the node_modules directories installed in the working tree into it, runs each member there and removes the directory. When no member runs, nothing is exported.

Some of what --at reads still comes from the working tree, which is how it stays offline:

  • Pinned packages, and the kinds they supply, are the ones installed now.
  • The chant each member runs under is the toolchain installed now, not the one the revision pinned. A member whose config needs a package the revision used and the working tree no longer has fails with command-failed.
  • The records kind file and its schema come from the working tree.
  • The exported tree is not a git repository, so a member’s chant can’t read git history there.
  • Remote revisions and URLs are not read. Fetch first.
  • status has no --at. It reads the local chant/lifecycle branch and names the tip it read.

Work in progress is a file in a work branch’s working tree until someone commits or applies it (#3158). A records read of the working tree in a git repository says which records those are, so hud’s kept-records view and the studio’s apply list read them from chant instead of diffing git (#3160). Since the release after chant 0.101.0, contract 1 also carries the fields in the table below.

FieldOnHolds
worktreeeach recordcommitted when the file is as HEAD holds it, modified when it differs, new when HEAD doesn’t hold it. Staged and unstaged changes count alike.
checkoutthe records documentbranch (null when detached), head (the commit each worktree is judged against, null before the first commit), base (the merge base of head and the target branch, null without one), baseFrom (flag, origin/HEAD, main or master) and deleted (the kind’s record files head holds and the working tree doesn’t)
lastWriteeach recordThe last chant write of the record’s file in this working tree, {verb, by, agent, at}, while the file still holds the text that write left, and null otherwise (#3173). It comes from a journal in the git directory, a cache that can be lost.
uncommittedthe records documenttrue under records --uncommitted, which lists only modified and new records. The schema then requires checkout and a worktree of modified or new on every record.
checkoutthe status documentbranch, head, base and baseFrom, as above

A read under --at or outside git prints no checkout and no worktree. records --uncommitted refuses --at and --since, and outside git it fails with the existing code not-a-git-repository. The target that base is forked from is the one the trust policy is read at, so --base moves both. Records without worktree come from a chant older than #3160; read them as committed. The read takes no index lock and leaves the index unrefreshed. The records page has an example.

Uncommitted records, work branches and kept attempts live on one disk until someone commits and pushes. On a box that disk can be lost. chant keeps that work in one ref namespace, refs/chant/wip/<branch>, and replicates it to a remote under a policy the box block declares (#3172, ws-085). hud’s turn checkpoints and studio’s checkpoints and kept work use this namespace rather than one of their own.

A snapshot is a commit whose tree is the whole working tree: staged, unstaged and untracked files, with ignored files left out. Its first parent is the snapshot before it on the same ref, when there is one, and its last parent is the commit HEAD named. Its message carries Chant-Wip-Branch, Chant-Wip-Head, Chant-Wip-Kind (save, or pre-restore for the checkpoint a restore takes first), and Chant-Wip-Label and Chant-Wip-By when given. The ref’s first-parent chain is the branch’s checkpoint history. chant workspace wip save takes one and wip restore puts it back. Neither moves HEAD or the branch.

FieldOnHolds
branchesthe wip documenteach branch with snapshots: branch, ref, tip, and snapshots, newest first, each { commit, tree, branch, head, kind, label, by, at }
checkoutthe wip documentbranch and head of the checkout read
replicationthe wip and status documentsnull when no box declares replicate. Otherwise policy ({ box, remote, refs, on, every }), remoteConfigured, replicated (every ref is on the remote) and refs, each { ref, class, commit, replica, replicated, ahead }
box.replicateeach member in status{ remote, refs, on, every } with the defaults filled in, or null

class is work (refs/heads/chant/work/...), kept (refs/chant/kept/...), wip (refs/chant/wip/...) or ledger (refs/heads/chant/lifecycle). replica is what the remote held at chant’s last push or fetch, recorded under refs/chant/replica/<remote>/, or git’s remote-tracking ref for a branch. ahead counts the commits that nothing known to be on the remote reaches. Both reads look only at local refs and never fetch, so a reader can call them as often as it draws.

A composed member in the graph document carries a stamp, sha256: and 64 hex digits, which is what chant workspace graph keys its member cache on (ws-059). Two reads of a member with the same stamp read the same source, so a reader can compare stamps between documents to tell which members changed, without taking stamps of its own. The rule is stated here so that every reader means the same thing by it.

ReadWhat the stamp covers
working treeevery regular file under the member’s directory, by path relative to it, mtime in milliseconds and size, sorted by path. Directories named node_modules, dist or .git are left out, as is every directory whose name starts with . and, for member ., the other members’ directories
--at <rev>the commit id and the member’s directory, which stand in for the files since a commit’s tree never changes
boththe install around the member: node_modules/.package-lock.json and the lockfiles (package-lock.json, npm-shrinkwrap.json, yarn.lock, pnpm-lock.yaml, bun.lock, bun.lockb) by absolute path, mtime and size, in each directory from the member’s up to the file-system root

stamp is null when no stamp could be taken, such as for a read that observes an account. A stamp is a fact about the source only. The toolchain, the command line and the environment are also part of the cache key, so an equal stamp does not by itself mean a cached read was served. cached says that.

The root’s chant reads the declaration (ws-021). A root names its chant by pinning @intentius/chant in pins.

The declarationWho reads it
pins @intentius/chant at this chant’s versionthis chant
pins another version, installed at the workspace rootthat chant: ls, graph, check and status hand the whole command line to it and exit with its exit code
pins another version, not installed at the rootnobody: the read fails with root-chant-required (a WSP001 finding for check)
pins no chant, and minReader is this chant or olderthis chant, the reader’s own
pins no chant, and minReader is newernobody: reader-too-old

The pin is checked before minReader and before the schema, since the pinned chant may know fields this one doesn’t. The pin is read from the tree being read, so --at follows the pin at the revision. Members are never read by the root’s chant. Each runs under its own toolchain, as chant workspace build describes.

A reader without chant may parse the declaration itself against its schema, which is how a Terraform-only estate stays viewable. It should apply the same two rules: defer to the pinned chant, and refuse a minReader newer than the format it knows.

One closed list covers every command. It lives in packages/core/src/workspace/reason-codes.ts, each command’s own list is a subset of it, and a test fails when a schema or the source names a code outside it.

CodeWhereMeaning
declaration-missingerrorNo chant.workspace.json or .jsonc between the directory and the git root.
declaration-ambiguouserrorBoth chant.workspace.json and chant.workspace.jsonc exist.
declaration-unparseableerrorThe declaration is not valid JSON, or not valid JSONC for .jsonc.
declaration-invaliderrorThe declaration doesn’t match its schema, repeats a name, or --member names no entry.
placement-invaliderrorA member or group match breaks a placement rule.
reader-too-olderrorThe declaration’s minReader is newer than the chant reading it.
root-chant-requirederrorThe declaration pins another chant, and it is not installed at the workspace root.
not-a-git-repositoryerror--at, status or work needs a git repository and there is none.
revision-unknownerror--at names no commit.
live-at-revisionerror, graph--live was given with --at. A live read is of the account now, not of a revision.
environment-invaliderror, statusThe environment name can’t name a ledger directory.
dir-missingmember, ls and graphThe member’s directory does not exist.
unknown-kindmember, ls and graphNo built-in kind or pinned package supplies the member’s kind.
kind-probe-failedmember, ls and graphThe member’s directory is not what its kind reads.
no-matchesgroup, lsThe example group matches no chant project.
kind-not-runmember, graphThe member’s kind is one the per-member commands don’t run, such as other.
command-failedmember, graphThe member’s own chant graph exited with a failure, or the lexicon that reads a member through its kind’s graph block isn’t installed where the workspace resolves its pin.
output-unreadablemember, graphThe member’s chant printed something that isn’t a graph IR.
ir-version-unsupportedmember, graphThe member’s IR has a version this chant can’t read.
ledger-unreadableledger, statusReading the ledger failed, so nothing from it is listed.
ledger-malformedledger, statusSome ledger lines aren’t release records, and the rest are listed.
gates-no-ledgergate ledger, statusThe checkout has no chant/lifecycle branch, so there is no gate ledger to read.
gates-no-gate-ledgergate ledger, statusThe branch has no gate ledger for the member, because no run of it has reached a gate.
gates-ledger-unreadablegate ledger, statusReading the member’s gate ledger failed, so no gate is listed.
stewards-unreadablestewards, statusAn *.op.ts file could not be imported, so a steward it declares may be missing.
stewards-conflictstewards, statusA steward was dropped: its name, or an Op it lists, belongs to another steward.
steward-runs-unreadablestewards, statusReading an Op’s run ledger, or a ConvergeOp’s converge ledger, failed, so its last run or last tick is null.
record-unparseablerecord, recordsNo front matter, a YAML error or a value outside the JSON subset of YAML, or for a JSON kind a file that is not one object or repeats a member name.
record-schema-invalidrecord, recordsThe record’s front matter, or its JSON object, doesn’t match the kind’s schema.
record-id-duplicaterecord, recordsAn earlier record in path order has the same id.
record-supersedes-unknownrecord, recordsA supersedes link names an id no record has.
record-supersedes-conflictrecord, recordsA second closed record supersedes one another record already superseded.
record-remediates-unknownrecord, recordsA remediates link names an id no record has.
record-remediates-not-closedrecord, recordsA remediates link names a record that isn’t closed; a record still open is amended instead.
record-seal-mismatchrecord, recordsA closed record’s seal is not the whole-file seal of its text now: the record changed after it closed.
session-seal-mismatchrecord, recordsA closed session’s seal is not the whole-file seal of its text now: the session changed after it closed, or was sealed by the rule before #2546.
session-verdict-unknown-recordrecord, recordsA session’s verdict names a record that none of the session kind’s subject records has.
asset-driftwarning, recordsA file the record pins by hash has changed: its bytes no longer hash to the pinned sha256. The record stays valid.
asset-missingwarning, recordsA file the record pins by hash does not exist in the tree read. The record stays valid.
asset-stalewarning, recordsA file the record pins is unchanged at the hash a record it supersedes pinned: the decision changed and the artifact did not follow. The record stays valid.
record-supersedes-pendingwarning, recordsA supersedes link from a record whose state is weaker than the record it names, so the link has no effect yet. The record stays valid.
record-no-evidencewarning, recordsThe record’s evidence list is empty: it cites nothing and pins no file, as a decision made in a product’s own design flow may. Information for a reviewer. The record stays valid and --current lists it.
review-undigestedwarning, recordsA verdict in the record’s reviews names no digest of the text it judged. It still counts, and an amendment won’t stop it counting. The record stays valid.
source-transcript-driftwarning, recordsThe record’s source block pins a transcript by hash, the file it names can be read here, and its bytes hash to something else, so it isn’t the transcript the record means (#2708). A transcript that can’t be read gives no warning. The record stays valid.
work-needs-unknownwarning, records and graph --intentA work record’s needs names a work id no record has, so the item stays blocked. The record stays valid.
work-implements-unknownwarning, records and graph --intentA work record’s implements names a decision id no decision has. The record stays valid.
work-needs-cyclewarning, records and graph --intentA work record needs itself through its needs links, so it can never be ready. The record stays valid.
work-implements-undecidedwarning, records and graph --intentA work record implements a decision whose state is not approved, such as proposed. The record stays valid.
work-done-unpinnedwarning, records and graph --intentA work record is done and its evidence list is empty, so nothing shows the work was done. A work record never carries record-no-evidence.
work-closed-without-datewarning, records and graph --intentA work record is done or dropped and has no closed_on.
work-done-gap-openwarning, records and graph --intentA work record is done, and the finding its source names still fires on its region. records walks that region with graph --intent to raise it, when the repository is in git and has a workspace declaration.
work-acceptance-unmetwarning, records and graph --intent; finding, checkA work record is done, and one of its acceptance criteria has no passing evidence of the verification it expects (#2772). check fails on it as WSP117.
work-acceptance-self-verifiedwarning, records and graph --intent; error, workEvidenceA passing manual verdict names the record’s implementer, so it does not count. workEvidence refuses every manual criterion, since the run holding the lease is the implementer.
work-contract-unknownwarning, records and graph --intentA work record names a contract that no record of its kind’s contract kind has (#3147). The record stays valid.
work-contract-undecidedwarning, records and graph --intentA work record names a contract whose state is not approved, such as a draft. The record stays valid.
work-tier-unknownwarning, records and graph --intentA work record names a builder tier that its kind’s work.tier.tiers doesn’t list. The record stays valid.
answer-points-unreadablewarning, records and pointsThe points file the answer kind names can’t be read, or is not valid (ws-058).
answer-point-unknownwarning, records and pointsThe answer’s point is not declared in the points file the answer kind names.
answer-point-changedwarning, records and pointsThe point’s declaration changed since the question was asked, so the answer is to an older version of the question.
review-deciderverdict, recordsThe verdict is the decider’s own, and the quorum counts verdicts besides the decider’s.
review-agentverdict, recordsThe reviewer holds the agent role in .chant/trust.json at base.
review-duplicateverdict, recordsA later verdict by the same principal replaces this one. Names are compared after NFKC, trimming and lower-casing.
review-older-digestverdict, recordsThe verdict’s digest is not the record’s digest now, so the record changed after the verdict.
review-unattestedverdict, recordsAn attestation policy is active at base, and the verdict carries no seal that verifies for its reviewer. The verdict’s attestation says which of the seal codes below applies.
seal-missingseal, recordsThe verdict, or the record, carries no seal.
seal-signer-unlistedseal, recordsThe reviewer, or the record’s author, has no key in the signers file at base, so the seal can’t count.
seal-signature-invalidseal, recordsThe seal is malformed, names a signer other than the reviewer or author, or its signature doesn’t verify over the verdict or record.
seal-unverifiableseal, recordsNothing here can say whose seal it is: there is no signers file at base, or ssh-keygen isn’t installed.
record-unattestedwarning, recordsA signers file is active at base, and the record names an author whose seal doesn’t verify. The record’s attestation says which seal code applies. The record stays valid.
kind-unreadableerror, records and work; reason, lsThe record kind file is missing or could not be imported.
kind-invaliderror, records; reason, lsThe record kind file exports no recordKind, or its shape is wrong.
schema-unreadableerror, records; reason, lsThe schema file the kind names is missing or isn’t JSON.
schema-id-mismatcherror, records; reason, lsThe schema’s $id differs from the id the kind names.
schema-invaliderror, recordsThe record schema doesn’t compile.
location-missingerror, recordsThe records directory does not exist, in the tree or at the revision. With --since, a directory missing at the --since revision is read as empty.
write-usage-invaliderror, records new, amend, review and workThe command line lacks a value the write needs, or gives one it does not take.
write-input-invaliderror, records new and amendThe fields given with --from or --set can’t be read, aren’t JSON, or aren’t a JSON object.
record-not-founderror, records amend and reviewNo record of the kind has the id given.
record-id-takenerror, records newThe id given for a new record is already used, by a record or a file name.
record-id-unallocatableerror, records newNo id was given and none can be allocated: the records share no single prefix and --prefix names none.
record-path-unmatchederror, records newThe file name made from the id and title doesn’t match the kind’s location.match.
record-closederror, records amend and reviewThe record is in a closed state, so nothing in it changes. A new record supersedes it instead.
amend-id-immutableerror, records amendThe amendment changes the record’s id, and ids are never renumbered.
amend-supersede-insteaderror, records amendThe record is approved, and the amendment changes a field the approval rule doesn’t let change in place. A new record supersedes it instead.
review-unsupportederror, records reviewThe kind’s schema has no reviews field, so its records take no review.
review-note-requirederror, records reviewA dissent was given with no note.
review-sign-failederror, records review--sign was given and no seal could be made: the key can’t be read or used, git names no ssh signing key, or ssh-keygen isn’t installed.
ratify-quorum-not-meterror, records new and amendThe write puts a record in its kind’s ratified state (reviews.ratified), and the record’s quorum isn’t met: too few agreeing verdicts count.
record-sign-failederror, records new and amend--sign was given and no author seal could be made: the record names no author, the key can’t be read or used, git names no ssh signing key, or ssh-keygen isn’t installed.
source-harvest-not-proposederror, records newThe fields’ source block says via: "harvest" and the state isn’t the kind’s first, such as proposed: a harvest proposes, and a person decides (#2708).
record-state-not-initialerror, records new through chant serve mcpThe fields give a state other than the kind’s first: a record written through MCP opens proposed, and a person moves it on (#2707).
write-scope-membererror, records new, amend, review and close; finding, check --changesThe write is to a file, or a record kind, of a member outside the writer’s write scope: an agent session writes only the members it is bound to, and writeScope.<class>.members leaves the member out (#2548).
write-scope-kinderror, records new, amend, review and close; finding, check --changesThe write is to a record kind writeScope.<class>.records doesn’t list, with a verb it doesn’t list for the kind, or deletes a record.
write-scope-class-unknownerror, records new, amend, review and close; finding, check --changesThe declaration’s writeScope at base names a principal class no pinned package supplies, and the writer is judged human, so it may be in that class. The write is refused until the package that supplies the class is installed at the pinned version, or the entry is removed (#3080). A writer in a core or known domain class is judged by that class instead.
write-scope-protectedfinding, check --changes; error, box listing setThe write is to a file writeScope.<class>.protected lists, or one under a directory it lists, and the change isn’t one the entry’s except allows: a change only to the JSON file’s listed top-level keys, or to the values its JSON Pointers name (#3146, #3308). A record write is never refused with it; records are judged by writeScope.<class>.records.
agent-unknownerror, records new, amend, review, close and agent; finding, check --changesCHANT_AGENT, or a commit’s Chant-Agent trailer, names an agent session the declaration at base doesn’t declare.
session-unknownerror, records review--session names no session of a session kind whose subjects are the record’s kind (#2693).
session-not-openerror, records review--session names a closed session, which takes no more verdicts.
record-conflicterror, records amend, review and close--expect named a digest the record no longer has: another write changed it after the caller read it. conflict in the document names the digest it has now and its lastWrite; re-read the record and write again (#3173).
write-lock-timeouterror, every working-tree write and lock acquireAnother write held the working tree’s write lock for longer than the write waits (CHANT_WRITE_LOCK_WAIT_MS, 15 seconds by default). The message names the holder. Run the write again (#3173).
write-lock-not-helderror, every working-tree write and lock releaseCHANT_WRITE_LOCK names a batch’s token that no longer holds the write lock: the batch released it, or it expired and another writer took it (#3173).
points-undeclarederror, points ask, points answer and points retractNo record kind with an answers block is declared, or given with --kind, so there is no points file to ask.
points-invalidsource, points; error, points ask, points answer and points retractThe points file an answer kind names can’t be read, or does not match decision-points.schema.json and the rules checked in code.
point-unknownerror, points ask, points answer and points retractNo points file declares the point asked, or the point an answer names.
point-inputs-invaliderror, points askThe inputs are not a JSON object of the point’s declared inputs.
point-candidates-invaliderror, points askAn ad-hoc point was asked without --candidates, a declared point with them, or they are not a question and criteria of the point’s question type (#3403).
point-decider-failederror, points askA model decider that fails closed (unreachable: "fail") could not answer, so nothing was written.
answer-not-candidateerror, points answerThe answer is not one of the question’s candidates.
quorum-not-meterror, points answer and points retractToo few of the people named count toward the point’s quorum: distinct, not holding the agent role, not the steward that asked, and holding one of its roles when it names any.
answer-in-steward-turnerror, points answer and points retractThe answer was given or retracted during a steward’s turn, or by a process it started. A steward never answers a decision point (#2749).
answer-not-answerederror, points retractThe question has no answer to retract: it is escalated or proposed, and people answer it instead (#3351).
answer-field-unsupportederror, points ask, points answer and points retractThe answer kind’s copy of point-answer.schema.json has no note, retractions, relayed_by or asked field for what the write was given. chant refuses rather than drop a person’s note, who relayed the answer, the answer it would retract or an ad-hoc question’s text and candidates; copying the schema anew turns them on (#3402, #3351, #3403).
principal-unidentifiederror, records new, amend and review, points answer and retract, work evidenceThe declaration at base sets identity.attribution to identified, and the write names a person by a bare name, such as a hud roster name. --by, an author field and each answerer must be a forge identity (github:<login>, gitlab:<login>, <forge>@<host>:<login>), a principal the signers file at base lists, or an agent, runner or service principal (#3163).
since-rev-unknownerror, records --since--since names no commit, or names a session with no opening revision whose file no commit added.
since-session-unknownerror, records --since--since has a session id’s shape and names no commit, and no session has that id (#2693).
since-session-openreason, records --since--since names a session that is still open, so the comparison runs to the working tree.
intent-region-invaliderror, graph --intentThe region’s path, or its line range, does not exist in the tree read.
intent-record-unknownerror, graph --intent --recordNo record of a decision kind read has the id.
intent-symbol-unsupportederror, graph --intentThe region names a symbol, path#symbol, in a file no symbol resolver reads: not core’s TypeScript and JavaScript resolver, and none a lexicon of the file’s member contributes (#3313). A line range still works for it.
intent-symbol-unknownerror, graph --intentThe file does not declare the symbol in the tree read. The message lists its top-level declarations.
intent-symbol-ambiguouserror, graph --intentThe symbol matches more than one declaration in the file. The message lists their qualified names, and giving one picks it.
patch-path-invaliderror, patchA --path is not a relative path inside the workspace.
intent-history-shallowreason, graph --intentThe repository is a shallow clone, so the region’s history stops at the clone’s boundary.
intent-plugin-failedreason, graph --intentA kind file’s commitJoins failed for a commit.
squash-unfollowedreason, graph --intent, runsWith --follow-squash, a squash commit’s pull request ref is not in the clone and could not be fetched from origin, so its original commits are not followed. The read keeps its answer without them (#3035).
intent-commit-undecidedfinding, graph --intentA commit changed the region when no decision constrained it at path granularity.
intent-commit-barefinding, graph --intentA commit names no unit, carries no record through its Chant-Record or Chant-Lease trailer, and has no pull request and no decision covering the region at its time.
intent-pin-driftedfinding, graph --intentA decision’s pinned artifact no longer hashes to the pin.
intent-pin-missingfinding, graph --intentA decision’s pinned artifact does not exist in the tree read.
intent-pin-stalefinding, graph --intentA current decision constraining the region pins an artifact at the hash a record it supersedes pinned, and the artifact has not changed since.
intent-artifact-unpinnedfinding, graph --intentDecisions in the graph pinned the artifact, and no current decision pins it.
intent-decision-superseded-livefinding, graph --intentEvery decision constraining the region is superseded.
intent-decision-provisionalfinding, graph --intentThe current decisions constraining the region are all in states their kind does not close, such as decided.
intent-decision-contestedfinding, graph --intentA current decision constraining the region has an open concern: a dissent neither addressed nor withdrawn. The finding’s openConcerns gives the count and the principals.
intent-constraint-coarsefinding, graph --intentThe region is constrained only through its member, not by path.
intent-constraint-lostfinding, graph --intentA decision’s path: constraint names a path that does not exist in the tree read.
intent-evidence-unpinnedfinding, graph --intentA decision’s evidence has no hash: a URL, or a path with no sha256.
intent-trailer-unverifiedfinding, graph --intentA commit carries a trailer a plugin says claims authorship, and the commit is not attested.
intent-region-unconstrainedfinding, graph --intentNo decision constrains the region at any granularity.
intent-decision-unimplementedfinding, graph --intentA decided decision constrains the region, no work item that is not dropped implements it, and no commit falls in its window. Only with a work kind read.
intent-work-blockedfinding, graph --intentA work item constraining the region has commits in its window while a work item it needs is not done.
intent-work-open-decided-codefinding, graph --intentCommits in the region are a decision’s own work while the work item implementing that decision is still open.
intent-commit-join-conflictfinding, graph --intentA commit joined to an agent run by its Chant-Run trailer or the run’s record has the patch-id of a commit another run recorded. The content join is not made, since a patch-id join never overrides the others (#3036).
intent-why-no-decisiongap, graph --intentNo current decision governs the region at any granularity, and none is carried out by the commits or runs that made its current lines. why.explained is false.
intent-why-no-rungap, graph --intentNo agent run is joined to the commits that made the region’s current lines.
intent-why-uncommittedgap, graph --intentSome of the region’s lines are not committed yet. The gap’s lines names them.
intent-why-run-ambiguousgap, graph --intentSome lines come from a commit several agent runs made, and no run’s recorded hunks say which wrote them. The gap’s lines names them.
change-uncoveredfinding, check --changesA path the diff changes is covered by no current decided record and no open work item, by path or by its member.
change-out-of-scopefinding, check --changesA record in hand for the change, such as the work item it is for or a decision that item implements, lists a path the diff changes in its out_of_scope.
composites-no-chant-memberreason, graph --compositesNo member of kind chant was read, so nothing declares a composite instance or a component.
composites-none-declaredreason, graph --compositesThe members read declare no composite instance.
composites-no-componentreason, graph --compositesThe members read declare no component, so no composite instance has one.
runtimes-config-unreadablemember, graph --compositesThe member’s chant.config.ts couldn’t be read, so its components list only the built-in local runtime, and no environment from the config.
runtimes-lexicon-unreadablemember, graph --compositesA lexicon the member’s config lists couldn’t be loaded, so it isn’t listed as a runtime.
environments-none-declaredmember, graph --compositesThe member’s chant.config.ts declares no environments, so only local and the environments in its ledger are listed.
environments-ledger-undeclaredmember, graph --compositesThe member’s ledger has releases in an environment its config doesn’t cover, so chant run --env would refuse it and it isn’t listed.
environments-ledger-unreadablemember, graph --compositesThe chant/lifecycle branch exists and the member’s ledger environments couldn’t be listed.
environments-component-undeclaredmember, graph --compositesA component declares an environment the member’s chant.config.ts environments don’t cover, so chant run --env would refuse it and it is not listed (#3153).
box-credential-declaredfinding, checkWSP121: a file in a box member’s directory carries a literal secret, a credential’s shape or a literal where a credential goes. A ${VAR}, $VAR or secret-manager reference isn’t one.
box-capability-unbrokeredfinding, checkWSP122: a capability in a member’s box block names no broker, so the box would hold its credential.
box-isolation-collisionfinding, checkWSP123: two boxes on one host resolve to the same port, state path or cookie name, or two ports in one box share an offset.
box-isolation-literalfinding, checkWSP124: a host’s stateRoot or a box’s state entry is a literal machine path instead of one derived from an environment reference and the box’s name.
diagram-source-missingfinding, checkWSP131: a diagram names a source, and it doesn’t exist in the tree read.
diagram-render-missingfinding, checkWSP132: a diagram names a render, and it doesn’t exist in the tree read. A mermaid or excalidraw diagram may name none.
diagram-render-driftfinding, checkWSP133: a diagram records a sourceHash, and the source’s bytes now hash to something else. check never runs the renderer to find this; it compares the recorded hash against the source in the tree read.
box-fountain-callback-undeclaredfinding, checkWSP125: a box member builds a fountain Box, whose persistent sandbox fountain gives a callback token scoped to its owner, and its box block doesn’t declare fountain-callback brokered by fountain with scope owner.
box-intent-unknownfinding, checkWSP126: a box block names an intent, and no record of a declared kind named decision has that id.
box-intent-unconstrainedfinding, checkWSP127: the decision record a box names as its intent constrains no member or path of this workspace at all.
box-noneplantable.reason, status and graphNo member’s box block declares services, so the workspace has no box for a host to plant (#3146). Not a finding: a workspace need not be plantable.
box-severalplantable.reason, status and graphMore than one member’s box block declares services, and a planted workspace runs one box.
signers-file-missingerror, signersThere is no signers file at the base revision, so there is no signer history to read.
signers-removedbroken history, signers and verifyThe signers file was removed. Commits merged before the removal keep the set they were judged by; nothing after it verifies.
rotation-unparseablebroken history, signers; rotation, verifyThe rotation file beside the signers file is not JSON.
rotation-invalidbroken history, signers; rotation, verifyThe rotation file does not match its shape: schema 1, a version, the previous set’s digest, a threshold and signatures.
rotation-first-versionbroken history, signers; rotation, verifyThe first signer set’s rotation file names a version other than 1, or a previous digest.
rotation-missingbroken history, signers; rotation, verifyThe signer set or its threshold changed with no rotation file signed by the set before it.
rotation-version-skewbroken history, signers; rotation, verifyThe rotation names a version other than the one after the set it replaces, as a replayed or skipped rotation does.
rotation-previous-mismatchbroken history, signers; rotation, verifyThe rotation names a previous digest other than the set it replaces, as a rollback does.
rotation-threshold-unsatisfiablebroken history, signers; rotation, verifyThe new threshold is more than the new set’s distinct signers, so no later rotation could meet it.
rotation-threshold-not-metbroken history, signers; rotation, verifyFewer distinct signers of the set before signed the rotation, in the chant-signers namespace, than its threshold.
trust-policy-unreadableerror, evidence and runs signThe trust policy at base can’t be read, or its signer history is broken, so no runner key is trusted.
envelope-unreadableerror, evidence verify and runs signThe --envelope file can’t be read, or is not JSON.
envelope-invaliderror, evidence verify and runs sign; statement verdict, runs and runs verifyThe file is not a DSSE envelope: payloadType, payload in canonical base64, and signatures with keyid and sig.
envelope-untrustederror, evidence verify and runs sign; statement verdict, runs and runs verifyNo signature in the envelope verifies against a runner key the policy at base lists.
evidence-payload-typeerror, evidence verifyThe envelope’s payload type is not application/vnd.in-toto+json.
evidence-statement-invaliderror, evidence verifyThe payload is not an in-toto Statement v1 with chant’s runner-evidence predicate, or has a field the predicate does not define.
evidence-runner-mismatcherror, evidence verifyThe statement names a runner other than the one whose key signed it.
runner-key-invaliderror, evidence sign and runs signThe key is not an Ed25519 private key in PEM.
runner-key-is-signererror, evidence sign and runs signThe key is a person’s key in the signers file. Evidence and run statements are signed by a service or CI identity.
runner-key-unlistederror, evidence sign and runs signThe policy at base lists no runner with the key.
lock-invalidfinding, checkThe lineage lock can’t be read.
manual-step-openfinding, checkA scope in the lineage lock has an open manual step.
work-kind-missingerror, workNo work kind to find the item in: --kind names a kind with no work block, or the declaration names no work kind.
work-kind-ambiguouserror, workMore than one declared work kind has a record with the id, so --kind must name one.
work-item-unknownerror, work and check --changesNo work record has the id, so there is nothing to lease, or no work item in hand for --changes --work.
work-item-closederror, work claim and workEvidenceA claim on a work item in a closed state, such as done or dropped: there is no work left to claim. workEvidence adds nothing to a closed item either.
work-criterion-unknownerror, workEvidenceThe work record lists no acceptance criterion with the id the evidence names, or its kind has no acceptance criteria.
lease-heldrefusal, work; error, workEvidenceSomeone holds a live lease on the work item: another worker, or, for a claim, the same one.
lease-not-heldrefusal, work; error, workEvidenceNobody holds a live lease on the work item: it expired, was released or was never claimed.
lease-token-mismatchrefusal, work; error, workEvidenceThe live lease on the work item carries another fencing token than the one given.
lease-racerefusal, workAnother writer changed the lease between this command’s read and its write.
lease-push-rejectedrefusal, workThe remote refused the lease push: another clone claimed the item first, or the remote could not be reached.
run-existserror, runs start and runs recordThe fields give a run id the ledger already has.
run-unknownerror, runs end, sign, statement and verifyThe ledger has no start for the run.
run-endederror, runs endThe run’s end is already recorded.
run-not-endederror, runs sign and runs statementThe run has no end recorded. A statement is signed over the run’s whole record (#3192).
run-statement-invaliderror, runs sign; statement verdict, runs and runs verifyThe envelope’s payload is not an in-toto Statement v1 with chant’s agent-run predicate, or has a field the predicate does not define.
run-statement-signer-mismatcherror, runs sign; statement verdict, runs and runs verifyThe statement names a signer other than the runner whose key signed it.
run-statement-mismatcherror, runs sign; statement verdict, runs and runs verifyA listed runner key signed the statement, and it does not match the run’s record: another run, a record that hashes differently, or another unit, harness, model, provider or principal.
runs-no-ledgerreason, runsThe checkout has no chant/lifecycle branch, so there are no agent runs to read.
runs-ledger-malformedreason, runsSome lines of the agent run ledger aren’t run events; the rest are read.
wip-no-brancherror, wip save and wip restoreHEAD is detached, or names a branch with no commit yet, so there is no branch to keep work in progress for (#3172).
wip-noneerror, wip restoreNo snapshot was named, and the branch has none under refs/chant/wip/<branch>.
wip-snapshot-unknownerror, wip restoreThe snapshot named is not a work-in-progress snapshot chant took.
wip-branch-othererror, wip restoreThe snapshot was taken on another branch than the one checked out.
wip-raceerror, wip save and wip restoreAnother writer moved refs/chant/wip/<branch> between the command’s read and its write.
wip-policy-noneerror, wip push and wip fetchNo box block declares replicate, so there is no remote.
wip-remote-unknownerror, wip push and wip fetchThe policy names a git remote the checkout doesn’t have. The host adds it, with its credential, before chant pushes.
ci-green-undeclarederror, ci last-green and ci tickThe declaration has no ci.green block, so chant does not know which check runs make a commit green (#3573).
ci-branch-unknownerror, ci last-green and ci tickThe checkout has no ref for the branch ci.green names, neither the remote-tracking branch nor a local one.
member-existserror, member addThe declaration already has an entry of that name, other than the one given. Giving the same entry again changes nothing and is not an error.
member-unknownerror, member removeThe member named is not declared.
factory-member-unknownerror, box factory setThe member named is not declared.
factory-box-missingerror, box factory setThe member’s entry declares no box block, so it has no factory.
listing-member-unknownerror, box listing setThe member named is not declared.
listing-box-missingerror, box listing setThe member’s entry declares no box block, so it has no listing.
publish-member-unknownerror, box publishThe member named is not declared.
publish-noneerror, box publishThe member declares no box block, or its box block names no publisher.
publish-refusederror, box publishThe box’s publisher refused (it exited 2): nothing was published, and its message says why.
publish-failederror, box publishThe publisher could not be run, failed with another nonzero exit, or ran out of time; its message says what it had done.
publish-answer-invaliderror, box publishThe publisher exited 0 with no JSON object on stdout, or one box-publish.schema.json doesn’t allow.
publish-unrecordederror, box publishThe commit the publisher named is missing, or lacks the apply record of ws-075: Chant-Applied-By naming --by, Chant-Applied-At, Chant-Applied-Commit and a Chant-Record for the item, or a Chant-Record for each record sent.
listing-cover-invaliderror, box listing setThe cover can’t be read, isn’t a PNG, JPEG or WebP picture, is larger than 5 MiB, has a path outside the workspace, or has an extension other than its picture format’s.

graph --intent may also carry findings a plugin contributes through its commitJoins (#2656). Their codes are outside the list. Each is plugin:<name>:<code>, where <name> is the kind’s name in the document’s kinds (the kind file’s commitJoinsName when it has one) and <code> is lower case words joined by dashes. The plugin owns its namespace, and chant only carries the finding. A reader that switches on codes can tell the two apart by the plugin: prefix.

check also reports WSP ids, the declaration check catalog. That catalog is closed too, and it is versioned with the check contract.

A record kind says how its files hold their records, and records --json names it in kind.format (#2664, ws-053). For markdown-front-matter, a record’s data is its front matter. For json, the whole file is one object, and data is that object. The rest of the entry has the same shape either way, so a reader that knows decisions needs nothing new to read a JSON kind. Two other kind options change what a reader sees in a record. A kind without states gives every record state: null. A content-addressed kind (idFrom: "sha256") gives each record the SHA-256 of its file’s bytes as its id, a 64-character hex string equal to the sha256 of any pin of that file. When the file’s name claims a different hash, the record lists itself in assets as drifted and carries asset-drift. None of this adds a code, so contract 1 holds. The kind file options are on the records page.

A record kind with a reviews list gives each record in records --json a digest and a quorum (#2671, #2672). The digest is the SHA-256 of the record’s text with its reviews block taken out, and a verdict names it in its own digest, so an amendment leaves earlier verdicts on an older digest. The quorum lists every verdict in counted or in notCounted, and each one in notCounted has a review- reason code. need comes from the declaration’s quorum, or is 2. met says the counted agree verdicts reach need. When met is true and a dissent is still open, metWithObjections is true too, and a reader shows that as met with objections, not as consensus. The rule for the digest and the order the reasons are checked in are on the records page. A reader never computes a digest itself: it reads digest from records --json. A kind can name a ratified state in reviews.ratified, as the decision kind does. records new and records amend then refuse to write a record in that state below its quorum, and the digest leaves the state out, so a ratified record’s quorum stays met (#2873). chant never ratifies a record by itself: a person sets the state with records amend (see Ratifying).

Each verdict in counted and notCounted also carries attested and attestation (#2687). attested is true when the verdict’s seal verifies for its reviewer against the signers file at base. A seal that fails gives false, and so does a missing seal while a signers file is active. When nothing here can say, the value is null. attestation holds a message, the key’s fingerprint when a signature was checked, and a seal- code whenever attested isn’t true. Once a signers file is active at base, only verdicts with attested: true count, and the rest are review-unattested. Without a signers file the seal doesn’t gate counting. A reader shows a verdict as signed only when attested is true, and never checks a signature itself. These fields, the four seal- codes and review-sign-failed were added within contract 1 in the release after 0.86.0, the same way 0.86.0 added its fields and codes. A reader that finds no attested is reading an older chant, and treats every verdict as unsigned.

A record of such a kind can carry its author’s seal in a top-level seal field, and each parsed record then carries attested and attestation of its own, with the same meanings and codes (#2688). The author is the field the kind’s reviews.decider names, decided_by for decisions. The digest leaves seal out as it leaves the reviews block out, so a record with no seal has the digest it always had. Under an active signers file a record that names an author and isn’t attested carries the warning record-unattested, and is read like any other. Sealing records is opt-in per workspace, and nothing requires it yet. hud calls a record author-signed only if that record’s attested is true. Whether the commit that last changed the file was signed is provenance, a separate question. The release that brought verdict seals brought these too, still within contract 1.

Artifact relationships are derived from decisions (#2549, D18 of #2524). No artifact links to code or to another artifact directly. A decision record says both halves of the relationship:

  • Its evidence pins the artifacts it rests on. Each pin names a file from the workspace root in path and holds the SHA-256 of the file’s bytes in sha256.
  • Its constrains names what it governs: a member as member:<name>, or a file or directory as path:<path>.

chant workspace records checks every pin against the tree it reads. A pin whose file still hashes to sha256 is pinned. One whose file changed is drifted and carries the warning asset-drift. One whose file is gone is missing and carries asset-missing. A matching pin is stale and carries asset-stale when a record this one supersedes pinned the same hash and the file’s last commit is older than the commit that added this record. The decision changed and the artifact did not follow. Each record carries its pins in assets and those warnings in warnings. A warning never makes the record invalid, and --current still lists it: the decision is still what was decided, and the drift asks for it to be looked at again. The paths resolve in workspaceRoot, the workspace whose declaration sits nearest above the kind file.

chant workspace graph --kind <kind file> fills records with the records it reads and adds their links to links, after the member links. A superseded record has no link rows. Each record also carries remediatedBy (#2774), the ids of records whose remediates link names it. A remediates link never changes the target’s state or supersededBy, so a remediated record keeps its link rows and stays current.

Fieldasset rowconstrains row
kindassetconstrains
origin, resolvesdeclared, sourcedeclared, source
recordKind, record, recordPaththe record’s kind, id and filethe same
targetthe pinned paththe entry as written, member:<name> or path:<path>
memberthe member that holds the path, or nullthe member named, or the one that holds the path, or null
statuspinned, drifted, missing or staleresolved, or missing when no member has the name or the path does not exist
reasonwhy the pin is not pinned, or nullwhy the row is missing, or null
sha256, actualthe pinned hash, and the file’s hash in the tree read or nullnot present

To find the artifacts behind a file, a reader walks through the decisions. It takes the constrains rows whose target covers the file: path:<p> covers p and everything below it, and member:<name> covers the member’s directory. From each of those records it takes the asset rows. A drifted asset row means the artifact changed after the decision that governs the file was made, and a stale one means the decision was replaced while the artifact stayed as it was. The walk the other way, from an artifact to what it shapes, starts from the asset rows whose target is the artifact.

A record kind with a work block is a work kind (#2683). The reference workspace’s is work/work.kind.mjs, and Work items describes its records. records --json on a work kind adds these fields, all within contract 1:

FieldOnMeaning
readyeach parsed recordThe record is valid, not superseded, in the kind’s open state, not in a needs cycle, and every work id its needs names is done.
blockedByeach parsed recordEach need that is not done, as { id, state }, with state: null for an id no record has.
implementseach parsed recordEach decision the record names, as { id, state }, with the decision’s state now.
decisionsthe documentEvery decision the work kind’s decision kind reads, as { id, path, state, supersededBy, implementedBy }. implementedBy lists the work records naming it as { id, state }. An empty one means the decision has no work item yet.
leaseeach record with an id, in the working treeThe item’s active work lease as { holder, token, acquiredAt, expiresAt }, or null (#2732). Absent under --at.
acceptanceeach parsed record, when the kind’s work block names acceptance criteria{ met, total, criteria }, where each criterion is { id, verification, met } (#2772). null when the record lists no criteria.
contracteach parsed record, when the kind’s work block names contractThe contract record the item builds as { id, state }, with state: null for an id no record has, or null when the item names none (#3147).
answerseach record with an id, when the kind’s work block names answersThe decision-point answers whose constrains names the item, as { id, point, state, answer, answeredBy }, joined by id and never copied onto the item (#3147). In the working tree it includes the answers a steward keeps on chant/lifecycle.

The decision kind is the file the work kind’s work.decisions names, read at the same revision as the work records. A work record’s warnings hold the work- codes above in place of record-no-evidence.

A criterion is met by evidence that names it in criterion, has result: pass, and has the criterion’s verification. A manual verdict also names its giver in by, and it counts only when that is not the record’s implementer. For a declared work kind with criteria, ls --json gives each kind an acceptance list of { item, state, met, total }, and status --json gives the workspace one of { member, kind, item, state, met, total }. Both list only the current items that state criteria. check reads the same records and fails a done item with a criterion unmet as WSP117.

A work lease is written by chant workspace work claim|renew|release to refs/chant/lease/[_members/<member>/]work/<id>, with a history in _leases/<id>.jsonl on chant/lifecycle. status --json lists every lease in leases, active and expired, each with its item, the member whose ledger holds it (null for the flat ledger), ref, holder, token, acquiredAt, expiresAt and state. Neither read fetches. A reader that needs the remote’s leases fetches refs/chant/lease/* first, or runs a claim, which does.

chant workspace work history <id> --json reads one item’s history back from the ledger of the member that owns its work kind (#2785). The document follows work-history.schema.json. The history file’s place on the branch is ledger, and the lease ref’s record now is lease. events holds every line, and claims folds the lines into one entry per fencing token. Each claim says how it ended: released with its release (by, at, outcome, note), held, expired, or lost to a later claim. Since #3147 the outcome is one of a closed list, and each claim says whether it counts as an attempt and names its kept attempt ref. The document totals them in attempts (failed, limit, remaining, exhausted) against the item’s attempt limit and lists kept, so a runner reads the limit from chant instead of counting claims itself. It never fetches either. chant serve mcp serves it as workspace-work-history.

chant workspace runs --json reads the run ledger, _agent-runs/<id>.jsonl on chant/lifecycle in the workspace root’s ledger (#3033, ws-076). chant records runs and never starts one; whatever ran the agent writes them with runs start, end and record. The document follows runs.schema.json. ledger names the branch, the directory and the tip read (null with no branch). filter repeats the options given. The table lists the fields of each entry in runs.

FieldMeaning
id, statethe run id, the value of its commits’ Chant-Run trailer, and running or ended
startedAt, endedAt, outcomewhen it ran and how it ended, null while it runs
by, agentthe principal it worked for and the agent session it ran as
harness, model, provider{ name, version }, and the model and provider as the writer named them
unit, lease, recordsthe work item { id, kind }, the lease token, and the other records it worked on as { kind, id }
instruction, transcripteach pinned as { sha256, bytes, ref }, or null. Neither is ever copied. The instruction may also carry excerpt, a short excerpt the writer gave to show beside the hash (#3034)
usage, modelsthe turns and token counts, null when it reported none, and each model’s share when the harness gave one
cost{ amount, currency, source }, or null when it reported no cost
commits{ sha, patchId, joinedBy, hunks }: record when its end lists the commit, trailer when the commit on HEAD’s history carries its Chant-Run, and patch-id when a commit on HEAD’s history has neither and its git patch-id --stable equals the patchId of a commit the end lists, as after a rebase or a cherry-pick that dropped the trailer (#3036). A patch-id commit also has recordedAs, the listed commit it rewrites. With --follow-squash, a squash merge on HEAD whose pull request’s original commits join the run is listed with squash and via, the originals (#3035). hunks lists { path, start, end }, the lines the run wrote in that commit, when its end gave them, and is null otherwise and for a patch-id commit (#3034)
decisions<kind>/<id> of each decision its work item implements, and each decision record it names
ledgerits file on the branch
record{ sha256 }, the hash a statement signs: SHA-256 of the canonical JSON of [start line, end line]. null while the run runs (#3192)
statementsthe signed statements the ledger holds for the run, each { at, envelope } with the DSSE envelope as stored
attestationthe statements judged against the runner keys at base: status (signed, unsigned, mismatch, untrusted, invalid), signer, class, keyid, code, reason, the commits the verified statement names, and one verdict per statement in statements

totals holds all, byUnit, byDecision and byPrincipal, each with runs, running, tokens, cost (one sum per currency), unpriced, the runs that report no cost, and unreported, the runs that report no usage. A run without a cost is listed, never counted as zero. A metering reader sums cost per currency and shows unpriced beside it, and a reader that trusts only signed figures counts the runs whose attestation.status is signed. trust names the base the statements were judged at (--base, else origin/HEAD, main or master), the runner principals listed there with their class, and any problems reading it, in which case nothing is signed. reasons holds runs-no-ledger, runs-ledger-malformed, kind-unreadable when a work kind can’t be read to find what its items implement, and squash-unfollowed when --follow-squash can’t read a pull request’s ref. chant serve mcp serves the document as workspace-runs.

A record kind with an answers block is an answer kind (ws-058). Its answers.points names a JSON points file, validated against decision-points.schema.json, which @intentius/chant exports as @intentius/chant/workspace/decision-points.schema.json. Each input a point reads is named for one of this contract’s outputs, optionally followed by dotted field names.

Input outputRead from
record, decision, work-itema record in records
finding, region, commit, nodegraph --intent; node is any of its nodes, such as the one an intent walk asks a question at (#3351)
memberls
gate, release, environmentstatus
componentgraph --composites
askno output: the asker’s own id for an ad-hoc ask, and who asked (#3403)

Decision Points describes the declaration and the answers.

chant workspace points --json lists every point, and every answer record as a question. Each point carries its candidates and their criteria, what each one means, as the points file declares it: an object of strings for noul and choice, an array of ordered level descriptions for score. A question carries its state and any model’s answer with its confidence, and the model’s own reason for it when the backend gave one (#3345). It also lists why each decider before the quorum did not answer, with a model’s explanation as model_reason. note is what the people who answered wrote with the answer, and relayedBy who relayed it for them (points answer --relayed-by, #3402). retractions lists, oldest first, each answer people took back with points retract and the retraction’s by, on and note (#3351). A point declared adhoc lists adhoc: true, no candidates and empty criteria: each ask brings the question’s text and candidates (points ask --candidates), and its question lists them as asked, { question, criteria }, null for a declared point’s question (#3403). A question is open while it is escalated to people or proposed by a model and not yet confirmed, and --open lists only those. chant serve mcp serves the same document as the workspace-points tool. The answer records are ordinary records of the kind, so records --json reads them too, with the answer- warnings above. In a steward’s turn, the answer record is held on the chant/lifecycle branch instead of the working tree (#2786). points lists it with ledger set to its place on the branch (chant/lifecycle:<path>), and path where it would sit in the kind’s directory. records does not read the ledger. ledger is null for a record in the tree. They follow point-answer.schema.json, which the package exports beside it.

chant workspace graph --intent <region> --json prints the intent graph over one region (#2651). Besides a path, a line range and a graph node id, the region can be path#symbol: a TypeScript or JavaScript declaration, resolved to its current lines (#3034). It encodes the walk from a piece of code back to the decisions, artifacts and commits behind it, as #2650 section B describes it. chant emits the graph and a reader such as hud draws it (D8 and D15 of #2524).

A result has $schema, contract, chant, at and workspace like the other documents. region is the id of the region node. history names the commit the history was read from (rev), how the region was followed (line-range, file or directory) and whether the clone is shallow. kinds lists each kind file with its name, the record kind it reads and the form of its commitJoins. The files are the --kind files, or without --kind, every kind the declaration names, in its order (#2680). The name is the record kind’s name, or for a file with no record kind, the file’s name without .kind.mjs. nodes and edges hold the graph, and reasons holds parts of the walk that could not be read. why answers why the region is like this, as described below. A failure has error: { code, message } with a declaration code, a record kind code, intent-region-invalid, or an intent-symbol- code for a symbol that can’t be resolved.

The walk runs in this order, and the nodes are listed in it:

  1. The region, the files under a directory region, and the region’s member.
  2. The commits that touched the region, newest first. A line range is followed with git log -L, a file with git log --follow, and a directory with git log. For the working tree the history is read from HEAD. Each commit may be joined to a unit, a contract and evidence by a kind file’s commitJoins.
  3. The decisions whose constrains cover the region, and the decisions in their supersession chains.
  4. The artifacts those decisions pin, and the URL evidence they cite.
  5. With a work kind passed as --kind, the work items whose constrains cover the region, matched as a decision’s are.
  6. The member links of the region’s member, declared in the declaration and resolved in source as check resolves them.
  7. The findings.
Node kindIdFields
regionregion:<path>, region:<path>:<start>-<end> or region:<path>#<symbol>path from the workspace root, lines or null, member, at, type (file or dir), generated for a file, node when the region was given as a graph node id, and symbol as { name, qualified, kind } when it was given as path#symbol, with the symbol’s current lines in lines
filefile:<path>path, member, generated
membermember:<name>name, dir, memberKind
commitcommit:<sha>sha, subject, author, date, trailers, pullRequest, signature (the provenance level, judged by the signers at base as records judges a record), lines, the ranges it changed in a line-range region, state: decided, decided-by-window, undecided, or null when no record kind was read, and joins, what chant’s trailers on it say. With --follow-squash, a squash commit also has squash: { pullRequest, forge, ref, head, fetched, followed, commits }, its pull request’s original commits, each with sha, subject, author, date, trailers, signature and joins (#3035)
unit, contract, evidence<kind>:<id>ref, the plugin’s own id, plugin, the kind file that supplied it, and data, its other fields. A decision’s URL evidence is an evidence node with plugin: null and data holding title, url, as_of and sha256
workrecord:<kind>/<id>recordKind, record, path, title, state, closed, valid, reasons, provenance, owner, ready, blockedBy, implements, needs, source (the gap it came from as { finding, region, decision?, artifact? }, or null), supersededBy, constrains, warnings, the work- warnings, and reviews when a review session led to the item (each { id, recordKind, record, path, state, comments }, #3154). Read only with a work kind passed as --kind
decisionrecord:<kind>/<id>recordKind, record, path, title, state, closed, valid, reasons, provenance, decided_by, decided_on, decidedIn (the commit that decided the record, as { sha, date, subject }, or null), reviews (agree, dissent, abstain and open concerns, the dissents with neither addressed_by nor withdrawn_on), supersededBy, supersedes, and constrains, the entries that cover the region with their granularity
artifactartifact:<path>path, anchor, pinnedSha256, currentSha256 and pinState: the state of a current decision’s pin, or unpinned when only superseded decisions pin it
linklink:<n>row, the member link row
runrun:<id>an agent run that made a commit in the walk (#3033), from the run ledger: run, recorded, state, harness, model, provider, by, agent, unit, startedAt, endedAt, outcome, usage, cost, transcript, and instruction and lease (#3034). A run a Chant-Run trailer names and the ledger lacks has recorded: false and nulls
findingfinding:<code>:<n>code, message, and concerns, the ids of the nodes it is about. A plugin’s finding also has plugin, the kind file that returned it, and refs, as the plugin gave them. With a work kind read, every finding has addressed and addressedBy, the work items addressing it as { id, state }
Edge kindFrom, toFields
constrainsdecision or work item to region, file or contractgranularity (path, member, contract or issue) and entry, as written
pinsdecision to artifactpinnedSha256 and pinState for that decision’s pin
touched-byregion to commitlines
produced-bycommit to unit
servesunit to contract
supersedesthe newer decision to the one it replaces, derived as records derives it
cites-evidenceunit, contract or decision to evidence
linksconsumer member to producer member
withincommit to a decision whose path window it falls in, or to a work item whose path window it falls instate: decided when the decision’s own unit made the commit, decided-by-window otherwise, and worked for a work item
implementswork item to the decision it carries out
needswork item to a work item it waits on
addressed-byfinding to a work item that addresses it
carriescommit to a decision or work item its Chant-Record trailer names, or to the work item its Chant-Lease was taken on
made-bycommit to the agent run that made itjoinedBy: trailer when the commit carries the run’s Chant-Run, record when the run’s end lists the commit, and patch-id when it has neither and its git patch-id --stable equals a commit’s the run’s end recorded, with recordedAs naming that commit (#3036). With --follow-squash, squash when the commit squashes a pull request whose original commits join the run, with via naming them (#3035)
worked-onagent run to the work item it worked on and each record it names, when a kind read has them (#3034)

A decision covers a commit from the commit that decided it until the window of the record superseding it opens. The commit that decided a record is the one that last moved it into an approved state and kept it there: a state its kind’s approval ranks above 0, or one of its closedStates for a kind with no approval. For the decision kind that is decided, ratified or superseded. A record added in an approved state was decided by the commit that added it. A record not approved in the history read has none, and its window opens at the commit that added it. records --json gives the same commit as decidedIn on each record of a kind with approval ranks that is not a work kind. A commit outside every path-granularity window is intent-commit-undecided, and its state is undecided.

A commit inside a window can still depart from the decision, and chant can’t tell whether it does (#2656). So each commit inside a path-granularity window has a within edge to the decision, and the edge says whether the commit is the decision’s own work. It is when a plugin’s commitJoins joins the commit to a unit and one of these holds: the unit or its contract lists the decision in decisions, by record id or as <kind>/<id>, or the decision constrains the contract the unit served. The commit’s state is decided when it is the own work of at least one decision whose window holds it, and decided-by-window when it only falls in windows. A decided-by-window commit is something for the person to judge. The text walk lists it under its decision and asks whether it is drift, a superseding decision nobody wrote down, or the decision being wrong.

A work item’s window runs from the commit that added its record to the commit that moved it into a closed state, done or dropped, that commit included. While the item is open, the window runs to the revision read. A commit inside the window of a work item that constrains the region by path has a within edge to the item with state: "worked". The commit’s own state still comes from the decisions. A work item addresses a finding when its source.finding is the finding’s code and its source.region contains the region or lies inside it, with overlapping lines when both name the same file. It also addresses an intent-pin-drifted, intent-pin-missing or plugin finding that concerns a decision the item implements. The walk brings in a work item that addresses a finding, and the decisions and work items that a work item in the graph implements or needs, one link deep.

A plugin’s finding is about the commit it was returned for, so that commit’s id comes first in concerns. Each of its refs that names a node in the graph follows as that node’s id. chant first tries a ref as a node id, and then behind each node kind’s prefix, such as commit: or artifact:. After that it tries the ref as a decision’s record id or the start of a commit sha. A ref that names nothing in the graph stays in refs only. Plugin findings come after chant’s own, in commit order. A decision that constrains a path below a directory region has a constrains edge to each file under that path, and it does not count as constraining the whole region.

why is the answer hud’s “why is it like this” view reads (#3034). It is derived from the graph and adds no node. Every id in it names a node in nodes.

FieldMeaning
linesthe lines accounted for: the region’s range or symbol, the whole file, or null for a directory
blamethe current lines, top to bottom, as spans { start, end, commit, sha, runs, narrowedBy, joinedBy }. git blame names the commit that last wrote each line, at --at or in the working tree, where a line not committed yet has commit: null. runs names the run nodes behind the commit, joined as made-by joins them, and joinedBy lists how, without repeats, so patch-id marks lines whose run is known by content only (#3036). When a run’s end recorded hunks for the commit, a run whose hunks miss the line is left out and narrowedBy is hunks. A followed squash’s span also has via: the original commit that last wrote the lines, by git blame at the pull request’s head, and runs keeps only that commit’s runs and the squash’s own. via is empty when the file at the head differs from the squash’s (#3035). Empty for a directory
decisionsevery decision node, most relevant first, as { decision, relevance, current, closed, lines }. relevance is carried when a commit or run that made current lines carries the decision out (through a unit, a Chant-Record or Chant-Lease trailer, or the run’s work item and records), else the granularity it constrains the region at (path, contract, issue, member), else related. Current decisions come first, then by relevance in that order, then the most current lines carried out, closed before open, the narrower path, and the later deciding commit
runsthe runs that wrote current lines, most lines first, as { run, lines, commits, unit, decisions, joinedBy }. joinedBy lists how the run joins those commits, as their made-by edges say. unit is the work item the run record names, { id, kind, node }, with its node when a work kind read has it. decisions lists the decisions its work item implements and the decision records it names. For a directory, every run in the walk, the latest start first
explainedwhether a current decision governs the region or is carried out by what made its lines
gaps{ code, message, lines? } with the closed codes intent-why-no-decision, intent-why-no-run, intent-why-uncommitted and intent-why-run-ambiguous

A recorded run’s work item and the records it names are carried by each commit the run made, the way a Chant-Lease item and a Chant-Record are, so the commit can be a decision’s own work through its run. why was added within contract 1, so a reader that finds no why is reading an older chant, and falls back to the graph.

A commit joins a run in this order: its Chant-Run trailer, then the run’s own list of commits, then, only when neither gives a run, its patch-id (#3036). A patch-id join never overrides the others. A commit one run claims by trailer or record whose content matches a commit another run recorded keeps its join, and the walk reports intent-commit-join-conflict. A squash commit is followed to its pull request’s original commits only with --follow-squash (#3035): the read then fetches a pull request ref the clone lacks, and a ref it can’t read is squash-unfollowed, never a failure. patch-id and squash in joinedBy, recordedAs, via, squash and the joinedBy fields of why were added within contract 1: a reader that finds no joinedBy on a span is reading an older chant, and a reader that switches on joinedBy values treats one it doesn’t know as a join it can’t vouch for.

The document joins the others by the member name (member on region and file nodes, and member:<name> nodes), by the record id (record on a decision node, records[].id in graph --kind and records), and by the commit id (at and provenance.commit).

chant workspace graph --intent --record <id> --json prints a different document, intent-record: the walk for one decision over every path: and member: entry its constrains lists. A path: entry’s history is read as a region’s is. A member: entry’s history is its directory’s, less the directories of the members inside it. Each commit appears once, and only the commits inside the record’s window are listed. The head fields, at, workspace, kinds and reasons are as in graph --intent.

FieldHolds
recordid (record:<kind>/<id>, the id of the same record’s decision node), recordKind, record, path, title, state, supersededBy, decidedIn and constrains
record.constrains[]every entry as written, with its granularity, its path (for a member, the member’s directory), whether it exists and whether it was walked
historyrev, the commit the history was read from, and shallow
windowfrom, the commit the window opens at, and until, the commit that closes it, or null
commits[]the commits in the window, newest first by commit date
countscommits, a count for each bucket (own, worked, withinOther, unexplained), and outsideWindow, the commits that changed the region outside the window

A commit has sha, subject, author, date, trailers, pullRequest and joins as a commit node has them. unit is the unit a commit join gave it, or null. runs lists the agent runs that made it, each with id, recorded, state, harness, model, provider, by, agent, unit and joinedBy, which is patch-id for a run joined by content, with recordedAs (#3036). It is squash for a run that a followed squash’s original commits join, and via names them (#3035). With --follow-squash, a squash commit also has squash as a commit node has it, less the signatures. It is own when an original commit’s trailers carry the record. entries names the constrains entries whose history lists it, and files the files it changed in the record’s region, from the workspace root. For a merge, files is read against its first parent. The bucket is the first of these that holds:

bucketThe commit
ownis the record’s own work, judged as for a within edge with state: "decided"
workedis in the window of a work item in workedBy: one that is not dropped and that implements the record or covers one of the commit’s files by a path: entry
within-otheris in the window of a decision in alsoWithin: another decision whose path: entries cover one of the commit’s files
unexplainedmatches none of these

workedBy and alsoWithin list every match as { recordKind, record, state }, whatever the bucket. If the record can’t be walked, the result is a failure whose error.code is the declaration’s code, the record kind’s, or intent-record-unknown.

chant reads seven trailers of its own on a commit (#3149, ws-075). They point at facts kept by id elsewhere, so a commit that keeps its message through a rebase or a cherry-pick keeps its joins. chant never makes a commit; the writer adds these lines.

TrailerValuejoins field
Chant-Agentthe agent session that wrote the commit (ws-067)agent
Chant-Leasethe fencing token of the work lease the commit was made underlease: { token, item }, where item is the work item a lease history on the local chant/lifecycle branch names for the token, or null
Chant-Runthe id of the agent run that made the commit (#3033)run, and a made-by edge to the run’s node
Chant-Record<kind>:<id>: a record the commit carries out or changes, by its kind’s name. Repeatablerecords: { kind, id, node } each, with node the record’s node id when a kind read has it, else null
Chant-Applied-By, Chant-Applied-At, Chant-Applied-Commiton the commit that applied a leased branch: who, when (ISO 8601) and the branch tip appliedapplied: { by, at, commit }, or null without Chant-Applied-By

Keys compare without case, as git’s do, and a malformed Chant-Record value is skipped. Every other trailer is a plugin’s. A carries edge runs from the commit to each record and lease item a kind read has, and the commit is the own work of a decision it carries, or of a decision a work item it carries implements. Commits made before these trailers, or by tools that write their own, keep their plugin joins.

chant workspace patch <range> --json prints the hunks of a diff for a reader that runs no git. <base>..<head> compares two trees. <base>...<head> starts from the merge base, as a work branch needs. A single commit is read against its first parent, or against the empty tree for a root commit.

FieldHolds
rangespec as given, form (range, merge-base, commit or worktree), and the base and head commits; head is null for worktree
pathsthe --path filters, from the workspace root
limitsfileBytes, 65536 unless --max-bytes sets it, and totalBytes, sixteen times that
files[]each changed file under the workspace root, in git’s order
summarythe counts of files, additions and deletions, and whether any file is truncated
File fieldHolds
path, fromthe path, and for a rename or a copy the path it came from
changeadded, modified, deleted, renamed or copied
binarywhether git reads the file as binary
additions, deletionsgit’s line counts, or null for a binary file
hunkCount, byteshow many hunks git wrote for the file, and their text in bytes, headers included
hunks[]each hunk’s header (the @@ line), oldStart, oldLines, newStart, newLines, and lines, each with its leading space, +, - or backslash
truncatedwhether hunks holds less than git wrote, because the file passed fileBytes or the document passed totalBytes; its last hunk may end early

A revision that names no commit fails the read with revision-unknown, and a --path outside the workspace with patch-path-invalid.

chant workspace check --changes <base>..<head> --json reads the join between records and code forwards (#2773), where graph --intent reads it backwards. It maps each path the diff changes to the current records whose constrains cover it. A decision is current when nothing supersedes it and its state is decided, decided or ratified for the decision kind. A work item is current while it is not in a closed state. A path: entry covers the path it names and everything under it, and a member: entry covers every path in the member’s directory. <base>...<head> diffs from the merge base, and <base> alone diffs to HEAD. The declaration and the records are read at <head>, so a change that adds the work item for itself is covered.

A record kind may name an outOfScope field (out_of_scope on the decision and work kinds): workspace paths, files or directories, that a change carried out under the record must not touch. The records in hand for the change are the work item --work <id> names and the decisions it implements. Without --work, they are every current record that covers some path the diff changes. A path any of them lists is out-of-scope, and a record never covers a path its own out_of_scope lists.

Beside the head every document carries, range gives the range as it was given (spec) and the base and head commits. severity and ignore come from the declaration’s changes block, and --severity replaces the severity. work is the work item in hand, or null. kinds lists each kind read with its role: decision, work, or other for a kind that covers nothing. Each entry in paths has the path from the workspace root, its change (added, modified, deleted or renamed, with the old path in from), its member, whether it is generated (as graph --intent reads a region’s generated), its status, the records in coveredBy and outOfScopeBy with the entry that ties each to the path, and the glob in ignoredBy.

statusMeaningFinding
covereda current record covers the pathnone
uncoveredno current record covers itchange-uncovered
out-of-scopea record in hand lists it in out_of_scopechange-out-of-scope
ignoreda changes.ignore glob matches itnone
recordit is a record file of a kind read: the change is to the records themselvesnone

A finding’s id is finding:<code>:<path>, one per path. Its severity is warn or fail. records lists the records an out-of-scope finding concerns. triage is { finding, region }, the gap source a work item takes. The finding-triage decision point (#2741) reads it to seed a work item or a decision. The work kind’s source.finding accepts both codes. When a work kind is read, addressed and addressedBy say which work items came from the gap: an item addresses the finding when its source.finding is the finding’s code and its source.region is the path or a directory above it, in any state, as graph --intent reads a gap source. The point’s inputs are the finding’s code, message and addressed, and the path’s path, member and generated (#2794). With severity off there are no findings, and ok is false only when severity is fail and there is a finding, which also exits 1. When the check can’t run, the document holds only error. Its code is revision-unknown for a range that names no commit and work-item-unknown for a --work id no record has; otherwise it is the declaration’s or the record kind’s code.

scope holds the write scope check (#2548). It is null unless the declaration at <base> has a writeScope block or agents. The declaration, its record kinds and the trust policy are all read at <base>, so a change can’t widen its own scope. scope.commits lists each commit in the range except merges. An entry names the commit’s principal and class, says whether the principal is attested, and gives the agent session it was judged as. A commit with a Chant-Agent: <name> trailer is judged as that session, and one by a principal a session lists is too. Otherwise the commit’s principal is its attested signer when the policy at base attests it, or else its author’s email, and its class comes from the role grants at base. class is human, agent, runner, service, or a domain class a pinned package supplies (#3080), so a reader that switches on it treats any other name as a domain class. A commit judged human while writeScope names a class no pinned package supplies is one finding with write-scope-class-unknown and a null path. Each path a restricted writer wrote outside its scope is a finding in scope.findings. For a record file the finding also names the verb. An added file is new and a removed one delete. A changed file is review when only its reviews (or a session’s verdicts) changed, and close when a session entered a closed state. Any other change is amend. A scope finding fails the check whatever the severity, so ok is false and the command exits 1.

chant serve mcp serves the document as the workspace-changes tool, and an Op that changes the checkout runs the same check on its own branch with the changeCoverage activity.

chant workspace graph --composites lists each composite instance the members declare, joined to the components that can deploy it (#2662). It is the data a deployment menu is built from. chant lists every match and says how each was made. Under the chant and hud boundary of #2657, the reader chooses what to offer.

An instance’s id is the composed graph’s compositeInstance, <member>/<instance>, and its nodes are the composed node ids. A component’s id is <member>/<name>. Each entry in an instance’s components names a component by that id and says how it matched:

FieldValue
bycomposites when the component’s contract lists one of the instance’s kinds. name when the contract lists no composites and the component’s name joins a kind or the instance’s name with the core joinKey()
against, valuekind or instance, and the name that matched
labelexact, or folded when only case or punctuation differ
viamember when the component and the instance are in the same member, link when the component’s member reads the instance’s member through a resolved member link, and unlinked otherwise

An instance no component matches has an empty components. When the whole list is empty, or no instance has a component, reasons holds a code from the closed list that says why.

Each component also lists the runtimes that can host its deploy in runtimes (#2674). The list starts with the built-in local runtime and adds each lexicon in the member’s chant.config.ts whose opRuntime hosts component runs, which is the list chant run --components <name> --on <runtime> accepts in that member. Each entry has name, lexicon (null for local), default and command, the exact command line to run in the member’s directory. A reader offers only these runtimes and never guesses one. When the member’s config or one of its lexicons can’t be read, the member’s runtimeReasons says so.

Each component also lists the environments it may be deployed to in environments (#2695). This is a contract version 1 addition, like runtimes: a reader that doesn’t know the field ignores it. The list starts with local, the default, which chant run --components deploys to without --env. It goes on with the names the member’s chant.config.ts declares in environments, then the environments with a release ledger for the member on chant/lifecycle. An entry’s source says where its name came from: config, ledger, or builtin for local when neither names it. Its command deploys the component there and carries --env unless the environment is the default. When a component declares environments of its own, chant 0.103.0 adds the names only it declares with the source component, and gives every environment it declares a site object, described on the graph page (#3153). The other entries have a null site. A ledger environment the config doesn’t cover is left out, because chant run --env would refuse it. The member’s environmentReasons says so, and says when the config declares none or can’t be read.

chant workspace status --json lists each member’s gates in gates, read from the member’s gate ledger on chant/lifecycle (#2674). That is the ledger chant approve writes to. A reader shows who has approved what, and how many approvals are still needed, without running git show on the branch. Each gate has its component, name, env and planDigest, a state, the approvals that count with principal, channel and at, the number needed, and approve, the exact chant approve line to run in the member’s directory. The state is read the way a run reads the ledger, and nothing is written.

stateMeaning
approvedThe approvals recorded since the gate was reached, for its plan, pass it.
pendingThe gate is waiting for approvals.
expiredThe gate isn’t approved and its pending fact has expired, so the next run records a fresh one.
supersededThe gate isn’t approved, and an approval recorded since it was reached names a different plan, so that approval doesn’t count.

A gate the declaration at base names in identity.gates has signed set to { "class": <class or null> } (#3163). Only an approval sealed with chant approve --sign by a key the signers file at base lists for its approver counts toward it, and, when class is not null, only one whose approver the role grants at base put in that class. Approvals that don’t meet the rule are left out of approvals and state, as a run leaves them out, and approve ends with --sign. The caller adds --actor <principal>. signed is null for every other gate.

gateLedger names the directory the gates were read from. Its reason holds a code from the closed list when there is no ledger branch, no gate ledger for the member, or a gate ledger that can’t be read.

chant workspace status --json prints each member’s fields: every field its kind declares, set from the entry or the kind’s default (null when it has neither), or null when the kind declares none or no pinned package supplies it (#3151). An object field is an object of its own fields. When the entry gives a value of the wrong type, status prints the default and chant workspace check fails with WSP005. chant 0.103.0 added the field to contract version 1. For an app member it holds the scripts, the variable names and the health path an orchestrator runs the app by.

chant workspace status --json lists each member’s box, the capabilities its box block declares, each with name, broker and scope (#2726). It is null for a member with no block. A broker reads the scope it enforces for a box here. chant workspace check reports a box that holds a literal secret with box-credential-declared and a capability with no broker with box-capability-unbrokered, each carried as code on its WSP finding.

A box block with a host also carries isolation: the ports, state paths and cookie names chant derives from the box’s identity (#2727), or null without a host. A runtime that plants or starts a box sets these values instead of choosing its own. check reports two boxes on a host sharing one with box-isolation-collision, and a hard-coded machine path with box-isolation-literal. The derivation is on the declaration page. chant never expands the environment reference in a state path and never opens a port.

Each box also has services, what the block declares for the box’s supervisor (#2880). The list keeps the declaration’s order and is [] for a block with none. A reader has the declared services before a converge tick reports them in lastTick. status prints each cmd as declared and never expands its ${VAR} references. The declaration page describes the fields.

A box block may declare a factory and a listing (#3146, ws-077), contract version 1 additions that readers who don’t know them ignore. status --json prints both under the member’s box, and graph --json prints them on the member’s entry as box: { factory, listing }, or null when the member’s box declares neither. graph reads the declaration at the revision --at names, so it gives them as they were then.

factory has every field, with the defaults filled in: builds (the members the factory builds, in declaration order), check ({ run, kind }, or null), checks (a directory, or null for the orchestrator’s choice), builders (a member, or null), tiers, builderFor and publish ({ forge, repo, base, branchPrefix, head }, or null). A null base means the repository’s default branch. The branch prefix defaults to chant/work/, and head names the fork the branch is pushed to, or stays null. At most one member’s box has a factory, so a reader takes the first non-null one.

tiers lists the builder tiers as declared, each { tier, agent, kinds, session } with kinds and session null when not given, and [] when the factory declares none. builderFor resolves them per member of builds and per tier into { agent, session } (#3152). The entry for the member’s kind wins over the tier’s entry without kinds. A tier with neither is absent. An orchestrator holding a work item’s tier, from the item or the slice-tier answer, looks up the agent there. Both fields are new in chant 0.103.0, within contract version 1.

listing is { published, title, line, cover }. published is true unless declared otherwise, title and line are "" when not declared, and cover is { path, sha256 }, the file from the workspace root and the sha256 of its bytes in the tree read (null when the file can’t be read), or null when none is declared. A reader can serve the cover from the repository and cache it by hash. A tool changes a listing with chant workspace box listing set (#3308), never by editing the declaration itself.

status --json also prints the box’s publisher, the command its box block names to publish the box’s work, or null (#3165, ws-088). A surface reads three things here in place of what a box’s run script used to put in its environment: whether to offer a publish (publisher is set), where an applied item goes (a pull request on factory.publish.repo, or the box’s checkout when factory.publish is null), and what an ask constrains (member: and the first of factory.builds).

The box’s ship (ws-100) says how it ships its staged work to its own site, or is null. A surface offers Ship when it is set and shows pending.files as the count of changes waiting to ship. To ship, it runs chant run <ship.op> in the member’s directory with the person’s principal in CHANT_SHIP_BY, and passes ship.gate the way it passes any Op’s gate. serving.commit is the commit the box’s site serves: the latest release in ship.env’s ledger under the member.

Both documents carry a top-level plantable: { plantable, box, reason }. A workspace is plantable when exactly one member’s box block declares services, and box names that member. Otherwise box is null and reason is box-none or box-several. Plantability is never a check finding, since a records-only or infra workspace is valid and simply isn’t plantable. A host such as a studio’s planter reads this field rather than reimplementing the rule.

A box block may also declare replicate, where the box’s work in progress is pushed (#3172). status --json prints it under the member’s box and reports where each ref stands in the top-level replication, as Work in progress that survives the box describes.

chant workspace agent --json lists the session’s protected paths in scope.protected, each { path, except }, so a factory’s guard enforces on a build’s diff what check --changes enforces on its commits.

chant workspace status --json also lists each member’s stewards in stewards (#2731), a contract version 1 addition that readers who don’t know it ignore. A steward is the one writer that runs a member’s operational Ops: the fountain lexicon’s Agent and Teammate, or chant operator --steward in the box. An entry names the steward and the file that declares it. form is where it runs in the environment asked for, and forms is the declared default with its per-environment exceptions. capabilities lists the box capabilities the steward reaches through a broker, joined with the member’s box block (broker, and declared: false when the block doesn’t list one), and vault names the vault a steward holds when it isn’t behind a broker. lease is the lease a local steward holds while it runs, or null. Each entry in ops has the Op’s schedule, the env its runs are recorded under and its lastRun from the run ledger; the schedule is null for an Op run only on request, and lastRun is null before the first run. Only members of kind chant with a config of their own are read, and only their *.op.ts files are imported. stewardReasons says when a steward or a last run could not be read. An Op also has changesCheckout and workLease (#2748): workLease is null for an Op that runs under no work lease, and otherwise names the work kind whose ledger holds the lease and lists in held the leases the steward’s turns of that Op hold, by holder <steward>/<op>@<process>. A ConvergeOp also has lastTick, its newest tick on the converge ledger with the rules that fired and the resources its observer step reported, and null for any other Op (#2778). An Op the steward runs beside its turns has beside, with ready and the Op’s own lease, and every other Op has beside: null (#2861). lastRun.phases gives each phase’s status and durationMs. inFlight is the run in flight now, or null: its current phase and step, the phases it has finished and the newest activity lines its steps reported through the file in CHANT_RUN_ACTIVITY. The run keeps it under the checkout’s git directory, not on chant/lifecycle, and removes it once its ledger record is written. hud and the studio read this to show which steward owns a box, what it last did and which work item its current turn holds.

A run that stops on an open decision point has status: "waiting", and its lastRun.point names the question: its answer record’s id, the point, its state, path, subject and since (#2749). The state is the question’s now, read as points reads it, so it is answered once a person has answered. When the questions can’t be read, or the answer record is gone, it is the state the run ledger recorded. The steward’s waiting lists the questions still open, one for each Op whose newest run is the steward’s own and stopped on one, with the Op and the run. It leaves out a question answered since, and one whose Op has a newer run in flight that has not written its record yet: the Op’s lease or one of its work leases was taken after the waiting run ended and is still held. In points --open the question carries askedBy, which names the steward and the run that asked it. hud joins the two by id to ask a person. The steward never answers a question itself, and runs the Op again on the first round after a person does.

A reader such as behold or hud builds one view from several commands. The join keys are the same in every document.

Keylsgraphcheckstatus
the workspaceworkspace.name, atworkspace.name, atworkspace.name, atworkspace.name and lifecycle.commit
a membermembers[].namemembers[].name, and member on every node and edgeentity on a declaration findingmembers[].name
a member’s directorymembers[].dirmembers[].dirnonemembers[].dir
a collectornonecollectors[].member, then pipelines[].id and exporters[].idnonenone
a recordnonerecords[].id, and record on a record linkthe record’s file on a WSP111, WSP112 or WSP113 findingnone

A reader starts with ls, which lists every member whether it can be read or not. It then reads graph for the nodes in groups.byMember, and matches each graph entry to the ls member of the same name. check findings attach to a member through entity, and status releases and gates attach through the member name. A gate’s component, or a status release’s, matches a graph --composites component’s name in the same member. Node ids in graph are <member>/<id>, so a node never collides with another member’s.

A live review session view (H5 of #2650) reads three documents. records on the session kind gives the agenda, the attendance and the verdicts. records on the decision kind gives each agenda decision, and citedBy on the session links the two through the decisions’ review entries. What the session changed comes from records --since <session id> on each kind, from the commit it opened at to the commit that carried its close, or to the working tree while it is open (#2693). chant reads both commits from the session record and from git, so the reader passes only the id. A session written before chant 0.88.0 has no revision fields, and chant falls back to the commits that added its file and last changed it.

Documents read at the same at belong together. Mixing a working-tree ls with a graph --at of an older commit can list members the graph doesn’t have, and each document names its own at so a reader can tell.

hud reads exactly as behold does, and caches per member. At a commit, a member’s entry and nodes don’t change once read. A cache keyed on the workspace name, at and the member name stays valid while the member’s toolchain (chant on its graph entry) stays the same. A working-tree read has at: null and is read again.

A span names the declaration and the release that produced it, so a reader can walk from a trace to the workspace’s graph, ledger and records (#2558, D22 of #2524, ws-060). The attributes are on the span’s resource. They use OpenTelemetry conventions where these exist and a chant.* namespace otherwise.

AttributeJoins to
chant.workspaceworkspace.name in every document, and at pins the revision
chant.membermembers[].name in ls, graph, check and status
chant.declthe node <chant.member>/<chant.decl> in graph, whose groups.byMember lists it
service.namethe service, which is the declaration’s name
deployment.environment.namemembers[].environments[].env in status
service.versionreleases[].digest of that member and environment in status
vcs.ref.head.revisionreleases[].gitSha in status, or at in a graph read at that commit

A reader joins in this order. It takes chant.workspace and chant.member to the graph document of the workspace and finds the member’s entry. chant.decl prefixed with the member is the node id, so the span lands on one node. The records that constrain that node are the constrains rows of links whose target is member:<chant.member>, read with graph --kind. status lists the member’s releases for deployment.environment.name, and the one whose digest equals service.version is the release that produced the span. Its gitSha is vcs.ref.head.revision, and graph --at <that commit> reads the workspace as it was then.

The docker lexicon stamps service.name, chant.workspace, chant.member, chant.decl and deployment.environment.name as OTEL_SERVICE_NAME and OTEL_RESOURCE_ATTRIBUTES in each Compose service’s environment, when it builds inside a workspace. A build knows the environment only from --env or ownership.env, so a build with neither leaves that attribute out. It stamps service.version when the service’s image is pinned by digest. vcs.ref.head.revision and a service.version the build can’t know are set by whatever deploys the release, which knows the digest and the commit: the build’s output does not change with each commit. A project with no chant.workspace.json is unchanged. It opts in with telemetry.attribution: true in chant.config.ts and gets the attributes that need no workspace, and inside a workspace telemetry.attribution: false turns stamping off. A value a service already sets in OTEL_SERVICE_NAME or OTEL_RESOURCE_ATTRIBUTES is kept, and only the missing attributes are added. The k8s lexicon stamps the same variables in the env of each container of a Deployment, StatefulSet, DaemonSet, ReplicaSet, Job, CronJob or Pod, with the workload’s metadata.name as service.name and its declaration’s export name as chant.decl. The fly lexicon stamps them in each Machine’s config.env, with the name of the Machine’s app on Fly as service.name and the Machine’s export name as chant.decl. A release deploy adds service.version (the release’s digest) and vcs.ref.head.revision (its gitSha) without changing the generated files (ws-081): OTEL_RESOURCE_ATTRIBUTES ends with a reference to CHANT_RELEASE_ATTRIBUTES, which chant run --components sets in the environment of docker compose up (through shell or remote-exec) and, on k8s, kubectl-apply sets through the pod annotation chant.intentius.io/release-attributes. A Fly Machine’s env values are literal, so fly-release writes the release’s attributes into the OTEL_RESOURCE_ATTRIBUTES of the stamped Machine it serves. A span from a workload deployed that way joins to the status release by service.version and vcs.ref.head.revision.

Where the service’s telemetry goes is a telemetry link: the service’s member links to a pipeline or exporter in the collectors section of graph. A link row says resolved, or why it isn’t.

A reader writes only through chant commands (ws-052), and so does every other tool, because the repo is the database (ws-074). Every durable fact about a workspace is a file in the repo, work in progress is uncommitted files in the working tree of the active work branch, and a tool keeps only secrets, telemetry, rebuildable caches and its substrate’s runtime state outside it. How a reader tells uncommitted records from committed ones is #3160, and the suite that holds a writer to this is #3159. For records those are records new, amend, review and close (#2670, #2693). Each validates the record it would write the way records reads it and then writes one file or none, leaving the commit to the caller. review --session is the one exception, and it writes the decision and the session together. The document each prints follows its own schema:

CommandSchema $idResult
chant workspace records newhttps://intentius.io/chant/schemas/workspace/records-new/v1/records-new.schema.jsonpath, id, digest, and seal with --sign
chant workspace records amendhttps://intentius.io/chant/schemas/workspace/records-amend/v1/records-amend.schema.jsonpath, id, changed, digest, and seal with --sign or sealDropped when a change removed the seal
chant workspace records reviewhttps://intentius.io/chant/schemas/workspace/records-review/v1/records-review.schema.jsonpath, id, review, digest, and session with --session
chant workspace records closehttps://intentius.io/chant/schemas/workspace/records-close/v1/records-close.schema.jsonpath, id, changed, seal, closedRev, digest

An agent run goes to the run ledger on chant/lifecycle, never to the working tree, through chant workspace runs start|end|record (#3033). Their document follows https://intentius.io/chant/schemas/workspace/runs-write/v1/runs-write.schema.json. It holds the run as runs --json reports it and the place on the branch the line went, with the Chant-Run line for the run’s commits in trailer. A refused write writes nothing and exits 1 with run-exists, run-unknown, run-ended or write-input-invalid in error.

A box’s listing is configuration on its box block, written through chant workspace box listing set <member> (#3308). The command edits members[i].box.listing in place and nothing else in the declaration, so the file keeps its formatting and comments. With --cover it also copies a picture into the repository. The document it prints follows https://intentius.io/chant/schemas/workspace/box-listing-write/v1/box-listing-write.schema.json. It holds the listing before and after, as status --json prints it, and paths names each file the command changed. The scope rule below judges each of those files as check --changes judges a path, so a protected declaration takes the write only when its entry’s except names /members/*/box/listing. When the write is refused, the declaration and the cover are left as they were, and error says why.

A declaration’s members and hosts are written through chant workspace member add|remove and chant workspace host set (#3596). Each changes one entry of members or hosts in place, so the file keeps its formatting and comments, and prints a document following https://intentius.io/chant/schemas/workspace/member-write/v1/member-write.schema.json with the entry before (previous) and after (entry), as the file holds them. A write is refused when the declaration it would write doesn’t read (write-input-invalid), and when it adds a collision or a literal path between boxes (box-isolation-collision, box-isolation-literal). The same scope rule judges the declaration, so a protected declaration takes these writes only when its entry’s except names members or hosts.

A box’s factory is written the way its listing is, through chant workspace box factory set <member> (#3600). It edits only the top-level properties of members[i].box.factory, so whoever plants a box from a template sets factory.publish, which the template can’t know. Its document follows https://intentius.io/chant/schemas/workspace/box-factory-write/v1/box-factory-write.schema.json and holds the factory before and after, as status --json prints it. A protected declaration takes the write when its entry’s except names /members/*/box/factory/publish, or whatever else the write changes.

A box’s work is published through chant workspace box publish <member> <item>, or --records for the records kept uncommitted (#3165). chant writes nothing itself here: it runs the publisher the box block names and checks that the commit the publisher made carries the apply record of ws-075. The document follows https://intentius.io/chant/schemas/workspace/box-publish/v1/box-publish.schema.json, whose $defs.request and $defs.answer are the publisher’s side of the call.

Work in progress is snapshotted, restored and replicated through chant workspace wip save|restore|push|fetch (#3172), never by a tool running git on the checkout itself. Each prints a document following https://intentius.io/chant/schemas/workspace/wip-write/v1/wip-write.schema.json, with action naming the verb. save reports the ref it moved, the snapshot and whether it was created, and replication when the policy pushes on save. restore reports the snapshot it restored and the checkpoint it took first. Its headMoved says whether HEAD has moved since the snapshot, and paths lists every file it wrote or removed. None of them writes a record or commits to a branch.

chant workspace work writes a work lease rather than a record, and prints a document following https://intentius.io/chant/schemas/workspace/work-lease/v1/work-lease.schema.json (#2732). Its result carries item, event, kind, ref, lease and history, and a refusal carries refused with a lease- code and exits 2.

The schemas ship beside the read schemas and follow their shape: a oneOf of a result and a failure, contract: 1, and a closed list of error codes from the same list as every read. A result also carries kind, dryRun, the written record’s warnings, and text under --dry-run. A failure exits 1 and writes nothing. chant 0.86.0 is the first release to print these documents.

A write never makes the caller compute anything about the record. new allocates the id, and for a session it records the commit the session opened at. amend applies the approval rule and checks the quorum before a record is ratified, and review computes the digest the verdict is bound to. close computes a session’s seal (#2693). review --session writes the verdict to the decision and to the session in one command, so the two lists never differ. The principal a review names is recorded as the caller gives it. review --sign seals the verdict with an ssh key, and records then reports whether the seal verifies for that principal against the signers at base (#2687). new --sign and amend --sign seal the record’s author the same way (#2688). The provenance of the commit that carries the file is reported apart from that (#2547).

Each write is judged against the writer’s write scope, read from the declaration at base (#2548). The writer is the agent session CHANT_AGENT names, or the session that lists the principal the write names (--by, or by through MCP), or else the class that principal’s role grants give. A write outside its scope is refused with write-scope-member or write-scope-kind, a session the declaration doesn’t name with agent-unknown, and a human’s write while writeScope names a class no pinned package supplies with write-scope-class-unknown. When the declaration at base sets identity.attribution to identified, a write whose --by or author field names a person by a bare name is refused with principal-unidentified (#3163).

One working tree is written by several principals at once: people through hud, the coding agent, the steward and builders (#3173, ws-089). chant’s model for them has three parts.

Writes are serialised. Every chant write to the working tree holds the working tree’s write lock from its first read to its last write. Those are the records writes and the points writes, with box listing set, and work evidence writes through records amend. The lock is the directory chant-write.lock in the working tree’s git directory, so two linked worktrees never wait on each other. A write waits for it up to CHANT_WRITE_LOCK_WAIT_MS (15 seconds by default) and is then refused with write-lock-timeout, naming the holder. A holder whose process has gone, or whose time has run out, is broken by the next writer. Each write reads what the write before it left, so two writes never both build on one reading of a file, and records new never hands out an id twice. A record file is replaced through a temporary file and a rename, so a reader never sees half of one. Ledger writes on chant/lifecycle were already compare-and-set on the branch and are unchanged.

Writes to different fields merge, and writes from a stale reading conflict. A write without --expect applies its fields to the record as it is when the write runs, so two amendments of different fields both land, and a verdict is appended to the reviews as they are. A write that set a field someone else set in the meantime replaces it. A caller that shows a person a record and writes what they did with it passes --expect <digest>, the digest the record had in records --json or in the document of the caller’s last write. amend, review and close then go ahead only when the record still has that digest, and are otherwise refused with record-conflict. The refusal’s conflict names the digest the record has now and its lastWrite. A UI shows the person the conflict and asks again after re-reading. Exactly one of several writes from one digest is written. Reviews and seals stay out of the digest, so a verdict landing between a read and an amendment is not a conflict, and the amendment keeps it.

A batch holds the lock across calls. chant workspace lock acquire --holder <name> [--ttl <duration>] takes the lock for a batch of writes, such as hud’s per-writer publish batch (hud#819), and prints a token. Each chant write in the batch runs with CHANT_WRITE_LOCK=<token> in its environment and goes ahead without waiting, while every other writer waits. lock release --token <token> gives it back. The lock lasts 60 seconds by default and at most 10 minutes, after which any writer may break it, and the batch’s next write is refused with write-lock-not-held. Its document follows https://intentius.io/chant/schemas/workspace/write-lock/v1/write-lock.schema.json. chant workspace lock alone says who holds the lock and never prints the token. A tool that writes files of its own in the batch, such as app files, takes the same lock first, so a chant write never lands between two of them.

chant workspace agent <name> --json prints what an agent session reloads from (#2548). The document follows agent.schema.json. agent is the session with its members, member (the first of them) and principals, and scope holds the members it writes (ws-101) and each declared record kind in its reach with the verbs it may write the kind with. spec is the spec block records --current --json prints, and reload lists the two reads that rebuild the session’s context. The session and its scope come from the declaration at base (workspace.scopeFrom is base), or from the working tree when there is no base or no declaration at it. The spec is read in the working tree. chant serve mcp serves it as workspace-agent.

A reader such as hud or behold reads only through this contract and writes only through chant commands (ws-086, the workspace boundary). @intentius/chant ships a conformance suite for that, so a reader can show it in its own CI with nothing else installed (#2679).

ImportFor
@intentius/chant/workspace/conformanceany test runner: runWorkspaceReaderConformance returns the problems, and checkReaderRead checks one read. It imports no runner and loads under plain node
@intentius/chant/workspace/conformance/vitestvitest: describeWorkspaceReaderConformance adds one test per command. Only this entry imports vitest

READ_CONTRACT_COMMANDS lists the read commands covered, and since #3160 it includes records --uncommitted. Since #3172 it includes wip, run as workspace wip --json. A reader runs it as workspace records --uncommitted <args> --json. That document is held to the records schema, which requires checkout when uncommitted is true and a worktree on every record.

The suite builds the reader from a function you pass, giving it a transport that runs chant in the workspace. The reader’s read(command, args) runs one contract command through the transport and returns the parsed document. commands lists the commands the reader reads. The suite reads only those, and the report names the others in skipped as not applicable.

A reader of ls, status and graph --composites, tested with node:test after npm i -D @intentius/chant:

// reader-conformance.test.mjs, run with: node --test
import assert from "node:assert/strict";
import { test } from "node:test";
import { runWorkspaceReaderConformance } from "@intentius/chant/workspace/conformance";
const JSON_FLAG = { ls: ["--json"], status: ["--json"], "graph --composites": ["--json"] };
/** The reader: one contract command through the transport, and the document chant printed. */
const myReader = (chant) => ({
async read(command, args) {
const run = await chant.run(["workspace", ...command.split(" "), ...args, ...JSON_FLAG[command]]);
return JSON.parse(run.stdout);
},
});
test("my reader reads only through the read contract", { timeout: 300_000 }, async () => {
const report = await runWorkspaceReaderConformance(myReader, { commands: ["ls", "status", "graph --composites"] });
assert.deepEqual(report.problems, []);
});

With vitest, describeWorkspaceReaderConformance({ name, reader, commands }) from the vitest entry takes the same reader.

For each command it reads, the suite fails when any of these is false:

CheckHow
the document validates against the command’s output schema and carries this contract’s versionthe schemas @intentius/chant ships, with its own ajv
the read made exactly one chant call, the contract command with the suite’s arguments and its JSON flagthe transport records every call
the reader returned the document chant printed, unchangedcompared with the recorded output
no file in the workspace changedevery file is hashed before the first read and after the last

The reader never gets the workspace’s path, so the transport is its only route to the workspace, and the transport records everything that goes through it.

A tool that also writes runs the writer suite from the same import (#3159). It drives scripted writes through the tool’s writer, checks that each is one chant command whose changes chant reports, and deletes the tool’s private state to check that the tool still shows the same facts.

With over: "mcp", the same reader’s calls are answered by chant serve mcp (#2707). The transport starts one server session in the workspace and turns each call into the workspace tool that answers it, such as workspace-records for workspace records --kind <kind> --json. It also runs the command itself, and the suite fails when the tool’s document differs from the command’s. check has no tool, so over MCP it is not applicable.

Outside the chant repository the suite writes a workspace to a temporary directory and removes it afterwards. The workspace is copied from the fixture in src/workspace/conformance/__fixture__/ of @intentius/chant. Its chant member declares one composite and the component that deploys it, and its decisions/ directory holds one decision. chant workspace init --yes declares it, and git commits it on main with a Chant-Run trailer, and chant workspace runs record writes that run to the run ledger, so runs reads a run with every field. The suite then writes a second decision, fix-002, proposed, and leaves it uncommitted, so records --uncommitted lists one record. createConformanceWorkspace() called directly leaves it out unless given uncommitted: true, so a test that writes records there allocates the ids it did before. Inside the chant repository the suite reads reference-workspace/. Both have the files the suite’s arguments name, such as decisions/decision.kind.mjs and app/src/server.mjs:19. workspaceDir names another workspace with those files. chantCommand names the chant to run. By default that is the first node_modules/.bin/chant above the current directory, then the chant the suite came with.