Skip to content

Workspace Writer Conformance

The repo is the database (ws-074). Every durable fact about a workspace is a file in the repository or a line on its chant/lifecycle ledger, and a tool writes one only through chant’s write commands. ws-074 lets a tool keep secrets and telemetry outside the repository. It may also keep caches it can rebuild from the repo, and the runtime state of its substrate.

The reader suite holds a reader to the read contract, and this one holds a writer to the write contract (#3159). Both ship in @intentius/chant and are wired the same way.

ImportFor
@intentius/chant/workspace/conformanceany test runner: runWorkspaceWriterConformance returns the problems. It imports no runner and loads under plain node
@intentius/chant/workspace/conformance/vitestvitest: describeWorkspaceWriterConformance adds one test per step and one per check after the script

WRITE_CONTRACT_ACTIONS lists them, and WRITER_SCRIPT performs each at least once, in this order. A writer lists the ones it performs in actions, and the report names the rest in skipped. Those steps still run, through chant directly, because later steps build on them.

StepActionCommandOutput schemaWhat chant writes
decisionrecords newworkspace records new decisions/decision.kind.mjs --from -records-new.schema.jsona new decision, fix-002
amendrecords amendworkspace records amend fix-002 --kind <kind> --set -records-amend.schema.jsonthe decision’s title
reviewrecords reviewworkspace records review fix-002 --kind <kind> --verdict agree --by <name> --note <text>records-review.schema.jsona review on the decision
sessionrecords newworkspace records new sessions/session.kind.mjs --from -records-new.schema.jsonan open review session, S-0001, holding one anchored review comment and its round (#3350)
closerecords closeworkspace records close S-0001 --kind <kind>records-close.schema.jsonthe session, closed and sealed
askpoints askworkspace points ask slice-tier --inputs - --subject W-001 --kind <kind>points-write.schema.jsona question, escalated to people
answerpoints answerworkspace points answer <id> --answer medium --by <name> --note <text> --kind <kind>points-write.schema.jsonthe people’s answer, with their note
retractpoints retractworkspace points retract <id> --by <name> --note <text> --kind <kind>points-write.schema.jsonthe question escalated again, the answer kept in its retractions (#3351)
reanswerpoints answerworkspace points answer <id> --answer large --by <name> --kind <kind>points-write.schema.jsonthe people’s new answer
adhocpoints askworkspace points ask agent-question --inputs <json> --candidates - --subject W-001 --kind <kind>points-write.schema.jsonan agent’s question with its own text and options, kept as asked, escalated to people (#3403)
adhoc-answerpoints answerworkspace points answer <id> --answer flag --by <name> --kind <kind>points-write.schema.jsonthe people’s pick among the options the question was asked with
claimwork claimworkspace work claim W-001 --holder <name> --kind <kind> --jsonwork-lease.schema.jsonthe lease ref and a chant/lifecycle line
renewwork renewworkspace work renew W-001 --holder <name> --token <token> --kind <kind> --jsonwork-lease.schema.jsonthe same
evidencework evidenceworkspace work evidence W-001 --holder <name> --token <token> --from - --kind <kind>work-evidence.schema.jsonevidence for AC-1 on the work item
releasework releaseworkspace work release W-001 --holder <name> --token <token> --outcome done --kind <kind> --jsonwork-lease.schema.jsonthe lease ref and a chant/lifecycle line
run-startruns startworkspace runs start --from -runs-write.schema.jsona run’s start on the run ledger
run-endruns endworkspace runs end writer-run-1 --from -runs-write.schema.jsonthe run’s end
run-recordruns recordworkspace runs record --from -runs-write.schema.jsona whole run
listingbox listing setworkspace box listing set app --from - --cover <dir>/cover.pngbox-listing-write.schema.jsonthe app box’s listing in the declaration, and its cover at app/listing/cover.png
checkpointwip saveworkspace wip save --label turn:1 --by <name>wip-write.schema.jsona snapshot of the working tree, every write above included, on refs/chant/wip/main
undowip restoreworkspace wip restore <snapshot> --by <name>wip-write.schema.jsonthe working tree as the snapshot holds it, which is how it already is, so no file and no ref changes

work evidence is the workEvidence activity as a command (chant workspace work), so a writer that is not an Op attaches evidence through chant too. box listing set is how a tool changes a box’s listing (chant workspace box, #3308). Its cover is a 1x1 PNG the suite writes for each run to a directory outside the workspace, WRITER_INPUTS. wip save and wip restore are how a tool takes a checkpoint of work in progress and puts one back, such as hud after each agent turn (chant workspace wip, #3172). records new, records amend, records review, records close, points, runs, box listing set and wip print their document without a flag, and a writer may add --json to them. The lease verbs print it only with --json.

The suite builds the writer by calling the function you pass with a transport and a context. The transport runs chant in the workspace and records every call. The context has stateDir, a directory outside the workspace for the writer’s private state, such as its database. The writer never gets the workspace’s path.

MemberRequiredDoes
write(step)yesPerforms one step: runs its one command through the transport, with step.input on stdin when the step has one, and returns the parsed document chant printed. step.params holds the step’s values by name, and step.args the arguments writeArgv(action, params) builds from them.
facts()yesReturns the facts the tool shows, read the way the tool reads them, as any JSON value. It may make read-contract calls only.
holds()noLists what the tool holds: { record, kind }, { run }, { lease, kind }, or { exempt, what } with exempt one of cache, telemetry, secret or runtime.
close()noLets go of the state directory, such as closing a database, before the suite deletes it.

The options are actions, the actions the writer performs (all of them by default), and privateState, what the writer keeps in stateDir: each entry a path from the state directory, where a directory covers everything under it, and is, one or more of cache, telemetry, secret and runtime. chantCommand and timeoutMs are the reader suite’s.

A writer of reviews and answers, with a SQLite database as its cache, tested with node:test after npm i -D @intentius/chant:

// writer-conformance.test.mjs, run with: node --test
import assert from "node:assert/strict";
import { join } from "node:path";
import { test } from "node:test";
import { runWorkspaceWriterConformance } from "@intentius/chant/workspace/conformance";
import { createWriter } from "./writer.js";
const myWriter = (chant, { stateDir }) => {
const writer = createWriter({ chant, dbPath: join(stateDir, ".hud", "events.db") });
return {
async write(step) {
if (step.action === "records review") return writer.review(step.params);
if (step.action === "points answer") return writer.answer(step.params);
throw new Error(`not one of this writer's actions: ${step.action}`);
},
facts: () => writer.listReviewsAndAnswers(),
close: () => writer.close(),
};
};
test("my writer writes only through chant", { timeout: 600_000 }, async () => {
const report = await runWorkspaceWriterConformance(myWriter, {
actions: ["records review", "points answer"],
privateState: [{ path: ".hud", is: ["cache", "telemetry"] }],
});
assert.deepEqual(report.problems, []);
});

With vitest, describeWorkspaceWriterConformance({ name, writer, actions, privateState }) from the vitest entry takes the same writer. referenceWriter is the smallest writer that conforms: each step is its command, and its facts are read with records, runs and the box listings status dev --json prints (readListing). It reads the snapshots wip --json lists with readWip.

For each step the writer performs, the suite fails when any of these is false:

CheckHow
the step made exactly one chant call: the action’s command, with the step’s arguments in order and its JSON flag, and the step’s fields on stdin. An argument that is JSON, such as an ad-hoc ask’s --inputs, matches when it holds the same value in any key order or spacingthe transport records every call and what it was given
the writer returned the document chant printed, unchangedcompared with the recorded output
the document validates against the action’s output schema, and chant wrote rather than refusedthe schemas @intentius/chant ships, with its own ajv
every file that changed is the path the document reports writing (for box listing set and wip restore, one of its paths)every file outside .git is hashed before and after the step
every git ref that moved is the lease ref, the chant/lifecycle commit or the snapshot ref the document reportsthe refs and HEAD are read before and after the step

After the script it fails when any of these is false:

CheckHow
facts() makes only read-contract calls and changes nothingits calls, the files and the refs, before and after
the state directory holds only what privateState declareslisted after the script
amnesia: the writer shows the same facts with its private state deletedfacts() before; then close(), the state directory deleted, the writer built again with the same stateDir, and facts() again; the two must be equal
everything holds() names is in the repository, or one of the four exceptionseach record read with records, each run with runs, each lease with work history
every fact the script produced reads back through the read contractrecords --uncommitted for each kind, points --json for the retraction and the ad-hoc question’s asked, work history W-001, runs, the listing in status dev --json, and each snapshot in wip --json

When the writer performs records amend, the suite also holds it to the concurrent case (#3173). It reads the script’s decision’s digest and has the writer make the three amendments in CONCURRENT_AMENDS at once, each with expect set to that digest, as two people and an agent answering one record would. writeArgv passes expect as --expect. Exactly one must be written. The other two must each be chant’s record-conflict refusal naming the winner’s digest. Each comes from one chant call and is returned unchanged. Retrying a conflict without --expect overwrites the other writer, and the one-call check catches it. The suite then has the writer retry each refused amendment from the digest its refusal named, and each retry must be written. The problems are listed in after.concurrent, each starting concurrent:.

A writer that keeps a review only in its database passes the step and fails amnesia. A writer that writes a record file itself fails the step’s file check. Writing it with git commit also moves HEAD and fails the step’s ref check.

The suite writes a new workspace in a temporary directory for each run and removes it afterwards. It never writes to a workspace you name. The workspace is the reader suite’s (Testing a reader) with src/workspace/conformance/__writer_fixture__/ copied over it. The overlay copies the work, answer and session kinds and their schemas from the reference workspace, with its decision points file. The contract and driver kinds come along too, since a session’s verdicts may name their records. It adds one open work item, W-001, whose criterion AC-1 takes unit evidence. The declaration names the four record kinds and gives the app member an empty box block for the listing step. Nothing is committed after that commit: the writes stay in the working tree, as ws-074 leaves them.