chant workspace work
Synopsis
Section titled “Synopsis”chant workspace work claim <id> --holder <name> [--kind <kind file>] [--ttl <seconds|duration>] [--note <text>] [--json]chant workspace work renew <id> --holder <name> [--kind <kind file>] [--ttl <seconds|duration>] [--token <token>] [--note <text>] [--json]chant workspace work release <id> --holder <name> [--kind <kind file>] [--token <token>] [--outcome <outcome>] [--note <text>] [--json]chant workspace work history <id> [--kind <kind file>] [--json]chant workspace work evidence <id> --holder <name> --token <token> --from <file|-> [--kind <kind file>]Description
Section titled “Description”A work lease says who is working on a work item right now (ws-055, #2732). A runner claims an item before it starts and renews the lease while its agent works. It releases the lease when the work ends. The item’s record doesn’t change. The lease lives outside the working branch, so it never shows up in a release’s diff or in a write-scope check.
The lease is the operator lease under another key. Its ref is refs/chant/lease/work/<id>. The ref points at a lease record holding the holder and a fencing token, with the times it was claimed and expires. A claim is one compare-and-set of that ref. When two workers claim the same item at once, one wins and the other is refused with the winner named.
| Verb | Effect |
|---|---|
claim | Takes an item nobody holds. Anyone holding a live lease on it refuses the claim, the same holder included. An expired lease doesn’t refuse it, and the new claim gets a new token. |
renew | Moves the expiry of a live lease the holder already has and keeps its token. With --token, the live lease must carry that token. After a lease has expired or been released, renew is refused and the holder claims the item again. |
release | Gives the lease back. The holder releases a live lease. Anyone may release an expired one, which is how a runner closes out a claim whose worker went away. --outcome says how the work ended, one of the outcomes. |
The command finds the item first, in the work kind --kind names or, without it, in the one work kind the declaration names that has a record with the id. An id no work record has is an error, and so is a claim on an item in a closed state such as done or dropped. Renew and release don’t check the state, so a lease can always be given back.
An Op that declares workLease claims, renews and releases an item’s lease itself, with the same semantics as these verbs: a heartbeat at a third of the lease’s time to live, a lost lease that stops the run, and a release carrying the run’s outcome (#2748). See Running under a work lease. This command is for a runner outside an Op, and for closing out a lease by hand.
Separate clones
Section titled “Separate clones”When the repository has a remote, a claim or renew fetches the lease ref from it before deciding. The write counts only once the remote has taken the push. If another clone claimed the item first, the remote refuses the push. The command then undoes the local write and refuses with lease-push-rejected. A release deletes the ref on the remote too. Workers in separate clones of one remote coordinate through it the way processes in one clone coordinate through the local ref. A repository with no remote coordinates only the processes that share its .git directory.
History
Section titled “History”Every claim, renew and release appends a line to _leases/<id>.jsonl on the chant/lifecycle branch, the append-only shape of the gate ledger. The command fetches the branch first when it fast-forwards and pushes it afterwards. The lease ref is what coordinates, so this push is best-effort. A history push the remote refuses is reported as pushed: false and doesn’t undo the lease.
{"acquiredAt":"2026-09-25T10:00:00.000Z","by":"worker-a","event":"claim","expiresAt":"2026-09-25T10:10:00.000Z","holder":"worker-a","item":"W-001","timestamp":"2026-09-25T10:00:00.000Z","token":"6f0c...","version":1}{"acquiredAt":"2026-09-25T10:00:00.000Z","by":"worker-a","event":"renew","expiresAt":"2026-09-25T10:13:20.000Z","holder":"worker-a","item":"W-001","timestamp":"2026-09-25T10:03:20.000Z","token":"6f0c...","version":1}{"acquiredAt":"2026-09-25T10:00:00.000Z","by":"worker-a","event":"release","expiresAt":"2026-09-25T10:13:20.000Z","holder":"worker-a","item":"W-001","outcome":"done","timestamp":"2026-09-25T10:09:00.000Z","token":"6f0c...","version":1}holder is the lease’s holder and by is who wrote the line: they differ only when someone releases another worker’s expired lease.
Outcomes
Section titled “Outcomes”A release’s outcome is one of a closed list (#3147). release --outcome refuses any other word with write-usage-invalid, and an Op whose step names one outside the list is released not_done. The list is $defs/outcome in work-lease.schema.json.
| Outcome | Meaning | Counts as an attempt |
|---|---|---|
done | The work was finished. | no |
not_done | A build ran and didn’t finish, or the run failed. | yes |
abandoned | Someone other than the holder closed out an expired lease, because the worker went away. | yes |
gated | The run stopped at a gate. | no |
waiting | The run stopped on a decision point’s open question. | no |
dropped | The item was dropped, such as by a refuse at the understand point. Nothing was built. | no |
redraft | The understand point sent the item back to its author to redraft. Nothing was built. | no |
ask | The understand point sent a question back to the item’s author. Nothing was built. | no |
A history written before the list was closed may hold other words. A reader takes such a release as an attempt.
An Op that changes the checkout and ends not_done keeps the attempt’s work at refs/chant/kept/<item>/<token>, or refs/chant/kept/<member>/<item>/<token> in a workspace member. The kept commit holds what the run committed on chant/work/<item> and what it left uncommitted. The branch goes back to where the run started (Ops). The refs are local, and work history lists them.
Evidence
Section titled “Evidence”work evidence attaches one piece of evidence for an acceptance criterion while you hold the item’s lease (#3159). It is the workEvidence activity an Op runs under workLease (#2772), as a command, so a writer that is not an Op, such as hud, writes evidence through chant too. --holder and --token are the lease’s holder and the fencing token its claim printed. --from gives the entry as JSON:
{ "criterion": "AC-1", "result": "pass", "title": "The unit test run", "url": "https://ci.example.com/runs/412" }criterion names one of the item’s acceptance criteria by id, and result is pass or fail. The entry links a url, or names a workspace path that chant pins by its sha256 now. as_of is optional and defaults to now. chant appends the entry to the item’s pins field through records amend, with the criterion’s verification and by set to the holder, and prints the work-evidence document with the criteria counted. The record is written and never committed. When the holder or the token isn’t the live lease’s, chant refuses the entry and writes nothing, as it does for a criterion the item doesn’t list. A manual criterion is refused too: the holder is the item’s implementer, so a person records a manual verdict with records amend instead.
Members
Section titled “Members”The lease lives in the ledger of the member that owns the work kind file (D7), whichever directory the command runs in. A work kind no member owns, such as the reference workspace’s work/work.kind.mjs, uses the flat ledger: refs/chant/lease/work/<id> and _leases/<id>.jsonl. A kind inside member api uses refs/chant/lease/_members/api/work/<id> and _members/api/_leases/<id>.jsonl.
Reading leases
Section titled “Reading leases”chant workspace records --json gives each work record its active lease in lease, or null. chant workspace status --json lists every lease in leases, active and expired. Both read the local refs and the remote’s as last fetched, and never fetch. The MCP tools workspace-records and workspace-status return the same documents.
work history <id> reads the item’s _leases/<id>.jsonl back from the ledger of the member that owns the work kind (#2785). A reader then doesn’t have to work out the member prefix or run git itself. The lines are folded into claims, one per fencing token and oldest first. Each claim carries its holder and token and the time it was acquired. It also carries the expiresAt that its last claim or renew set and its count of renewals. ended says how it ended, as the table below lists.
ended | Meaning |
|---|---|
released | Given back. release has by, at, outcome and note. |
held | It is the live lease now. |
expired | It ran out, and nobody released it or claimed the item since. |
lost | It ran out unreleased, and a later claim took the item. |
Each claim also has attempt, which says whether it counts toward the item’s attempt limit (#3147). A claim counts when it was released not_done or abandoned, released with no outcome, or ended expired or lost. A claim that is held doesn’t count. Each claim’s kept names the ref keeping its unfinished work, or is null. attempts totals them against the limit. The limit is the item’s own field that the work kind’s work.attempts.field names (max_attempts in the reference kind), or else the kind’s work.attempts.max. exhausted turns true once failed reaches the limit, and a runner then leaves the item to people. Without a declared limit, limit and remaining are null and exhausted is false. kept lists every kept attempt of the item. summary counts the claims by how they ended, and outcomes counts the released ones by outcome. events holds every line as written. The read is a read-contract output, work-history.schema.json, and the MCP tool workspace-work-history returns the same document. Like the other reads, it reads the local branch and refs and never fetches. It takes no --holder, --ttl, --token, --outcome or --note.
Options
Section titled “Options”| Option | Effect |
|---|---|
id | The work item’s id, such as W-001. Letters, digits, ., _ and -. |
--holder <name> | Required for claim, renew, release and evidence. Who claims, renews or releases, recorded as given. For evidence, the live lease’s holder. |
--kind <kind file> | The work kind. Without it, the declared work kind that has the id. |
--ttl <seconds|duration> | How long a claim or renew lasts: a whole number of seconds, or a duration such as 10m. Default 10 minutes. |
--token <token> | For renew and release: the fencing token the caller holds. A lease with another token is refused with lease-token-mismatch. |
--from <file|-> | For evidence, required: the entry as JSON, from a file or stdin. See Evidence. |
--outcome <outcome> | For release: how the work ended, one of the outcomes, written to the history line. |
--note <text> | Free text written to the history line. |
--json | Print the result as JSON. |
Exit codes
Section titled “Exit codes”| Code | Meaning |
|---|---|
| 0 | The lease was written. |
| 1 | The command could not run: a usage error, an unreadable kind, an id no work record has, or a claim on a closed item. For evidence, also any refusal, with its code in error.code. |
| 2 | The lease was refused. refused.code says why. history and evidence never exit 2. |
Output
Section titled “Output”With --json, a written lease prints:
{ "$schema": "https://intentius.io/chant/schemas/workspace/work-lease/v1/work-lease.schema.json", "contract": 1, "item": "W-001", "event": "claim", "kind": "work/work.kind.mjs", "ref": "refs/chant/lease/work/W-001", "lease": { "holder": "worker-a", "token": "6f0c...", "acquiredAt": "2026-09-25T10:00:00.000Z", "expiresAt": "2026-09-25T10:10:00.000Z" }, "history": { "path": "_leases/W-001.jsonl", "commit": "3f2a91c0...", "pushed": true }}For release, lease is the lease that was released. A refusal prints refused in place of lease and history, with the live lease in heldBy when there is one:
{ "$schema": "https://intentius.io/chant/schemas/workspace/work-lease/v1/work-lease.schema.json", "contract": 1, "item": "W-001", "event": "claim", "kind": "work/work.kind.mjs", "ref": "refs/chant/lease/work/W-001", "refused": { "code": "lease-held", "message": "W-001 is held by worker-a until 2026-09-25T10:10:00.000Z", "heldBy": { "holder": "worker-a", "token": "6f0c...", "acquiredAt": "2026-09-25T10:00:00.000Z", "expiresAt": "2026-09-25T10:10:00.000Z" } }}An error prints { "$schema", "contract", "error": { "code", "message" } }. The document follows work-lease.schema.json, shipped in @intentius/chant at src/workspace/work-lease.schema.json, and every code is in the one closed list of the read contract.
work history --json prints the document below.
{ "$schema": "https://intentius.io/chant/schemas/workspace/work-history/v1/work-history.schema.json", "contract": 1, "chant": "0.91.0", "item": "W-001", "kind": "work/work.kind.mjs", "ref": "refs/chant/lease/work/W-001", "ledger": { "branch": "chant/lifecycle", "path": "_leases/W-001.jsonl" }, "lease": null, "claims": [ { "token": "6f0c...", "holder": "worker-a", "acquiredAt": "2026-09-25T10:00:00.000Z", "expiresAt": "2026-09-25T10:13:20.000Z", "renewals": 1, "ended": "released", "release": { "by": "worker-a", "at": "2026-09-25T10:09:00.000Z", "outcome": "done", "note": null }, "attempt": false, "kept": null } ], "events": [], "malformed": 0, "summary": { "claims": 1, "released": 1, "held": 0, "expired": 0, "lost": 0, "outcomes": { "done": 1 } }, "attempts": { "failed": 0, "limit": 3, "remaining": 3, "exhausted": false }, "kept": []}events is shortened here. lease is the lease ref’s record now, with state active or expired, or null when there is none. malformed counts lines that are not lease events, which are left out. An error prints { "$schema", "contract", "chant", "error": { "code", "message" } } with one of kind-unreadable, work-kind-missing, work-kind-ambiguous, work-item-unknown or not-a-git-repository, and exits 1.
Refusal codes
Section titled “Refusal codes”| Code | Cause |
|---|---|
lease-held | Someone holds a live lease on the item: another worker, or, for a claim, the same one. |
lease-not-held | Nobody holds a live lease: it expired, was released or was never claimed. |
lease-token-mismatch | The live lease carries another token than --token. |
lease-race | Another writer changed the lease between the read and the write. heldBy names who has it now. |
lease-push-rejected | The remote refused the push, because another clone claimed the item first or the remote could not be reached. |
Error codes
Section titled “Error codes”| Code | Cause |
|---|---|
write-usage-invalid | A missing verb, id or --holder, a flag the verb doesn’t take, or an --outcome outside the outcomes. |
kind-unreadable | The work kind or its records could not be read. |
work-kind-missing | --kind is not a work kind, or the declaration names none. |
work-kind-ambiguous | More than one declared work kind has a record with the id; pass --kind. |
work-item-unknown | No work record has the id. |
work-item-closed | A claim on an item in a closed state. |
not-a-git-repository | The directory is not in a git repository, so there is no ref to write. |
Examples
Section titled “Examples”# Take W-001 for ten minuteschant workspace work claim W-001 --holder "$(hostname):$$"
# Heartbeat while the agent works, with the token the claim printedchant workspace work renew W-001 --holder "$(hostname):$$" --token 6f0c... --ttl 10m
# Give it back when the work is donechant workspace work release W-001 --holder "$(hostname):$$" --outcome done
# Which items are taken, and by whomchant workspace records --kind work/work.kind.mjs --json | jq '.records[] | select(.lease) | {id, holder: .lease.holder}'