workagentwork
v0.1.0
Published
Merge governance for agent-written code: a disk-based task state machine with a recorded-approval merge gate.
Maintainers
Readme
Work, Agent, Work
Work, Agent, Work is a git-native task system for parallel coding agents: every task is a branch, every merge needs a recorded approval, and the audit trail lives in your repo.
Agents write code faster than humans can review it. In an EASE 2026 study of 33,596 agent-authored pull requests, 61% received no human review at all (arXiv:2605.02273). A rubber-stamp approval answers no question when something breaks, and nobody can say who let a change land.
Quickstart: the whole loop in 60 seconds
Prerequisites: git, Node >= 20.6 (full support matrix), a repo you own that already has commits.
npx workagentwork init # inside your repo — sets up work/
npx workagentwork new fix-the-thing
# edit work/proposed/0001-fix-the-thing.md: goal, owns-files, gates
npx workagentwork ready 0001 # machine checks pass it to the queue
npx workagentwork claim 0001 --session me
# you get a worktree: ../<repo>-work/0001 — work there, commit there
npx workagentwork submit 0001 # gates run
npx workagentwork approve 0001 --session me # there is no bypass
npx workagentwork merge 0001 # branch lands, ledger records it
npx workagentwork serve # open the board — or just open
# work/board.html from disknpx workagentwork stats reads back cycle times, decision latency, and when
the board clears. docs/getting-started.md walks
the same cycle with explanations.
What it is
- A merge gate.
mergerefuses to run without a recorded approval — there is no bypass flag — and runs the gates again onmainafter the merge commit, resetting the merge away if they fail. - A task state machine. Tasks are markdown files in
work/<state>/directories (proposed,ready,in-progress,review,blocked,done). State lives in the path, not a status field; any session can pick up a task from its file alone. - Worktree isolation. Claiming a task creates a branch
work/NNNN-slugand a worktree at../<repo>-work/<id>. One task, one branch, one worktree — no shared checkout, no watchers trampling each other. - Disjoint file ownership. Two tasks may not own the same file without a
recorded dependency edge;
readyandclaimrefuse the overlap. - Dependency-aware merge refusal. A task whose declared dependencies are not done cannot merge, no matter how green its own gates are.
- Recorded approvals, on your disk. Every approval, state move, and gate
verdict lands in committed files: the task's
Approved:field, the notes sidecars, and thework/events.jsonlledger.git logis the audit trail. - A derived board.
work/board.html(andboard.json) regenerate on every state change;work serveserves the live board over http.
What it is not
- Not an AI code reviewer. The machine never grants the approval; a human does, on the record. Gates you configure (typecheck, tests) decide what "verified" means; the gate only refuses to move without the recorded decision.
- Not a hosted dashboard. It is a CLI, not a service. State is plain files in your repo, inspectable outside any agent harness. The record stays in the customer's git — there is no telemetry and no sync target.
- Not a merge-conflict resolver. Conflicts are prevented by disjoint file ownership and one-worktree-per-task, not resolved after the fact.
Work, Agent, Work began as the work system of a production repository and was extracted to stand alone. This repo is still governed by its own gate — its board is the reference deployment.
Free, and staying free (open core)
The CLI is Apache-2.0 and stays free. A future paid Team tier adds hosted identity, policy, and review-dossier sync on top; nothing that exists today moves behind a paywall. The boundary and the evidence gates behind the paid tier are public: docs/roadmap.md.
Documentation
- docs/compatibility.md — what the CLI is tested on, and what is known but untested.
- docs/format.md — the normative on-disk task format specification.
- docs/getting-started.md — the full first cycle, explained.
- docs/manifesto.md — the product principles.
- docs/roadmap.md — the revenue path and its gates.
Install
No install step is required: npx workagentwork <command> fetches and runs the CLI
on demand (Node >= 20.6). To install once and use it everywhere:
npm install -g workagentwork # then: work <command>Claude Code plugin
Work, Agent, Work ships as a Claude Code plugin: .claude-plugin/plugin.json is the
manifest, and .claude-plugin/commands/work.md registers a /work command
whose body is a thin pointer — it runs work --help and follows the
printed usage, so the CLI stays the single source of truth. Add this
repository as a plugin marketplace and install workagentwork from it; the exact
commands follow the
Claude Code plugin docs.
Commands
| Command | Effect |
|---|---|
| work init | bootstrap work/ in this repository and write the agent-instruction block to AGENTS.md/CLAUDE.md; refuses an existing work/ tree |
| work new <slug> [--title T] [--parent P] | scaffold a task file into work/proposed/ (built-in) |
| work ready <id> [--allow-overlap] | proposed\|blocked -> ready (human gate; refuses Owns-file overlaps) |
| work unblock <id> | alias for ready |
| work block <id> [reason…] | active state -> blocked, reason appended (built-in) |
| work claim <id> [--session S] [--purpose P] [--force] | ready -> in-progress; writes claim block, creates branch + worktree, runs the configured install; refuses unmet deps; held under work/.claim.lock; --purpose updates the session's registry sidecar |
| work unclaim <id> [reason…] | in-progress -> ready; release a stalled claim (branch kept; clean worktree removed, dirty kept) |
| work note <id> <text…> | append a timestamped progress line to the task's notes sidecar (work/notes/<id>.md), no state change |
| work tag add <id> <text…> / tag rm <id> <text> | add/remove a task tag (exact match, case-sensitive, no commas); no state change; editable states: proposed, ready, blocked |
| work priority <id> <n> | set task priority (integer 0–4, 0 most urgent); no state change; same editable states as tag |
| work flag <id> <text…> / flag rm <id> <n> | record/clear an open decision on a task in the sidecar work/flags/<id>.md; rendered as ⚑ chips on the board; works on review tasks (task file untouched); refuses done |
| work submit <id> [--no-wait] [--wait S] | in-progress -> review; enqueues a durable record under work/queue/ and a detached executor (work gates-run, spawned automatically) runs the configured gates in the worktree — on drift it first replays the branch onto main (conflicts abort cleanly; gates re-run there). Waits within a bound (default 600s) unless --no-wait; a failed run records the failing gate and output tail, and the task stays in-progress. An orphaned executor's runs are reaped and re-enqueued automatically; a wedged one is reported, never killed (built-in) |
| work approve <id> [--session S] | record reviewer approval in the notes sidecar of a review task; required before merge |
| work session <name> [--purpose "line"] | register a session's purpose in work/sessions/<name>.md (create or update); without --purpose prints the record; no state change |
| work sessions | list registered and ledger-seen sessions: name, derived last-seen age, purpose line; read-only |
| work status | one screen of submit-queue state: queue depth, current run with duration, orphaned and wedged entries with ages, recent failures with failing gates; read-only |
| work adopt <id> --session S | take over a claimed task: records the handoff (claim-field rewrite + ledger event) and prints the full brief — task file, state, claim history, latest queue record with failing gate and output tail, suggested next step |
| work merge <id> [--dry-run] [--takeover\|--abort] | review -> done; requires approval; refuses unmet deps and an unclean main tree; merges branch, verifies the gates on main, removes worktree, deletes branch. The merge lease spans a conflicted merge's resolution window; --takeover adopts a foreign open merge and --abort backs one out (both recorded) |
| work rebase <id> | replay an in-progress/review task branch onto main in its worktree, then re-run the gates there; conflicts abort cleanly; records a sidecar note; no auto-merge; held under work/.rebase.lock |
| work list [--json] [--state a,b] | one line per task; --json emits a machine-readable array, --state filters |
| work board | regenerate board files |
| work serve [port] | serve work/ over http for the live board (default port 8080); regenerates the board on start |
Commands are discovered modules: a file exporting command with name,
usage, run in the CLI's commands/ directory registers itself — no
entry-file edit. A module colliding with a built-in dies at startup.
Trains: staged branches for units of work
work stage <parent-id> opens a cohesion stage: branch stage/<parent> plus
a staging worktree. Children of the parent then merge into the stage
(routing is derived — no flags, no config), each with its own recorded
approval; when the subgraph is done, the parent's own submit/approve/merge
promotes the stage to main with one operator decision for the unit. Trains
are recorded in work/trains/ and shown by work list. A repo that never
stages behaves exactly as before.
Tiered gates: local gates run per ticket and may be trimmed; the ci
tier in work.json is the full, unselected suite, run by CI on the pushed
train branch. The verdict comes back as a branch in your repo
(verdict/stage-0003, tip subject verdict: green <sha>) — the CLI reads
it with a plain fetch, no token, no gh. A green verdict moves the train's
lastGreen.
Red trains restore first and attribute second: a red verdict rolls the
train branch back to its last-green ref (the recorded --force-with-lease
exception — main is never force-pushed) and the record lands as
unattributed. A red cohesion train reopens its parent; a red economy
train unbundles and retries its members individually. Closing the
investigation is an agent's recorded act:
work stage <train> --attribute <single-ticket|interaction|flake|infra>.
Nothing blocks on attribution, and queued work proceeds.
Economy trains (work bundle <id> <id>...) batch independent, finished
tickets so one CI run amortizes across them. Membership is imperative; the
premise is independence, and the red policy unbundles when it fails. Each
ticket keeps its own recorded approval (work approve takes several ids;
each records its own verdict) and lands on main individually. Bundles are
pointless on a single machine — they exist to save CI runs, so use them
only when several approved tickets are waiting on the same CI.
Audit: the trust-on-pull ritual
work audit replays the event ledger against the work tree and reports
drift: a task file with no event chain, an approval whose ticket never
merged, a done file touched after its done event, a state event with no
tree change behind it. Each finding carries a file pointer and an event
pointer. Audit refuses nothing and repairs nothing — findings exit nonzero,
a clean repository exits zero, and the tree is untouched. After a git
fetch from a machine you do not fully trust, run work audit before you
believe its work/; solo, it is the one-command integrity check.
Sync contract (multi-machine)
Etiquette in one place: pull before you decide, push after you change (the
CLI does both when a remote exists); sweep orphaned trains and failed
submits as part of normal work; UAT staged features before approving
promotion; choose stage/bundle/direct-to-main by cohesion and cost.
Liveness is per machine — a pid proves nothing across machines, so
cross-machine orphan classification is age-based and takeover goes through
the recorded paths only (lock break --force, merge --takeover,
work adopt). The same rules ship in the init-written instructions and the
public docs, word for word.
The CLI never runs git pull blindly and never force-pushes; the remote is
the cross-machine serialization point:
- Before deciding, every state-changing command fetches and fast-forwards the current branch (rebasing local commits when they diverge), so decisions are made against the remote's truth. An unreachable remote warns and the command continues local-only — being offline never stops solo work.
- After committing, the same commands push. A rejected push means another machine moved the branch: fetch, pull-rebase, revalidate the decision precondition against the post-pull state, retry once.
- A second loss reports the race by name —
0007 was claimed by session agent-2 on another machine— records araceevent in the ledger (work/events.jsonl), and exits nonzero. The local commit stays; it lands with the next successful push. - The loser never faces a merge conflict: a conflicted pull-rebase aborts and reports instead of leaving one open.
- Local locks (
work/.*.lock) keep their same-machine duty; the push race takes the cross-machine duty. Two layers, one pattern. - A repository without a remote behaves exactly as before: no fetch, no push, no change.
Configuration
Gates and install (per repository)
Nothing about your build tooling is hardcoded. The gate commands that submit
(in the worktree) and merge (on main) run, and the install command claim
runs in a fresh worktree, are configured in work.json at the repo root — as
argv arrays, so any runner works:
{
"install": ["pnpm", "install", "--prefer-offline"],
"gates": [
["pnpm", "run", "typecheck"],
["pnpm", "test"]
]
}work.json is the only config file: the CLI never reads configuration
from package.json. A repo without work.json simply runs no install
and no gates.
- Gates run in order; the first failing one is named in the refusal, in the task's notes sidecar, and in the merge-recovery messages.
- A repo without
work.jsonskips the install and gate steps gracefully (a notice is printed, nothing fails); a repo with neitherwork.jsonnorpackage.jsongets the foreign-repo warning instead. Non-JS repos — Python, Go, Java, anything — can point the argv atmake,cargo,just, a shell script, anything. work merge --dry-runpreviews every pre-flight check, the commit range, diffstat, and the Result/notes tails without mutating anything — the review dossier before approving.- Gate evidence: every gate run appends a line to the task's notes sidecar — gate name, duration, exit status — so a gate verdict can be audited after the fact.
- Gate environment: gate processes run with
CI=1andWORK_GATE=1in their environment; suites that switch behavior under CI (log levels, watch mode) see the same environment insubmitandmerge. - No retry path: one bounded run per invocation; a red gate stays red. Fix the branch and re-run the command — that is a new, recorded run, not a retry of a failed one.
Gate concurrency (per repository)
Concurrent submit/merge runs in several sessions can oversubscribe the
machine and make gates fail under load — a green gate should mean green
regardless of who else is running suites. Set gateConcurrency to bound
simultaneous gate runs across all sessions:
{
"gateConcurrency": 2,
"gates": [["npm", "test"]]
}- Work, Agent, Work budgets concurrency across sessions: runs beyond the limit
queue on a semaphore under
work/.gate-slots/(never committed), printingqueued behind N run(s). The wait is bounded; when no slot frees in time, the run refuses withno gate ran— it does not run the gate anyway. - The repo's gate argv budgets resources within one run (for example the test runner's worker cap). The two budgets are independent on purpose.
- A crashed holder's slot (dead pid) is recovered automatically, like a stale
lock. The key is absent by default: unlimited, and no semaphore directory
is created.
gateConcurrencymust be a positive integer; anything else is ignored with a warning.
Conventions kept from the extracted system
- Worktree root:
../<repo-basename>-work/<id>relative to the repo root; branch prefix:work/. Fixed, not configurable. - Session identity: commands that attribute actions (
claim,approve) take--session S, otherwiseWORK_SESSION_ID, otherwiseZCODE_SESSION_ID, otherwisemanual-<ts>. A session can state its purpose in a registry sidecar (work session <name> --purpose "…", read back withwork sessions); last-seen derives from the events ledger, never from the sidecar. - Specialist field: a free-text role label in the task file format. Work, Agent, Work
attaches no semantics to it;
claimmerely prints it.
Task file format
work/<state>/NNNN-slug.md, fields as bold bullets. The normative
specification of this format — fields, states, transitions, invariants — is
docs/format.md:
# 0007-add-export-button
- **Title:** Add export button
- **Parent:** docs/prd/0001-mvp.md
- **Depends on:** 0005, 0006 (bare 4-digit task ids only)
- **Owns files:** src/ui/export.ts, src/ui/export.test.ts
- **Branch:** — (filled by claim)
- **Claim:** — (filled by claim: session @ timestamp)
- **Approved:** — (on done copies only, assembled by merge)
- **Specialist:** — (free-text role)
- **Tags:** — (comma-separated labels; — means none)
- **Priority:** 2 (integer 0–4, 0 most urgent)
## Goal
Self-contained description: what and why, pointers to docs and code.
## Acceptance criteria
- [ ] Verifiable condition
## Result
Filled in by the claiming session; submit renames the file into review/ on
main unchanged, and merge folds this section plus the notes sidecar into the
done copy.Notes and approvals never edit the task file while a branch holds a copy of
it: note and approve write to work/notes/<id>.md, and merge assembles
the done copy (branch body wins, notes land under ## Notes, the approval
becomes the Approved: header field). This keeps task-file merges
conflict-free by construction.
Read freshness
Every board generation stamps board.json with generatedAt and rev (the
main HEAD sha); text list prints the same stamp as an
as of <generatedAt> @ <rev> header. The contract: a board older than your
last action is a rumor — re-read it before claim or merge, and treat a
decision command's refusal as authoritative over any earlier snapshot.
Decision commands do not trust the snapshot regardless: claim re-validates
the task's state and dependencies under work/.claim.lock, immediately before
the claim write.
Invariants
- State moves happen only through the CLI, never by moving files by hand.
board.json/board.htmlare derived; every state change regenerates and auto-commitswork/.claimserializes onwork/.claim.lock;mergeonwork/.merge.lock;rebaseonwork/.rebase.lock.- The claim race guard uses
link()+unlink(), so two sessions cannot both claim one task. - Merge has no bypass for the approval gate.
- Board edits over
servego through the sametag/prioritycommands as the terminal (one allowlisted code path); thefile://board snapshot is read-only.
Development
npm install
npm run typecheck
npm test
npm run build # tsc -> dist/ (bin: dist/work.js)License
Apache-2.0 — see LICENSE.
