Skip to content

chant workspace box

chant workspace box listing set <member> [--from <file|->] [--cover <image> [--cover-path <path>]] [--by <principal>] [--dry-run] [--json]
chant workspace box factory set <member> --from <file|-> [--by <principal>] [--dry-run] [--json]
chant workspace box publish <member> (<item> | --records) [--by <principal>] [--head <owner/name>] [--dry-run] [--json]

The command has three verbs: box listing set, described first, box factory set, under The factory, and box publish, under Publishing a box’s work.

A box’s listing is what a home site that lists boxes shows of it, such as its title and its cover picture. The listing lives on the member’s box block as members[i].box.listing in chant.workspace.json (#3146, ws-077). Under ws-074 a tool writes the repo only through chant. A tool such as hud therefore changes a listing with this command and never edits the declaration itself (#3308).

box listing set changes only the listing’s own properties, editing the declaration in place. The rest of the file is left byte for byte, comments and trailing commas in a chant.workspace.jsonc included. A property the write adds goes after the listing’s last one, and a box with no listing gets one after the box block’s last property. Before writing, the command checks the declaration it would write against the declaration schema. It never commits. As after the record writes, the caller commits the change and opens the pull request.

The member must declare a box block. A member without one is refused with listing-box-missing.

--from names a JSON file, or - for standard input, holding an object of the fields to change. A field left out keeps its value, and null takes a field out of the declaration so it reads as its default.

FieldHolds
publishedtrue or false. Default true
titleat most 60 characters, with no control character. Default ""
lineat most 140 characters, with no control character. Default ""
covera PNG, JPEG or WebP file already in the workspace, as a path from the workspace root
x-*any value, kept as given

Any other field is refused with write-input-invalid, and so is a value the declaration schema refuses, such as a 61-character title.

--cover <image> copies a picture into the repository and points the listing at it, so one call sets the fields and the cover. The image is read from the path given, relative to the current directory, and must be a PNG, JPEG or WebP of at most 5 MiB, told by its first bytes. It goes to:

  1. --cover-path <path>, from the workspace root, when given. Its extension must fit the picture’s format.
  2. Otherwise the listing’s current cover, when its extension fits.
  3. Otherwise <member dir>/listing/cover.png, .jpg or .webp.

When the cover moves to another file, cover.replaced names the one the listing named before. That file stays in place, and the caller removes it in the same commit if nothing else uses it. --cover and a cover field together are refused with write-usage-invalid. Bytes the destination already holds are not written again.

The write is judged as check --changes judges the commit that will carry it, by the write scope in the declaration at base (#2548), so a working-tree edit can’t widen it. The writer is the agent session CHANT_AGENT names, the session listing the --by principal, or the class the principal’s role grants give.

  • The declaration is at the workspace root, in no member. A class whose writeScope.<class>.members leaves out paths in no member is refused with write-scope-member, and so is every agent session, since a session writes only the members it is bound to.
  • A protected entry covering the declaration refuses the write with write-scope-protected, unless its except allows the change. { "path": "chant.workspace.json", "except": ["/members/*/box/listing"] } keeps the declaration protected and lets any box’s listing change.
  • The cover’s path is judged the same way, as a file in its member.
  • When the declaration at base sets identity.attribution to identified, a bare name in --by is refused with principal-unidentified. Pass a forge identity such as github:alice, or a signer (ws-080).

The command prints one JSON document following https://intentius.io/chant/schemas/workspace/box-listing-write/v1/box-listing-write.schema.json, with or without --json.

Terminal window
chant workspace box listing set app --by github:alice --cover /tmp/upload-7f3a.png --from - <<'EOF'
{ "title": "Fern", "line": "A garden planner for a small allotment" }
EOF
{
"$schema": "https://intentius.io/chant/schemas/workspace/box-listing-write/v1/box-listing-write.schema.json",
"contract": 1,
"chant": "0.103.0",
"member": "app",
"declaration": { "path": "chant.workspace.json", "sha256": "20dd0267..." },
"paths": ["app/listing/cover.png", "chant.workspace.json"],
"changed": true,
"dryRun": false,
"previous": null,
"listing": {
"published": true,
"title": "Fern",
"line": "A garden planner for a small allotment",
"cover": { "path": "app/listing/cover.png", "sha256": "7343d363..." }
},
"cover": { "path": "app/listing/cover.png", "sha256": "7343d363...", "bytes": 48213, "type": "image/png", "replaced": null }
}
FieldHolds
memberthe member whose box holds the listing
declarationthe declaration file from the repository root, and the sha256 of its bytes after the write
pathsevery file the write changed, from the repository root. Empty when the listing and cover already held what was given, and then changed is false
previous, listingthe listing before and after, as status --json prints it, with defaults filled in and the cover’s sha256. previous is null when the box had no listing
coverthe image --cover copied, or null

--dry-run prints the same document and writes nothing. A refused write leaves every file as it was and exits 1. Its document carries error: { code, message } instead.

CodeWhen
listing-member-unknownthe declaration names no such member
listing-box-missingthe member declares no box block
listing-cover-invalidthe cover can’t be read, isn’t a picture of a format above, is over 5 MiB, or has a bad path or extension
write-usage-invalidthe command line is missing a value or gives a conflicting one
write-input-invalidthe fields aren’t a JSON object of listing fields, or the declaration schema refuses them
write-scope-member, write-scope-protected, write-scope-class-unknown, agent-unknownthe write scope at base refuses the writer
principal-unidentified--by is a bare name under identity.attribution: "identified"
a declaration codethe declaration can’t be read, as for any workspace read

The writer conformance suite drives this command as its box listing set action, and its amnesia test reads the listing back through status --json.

box factory set <member> --from <file|-> changes the box’s factory, members[i].box.factory, the same way box listing set changes its listing (#3600). A template can’t carry factory.publish, because its repo must be a real owner/name and every box planted from the template publishes to its own repository. Whoever plants a box knows that repository, and sets the target once the box is planted.

Terminal window
echo '{ "publish": { "repo": "alex/fern", "base": "main" } }' | chant workspace box factory set box --from -

The factory takes builds, check, checks, builders, tiers, publish and x- keys at its top level, and the object --from gives may hold any of them. Each one given replaces that field’s whole value, null deletes the field, and the fields not given stay as they are. The write is refused with write-input-invalid for any other field, and when the declaration would no longer read, such as with a repo that is not owner/name. A box with no factory gets one, and then the fields must include builds. A member with no box block is refused with factory-box-missing, and an unknown one with factory-member-unknown.

The write scope at base judges the write, as under Who may write. A protected declaration takes it when the entry’s except names what changes, such as /members/*/box/factory/publish.

The document printed follows https://intentius.io/chant/schemas/workspace/box-factory-write/v1/box-factory-write.schema.json. Its member, declaration, paths, changed and dryRun mean what they mean for the listing. previous and factory hold the factory before and after the write as status --json prints it, or null when there is none. Giving the same fields again changes nothing (changed: false), and chant never commits the change.

box publish is the one call a surface such as hud makes to publish a box’s work (#3165, ws-088). It replaces the HUD_APPLY_CMD a box’s run script used to set.

chant workspace box publish app W-12 --by github:alice
chant workspace box publish app --records --dry-run
chant workspace box publish app --records --by github:alice

The work is the orchestrator’s. The box block names a publisher, a command an orchestrator such as studio supplies (declaration). The publisher applies a built work item to the box’s checkout. When the box’s factory.publish names a target, the publisher also pushes the item’s branch and opens the pull request. With --records it sends the records a person kept uncommitted instead. chant runs the publisher and checks what it did, and leaves git and the forge to it.

What chant does, in order:

  1. Checks the command line. It needs an item or --records but not both, --by unless --dry-run is given, and a --head of the form owner/name. Anything else is write-usage-invalid.
  2. Reads the member’s box.publisher: publish-member-unknown when the member isn’t declared, publish-none when it names no publisher.
  3. Applies the identity rule to --by: a bare name under identity.attribution: "identified" is principal-unidentified (ws-080).
  4. Runs the publisher from the workspace root, split into words as a shell splits plain words but with no shell, with CHANT_PUBLISH_CONTRACT=1 in its environment and one JSON request on stdin. It is stopped after ten minutes.
  5. Reads its exit code and the last JSON object on its stdout, the answer.
  6. Unless --dry-run, checks the commit the answer names for the apply record of ws-075.

An orchestrator implements this. The request on stdin is $defs.request of box-publish.schema.json:

{
"contract": 1,
"action": "item",
"member": "app",
"item": "W-12",
"by": "github:alice",
"head": null,
"dryRun": false,
"workspace": { "root": "/home/box/app" },
"factory": { "builds": ["app"], "check": null, "checks": null, "builders": null, "publish": { "forge": "github", "repo": "acme/fern", "base": "main", "branchPrefix": "box/", "head": null } }
}

action is item or records. item is null for records. head is the fork --head named, which takes the place of factory.publish.head. factory is the workspace’s factory as status --json prints it, so a publisher reads the target from the request rather than parsing the declaration.

The publisher answers by exit code:

ExitMeaningchant’s code
0done, with the answer as the last JSON object on stdoutnone, or publish-answer-invalid when the answer is missing or malformed
2refused, nothing published; stderr says whypublish-refused
any otherfailed, possibly after part of the work, such as applying an item and failing to push it; stderr says whatpublish-failed

The answer is $defs.answer. Only ok: true is required, and x- fields are passed through:

FieldHolds
committhe commit made in the box’s checkout. Required unless dryRun
recordsthe records sent, each { kind, id, path, title }. With dryRun and records, the ones that would go
pullRequest{ url, number, branch, base, head }, the pull request opened or found open
pushed{ branch, repo } when the branch was pushed and no pull request could be opened
localwhy the work stayed in the box although factory.publish names a target

The commit carries the apply record. An item’s commit has Chant-Applied-By naming the request’s by, Chant-Applied-At, Chant-Applied-Commit with the applied branch’s tip, and Chant-Record: <kind>:<item>. A records commit has one Chant-Record per record sent. When the commit is missing or lacks one of these trailers, chant refuses the result with publish-unrecorded. The document still holds the publisher’s answer, so the caller can show the pull request.

What a surface reads instead of the environment

Section titled “What a surface reads instead of the environment”
It neededIt reads, from status --json
whether to offer Apply (HUD_APPLY_CMD)the member’s box.publisher: offer it when not null, and call box publish rather than running it
where an applied item goes (HUD_APPLY_TO)box.factory.publish: a pull request on its repo when set, the box’s own checkout when null. The answer’s local says when an orchestrator kept it in the box anyway
what an ask constrains (HUD_ASK_CONSTRAINS)member:<box.factory.builds[0]> (ws-077)

box publish answers on stdout in the https://intentius.io/chant/schemas/workspace/box-publish/v1/box-publish.schema.json shape, whether or not --json is given.

{
"$schema": "https://intentius.io/chant/schemas/workspace/box-publish/v1/box-publish.schema.json",
"contract": 1,
"chant": "0.103.0",
"member": "app",
"action": "item",
"item": "W-12",
"by": "github:alice",
"dryRun": false,
"publisher": "node box/ops/factory/publish.mjs",
"commit": "9f2c4e1a...",
"records": [],
"pullRequest": { "url": "https://github.com/acme/fern/pull/41", "number": 41, "branch": "box/W-12", "base": "main", "head": null },
"pushed": null,
"local": null,
"applied": { "by": "github:alice", "at": "2026-10-03T18:22:05Z", "commit": "5b1d07c3..." },
"answer": { "ok": true, "commit": "9f2c4e1a...", "pullRequest": { "url": "https://github.com/acme/fern/pull/41", "number": 41, "branch": "box/W-12", "base": "main" } }
}

applied is the apply record read back from the commit’s trailers, for an item; null for records and with --dry-run. When chant has no result to print, the exit code is 1 and error names one of the codes below. The publisher’s answer is kept beside it whenever the publisher printed one.

CodeWhen
publish-member-unknownthe declaration names no such member
publish-nonethe member declares no box block, or its box block names no publisher
publish-refusedthe publisher exited 2
publish-failedthe publisher could not be run, exited with another nonzero code, or ran out of time
publish-answer-invalidthe publisher exited 0 with no JSON object on stdout, or one the schema doesn’t allow
publish-unrecordedthe commit the publisher named is missing or lacks its apply record
write-usage-invalidthe command line is missing a value or gives a conflicting one
principal-unidentified--by is a bare name under identity.attribution: "identified"
a declaration codethe declaration can’t be read, as for any workspace read
CodeMeaning
0The listing was written, would be with --dry-run, or already held what was given; or the publisher published, or said with --dry-run what it would
1Nothing was written, or the publish printed no result: the document’s error says why