The xSwarm Work Harness
How work becomes shipped code here — the stages, the rules that do not bend, the commands, the hosts, and the traps that have already cost real time.
How work becomes shipped code here. Chad, 2026-09-12: “I asked for a clear ‘xSwarm Work Harness’ document so we could have a clear plan for how to translate feature specs into quality-assured code implementation. This is the most valuable thing we can do.”
Read this instead of re-deriving it. Every fact below was learned the expensive way by a session that had to work it out from the code. Working it out again costs the same tokens and reaches the same answer.
The shape
request ──plan──▶ ticket ──▶ Planning ──▶ Coding ⇄ Testing ──▶ Refactoring ──▶ Staging ──▶ Deployed #<release>
└── code⇄test loops here ──┘ └─ its own test ─┘
A request is what someone asked for. A ticket is a request broken into work with BDD
requirements and dependency edges. Requests live in the requests table; tickets live in
tasks. A request becomes tickets through POST /api/requests/:id/plan, which needs the
request to have a project_id and the plan to carry tasks[], each with a non-empty bdd.
The six stages are defined once, in packages/api/src/lib/stages.js, and imported everywhere
— the board’s columns, the funnel’s steps, and the watchdog’s evidence are all that one list.
Do not add a seventh without changing that file; three vocabularies for one pipeline is a defect
this project has already paid for, and the 2026-09-22 rename proved it again: packages/app/src/task/runner.mjs spelled
one stage inline, so renaming it silently made every landed ticket re-dispatch instead of resume.
ALL TESTING HAPPENS BEFORE THE BRANCH LEAVES, E2E INCLUDED. Chad, 2026-09-15: “we decided
to make all testing, even E2E, part of the initial coding portion so that we never submit a pull
request or merge to trunk without having tested completely first. Just as we would require a
developer to only push tested code.” As of 2026-09-22 that proving is its own stage rather than
the back half of Coding — Chad: “our coding stage has two parts code⇄test and refactor⇄test, but
testing is more significant when first coding, so the ticket can go back and forth between code
and test and then progress to refactor (which includes its own test).” Nothing moved out of the
worktree; the loop is simply measured where it happens. The split paid for itself immediately: 51
unearned advances that read as “coding” were all at Testing. E2E was previously its own stage, running against a merge candidate
after the branch was already pushed. That contradicted the push gate in
packages/app/src/hooks/gate.mjs, which has always refused a push without a green
critical-path run for that exact head sha. The gate was right, because it is the one that
actually runs.
Each stage starts from the evidence the one before it ended with. A planned ticket that has not started has no column — that is queue depth, not work.
| Stage | Means | Entry evidence | Exit evidence | Performed by | Code |
|---|---|---|---|---|---|
| Planning | the ticket is fit to work: an executable spec, a named test, declared touches, a design brief | a planned ticket from POST /api/requests/:id/plan |
none — scored, never gated | the planner agent | packages/app/src/task/planner.mjs, packages/app/src/task/spec.mjs |
| Coding | the change exists as a commit | none — scored, never gated | commitExists |
the coder agent | packages/app/src/cli/run.mjs, packages/app/src/task/loop.mjs, packages/app/src/task/dispatch.mjs |
| Testing | the change is PROVEN: seen failing, seen passing, green end to end | commitExists |
testObservedFailing, testObservedPassing, e2ePassed |
the pipeline (xswarm run) |
packages/app/src/task/observe-red-green.mjs, packages/app/src/task/dispatch.mjs |
| Refactoring | cleanup, whole suite still green | testObservedFailing, testObservedPassing, e2ePassed |
refactorRan, testsStillPassing |
the coder agent | packages/app/src/task/refactor.mjs |
| Staging | branch pushed, candidate built, tested sha landed — landing on trunk is what publishes to staging | refactorRan, testsStillPassing |
branchOnRemote, candidateBuilt, landedOnTrunk |
the pipeline (xswarm run) |
packages/app/src/task/dispatch.mjs, packages/app/src/task/merge-queue.mjs |
| Deployed #N | verified live in production | branchOnRemote, candidateBuilt, landedOnTrunk |
deployVerified |
the deploy script | packages/app/src/task/deploy-step.mjs, packages/app/src/task/deployed.mjs, scripts/deploy-prod.mjs |
The pipeline is xswarm run (packages/app/src/cli/run.mjs) ticking packages/app/src/task/loop.mjs,
which hands each ready ticket to packages/app/src/task/dispatch.mjs (worktree, agent, red/green
observation, refactor, push) and then to the merge queue; packages/app/src/task/publish.mjs
calls the deploy step once the ticket has landed.
How a ticket reaches git
Planning defines the proof — the BDD scenarios that would show the feature correct, and the path of the test that carries them. Then, in the worktree:
commit 1 test: the criterion, made executable, RED ← nothing implements it yet
commit 2 feat: first attempt Tests: red
commit 3 fix: what that attempt got wrong Tests: red
commit n fix: green Tests: green
└─ push, PR, squash onto main, tag
The trail is the documentation. Every commit says whether it was red or green, so a history containing failing commits is safe to read rather than a minefield for anyone bisecting. The agent’s own account of what it tried goes in the commit body — written at the moment the work happened, by the thing that did it.
The first commit is the failing test, and that is verified rather than asked for. trailFacts
reads the commits back and records whether the test came first, plus iterations, iterationsRed
and iterationsUnlabelled — measured facts about the attempt; no stage gates on them. A test committed green before any implementation exists is
the wrong test — it proves nothing, which is the whole failure red/green exists to catch — so that
fails the contract too.
Push only when green; squash onto main. The rule is only push tested code, and it constrains pushes, not commits — git separates them precisely so a branch can carry the whole messy trail while every published state is proven. Main gets one green, deployable commit per ticket, because every sha on main is potentially a release; the iteration history lives on the branch and the pull request, which GitHub keeps.
Planning carries no proof key, deliberately. stageFor walks to the first INCOMPLETE stage, so
a required key on the first stage outranks every fact established after it — making designed
required re-filed all 191 tickets under Planning, including 91 serving in production. Planning is
therefore scored from the ticket’s own shape (xswarm score) and gates nothing.
Not-started is not Coding. A ticket with no evidence has no column. Rendering ready work as in-progress made the board show 37 cards nobody was working on.
The rules that do not bend
- Failures are work, not stop conditions. A red test means repair the code or correct the test — and say which you found. Never weaken a test to make it pass.
- Verify at the destination. A push is not a deploy. Fetch the thing and read what it says. Code that looks correct has been wrong three times in one day here.
- Derive, don’t store. A value that can be derived is derived at read time. Every stored copy is a clock someone has to wind, and it will drift.
- Absent is not green. A check that could not run is
not_run, never a pass. A suite that cannot launch a browser has verified nothing. - One rule, one place. When several models describe one reality, delete copies until there is one.
- A guard needs a reachable failure. Name the input that reaches it, or it is dead code.
- Nothing leaves a worktree untested.
packages/app/src/hooks/gate.mjsdenies a push without a green critical-path run recorded for that exact head sha.
Commands that matter
| Command | What it does |
|---|---|
npm run brief |
Branch, unpushed, daemon sha, production release, queue depth. 5 lines, ~0.3s. Run this to orient. |
npm run status |
24 checks across deployment, runtime, code-vs-reality and open work, ~6s. A diagnostic for when something is wrong, not an orientation. |
npm test |
The suites. Use this, never a bare root vitest — that walks node_modules. |
npm run test:critical |
The push gate’s suite, budgeted at 90s. Exceeding the budget fails. |
npm run repro:visual |
Reproduces the visual snapshot failures against staging and saves every expected/actual/diff into tmp/visual-repro/<stamp>/, with a triage of which diffs are one shared strip of chrome and which are a route’s own. Exits non-zero on an empty capture — nothing failing means it failed to reproduce. |
npm run deploy:stage |
Pushes to the staging branch; Cloudflare builds. Waits on sha. |
npm run deploy:prod |
E2E against staging, promotes the tested sha, verifies, then mints. |
npm run ship |
Both, in order. |
npm run verify:release |
Production reports the release it carries, on every surface. |
npm run verify:pipeline |
A push to production still IS the deploy: the trigger is there, the last build came from a push event, and the build is what uploaded. Needs the setup-time admin token. |
npm run daemon:version |
What sha the daemon is actually running. Non-zero if stale. |
What gates, and what only reports
The difference matters more than any individual check. A gate can stop the work; a report cannot, and must never be mistaken for one.
Gates — these refuse:
| Gate | Refuses |
|---|---|
packages/app/src/task/spec.mjs isAdmissible |
a ticket whose acceptance criteria are not executable. No Given/When/Then, no ticket. |
the run lock (.xswarm/local/run.lock) |
a second run on the same checkout. One ticket at a time is a file, not a parameter. |
the merge gate (packages/app/src/task/runner.mjs) |
merging a ticket whose stages are not complete, naming the stage and the missing key. |
| the merge queue | landing a red candidate. Trunk is never red because the candidate is tested first and discarded if it fails. |
the push gate (packages/app/src/hooks/gate.mjs) |
a push with no green critical-path run recorded for that exact head sha. |
packages/app/src/task/decision.mjs |
a hand-written proof key. deployVerified is the strongest forgeable claim on a row. |
quality/gate.mjs |
a regression against the recorded baseline, and any failing test at all. |
scripts/deploy-prod.mjs |
promoting a sha whose E2E found a production risk. Production simply does not move. |
Reports — these inform and never block:
xswarm score, xswarm monitor, npm run brief, the board, the ticket audit, the published pull
request record, and planning quality. Planning is scored precisely because gating it would put a
required key on the first stage, and stageFor walks to the first incomplete stage — which would
re-file every shipped ticket under Planning. That was tried on 2026-09-22 and reverted the same
hour.
Observability
Three surfaces, one derivation. Nothing here is stored: a saved score is another column that can drift from what happened, which is exactly what 91 fabricated deploys were.
| Where | What it answers |
|---|---|
npm run brief |
Is anything wrong right now? Five lines. Adds a stages line only when a stage did not earn its result. |
xswarm monitor |
Every pipeline fact in one answer — board, per-stage scores, efficiency, runner, deploy. Same function /api/monitor calls, so the terminal and the dashboard cannot disagree. |
xswarm score |
Quality, stability, speed and token cost per stage, per ticket, per release. |
The four dimensions
| Measures | Source | |
|---|---|---|
| quality | the share of a stage’s required evidence actually held | evidence keys, read from the lifecycle |
| stability | measured steps that passed first time | step events |
| speed | seconds against a declared budget | step events |
| tokens | the same, against a token budget | step events |
unearned is the headline — a ticket sitting past a stage whose proof is incomplete. That
is the shape 77 of 91 “deployed” tickets took, and no count of tickets can show it.
Two rules the scores obey, both learned by getting them wrong first:
- Unmeasured is not perfect. A dimension with no measurement is
null, never 100 and never 0. - Unreached is not low quality. A stage a ticket never got to is excluded, or a pending backlog drags the average down and a real improvement reads as a collapse.
xswarm score # per stage, all time
xswarm score --since 2026-09-21 # only work done since
xswarm score --by-ticket # one line per ticket, oldest first — the trend
xswarm score --by-release # per version, which is the axis improvement moves along
xswarm score --ticket <id> # every stage of one ticket, naming the missing key
Each ticket’s score carries the release that shipped it, derived from the git tag containing its landed sha, and a link to its pull request.
What must exist
| Why | Hard? | |
|---|---|---|
| git, with a remote | the merge queue fast-forwards trunk, the deploy pushes branches, the push gate reads remote state | yes — there is no pipeline without it |
| Node | the runner, the CLI, the suites | yes |
| an xswarm.ai account | the board, the ticket source, the machine’s identity — see Getting Started | yes, for the web interface |
| Playwright browsers | the critical path and the E2E suite | yes, for the testing stage |
Cloudflare watching staging and production |
the deploy IS a branch push; no CI service is involved | yes, for deployment |
gh |
publishing the development record, and the PR link in score |
no — reporting only, and a failure is reported as unknown rather than “no PR” |
No GitHub Actions. Deployment is a branch push that Cloudflare subscribes to, which costs no CI minutes and has no workflow to keep in step with the pipeline.
The CLI
npx xswarm login and npx xswarm project add connect a machine and register a project — see
Getting Started. After that:
| Command | What it does |
|---|---|
xswarm task add "<title>" --spec "<gherkin>" --test <path> |
File a ticket. Refused without executable criteria. |
xswarm run --watch |
Tick the loop. --max N stops after N tickets finish. Takes the run lock. |
xswarm sync |
Pull the board’s tickets down, report their stage back up. |
xswarm score |
The four dimensions, per stage, per ticket, per release. |
xswarm monitor |
Every pipeline fact in one answer. |
xswarm stages |
Where tickets die and where the time goes. |
xswarm publish <ticket> |
Put a ticket’s development record on its pull request — spec, every attempt, the refactor, the scorecard. |
xswarm reap --dry-run |
Processes and worktrees this project left behind. Scoped to this repo; it will not touch another project’s browser. |
xswarm work start <ticket> |
Declare what this session is working on, so the stop hook knows. |
Hosts, exactly
| Role | Host |
|---|---|
| production web | https://xswarm.ai |
| production api | https://api.xswarm.ai |
| staging web | https://stage.xswarm.ai (worker: xswarm-web-staging.chadananda.workers.dev) |
| staging api | https://api-staging.xswarm.ai |
There is no staging.xswarm.ai. Pointing a test suite at it produces 149 DNS failures that
look like application defects.
Releases
A release version names code that reached production. It is minted only after production is verified serving the tested sha, so a build cannot stamp its own release number — at compile time the number does not exist yet.
- The version is a git tag on the tested sha. The sha is the artifact’s identity.
deploy-prodmints it seconds after verification, in the same run. There is no sweep.- A deploy with no user-visible change mints nothing, and still carries the previous
release.
unreleasedis correct only before the first release ever. - A deploy that fails verification is rolled back automatically. The last deployment
observed rendering is recorded by the probe that observed it (
scripts/deploy-rollback.mjs, ledger at~/.xswarm/deploy-verified.json); a FAILED probe restores it — a leased force-push of that sha ontoproduction— then re-probes what came back, because a restore command is not a working site. Only a PASSED re-probe counts as rolled back; anything else says MANUAL ACTION REQUIRED and names why. The failed deployment id and the outcome go in the ledger either way. The deploy still exits non-zero: a rollback is not a success. - Anything displaying a version resolves it at read time:
GET /api/release/current, or/healthand/build-info.json, which reportreleaseandbuildVersionseparately.buildVersionispackage.json’s number and is not the release.
Traps that have cost real time
git add -Ain a worktree stages thenode_modulessymlinks the runner creates. They carry absolute paths; merging one poisons every checkout. 46 empty branches came from this.npm run X -w <pkg>does not forward--arguments. A--projectfilter passed that way is silently dropped and the full matrix runs.wranglerneeds the credential from.env. The deploy scripts callprocess.loadEnvFile()first; a barewranglerinvocation fails with “set CLOUDFLARE_API_TOKEN” and it looks like a permissions refusal.- An untracked file counts as a dirty tree and aborts
npm run ship. - The visual suite SKIPS its pixel comparisons against staging, and a run full of skips reads
like a clean one. A committed baseline is a photograph of one build, and staging carries the
last PROMOTED sha rather than yours, so comparing them reports the gap between two builds as a
UI regression once per route — that ticket has been filed five times.
helpers/snapshot.tsrecords UNMEASURED instead. To actually look at the diffs,npm run repro:visual; nothing else may setVISUAL_REPRO, and a test fails if anything starts to. - pm2’s
versioncolumn ispackage.json’s version, not the running code. It read 3.0.136 for a daemon three hours into code from before that day’s work. networkidlenever fires on pages holding a connection open. Wait on the element.playwright installtimes out wherecurldoes not. Its downloader gave up at 30s on every mirror while a direct fetch of the same URL ran at 3.5MB/s. Download the build zip and unzip it into~/.cache/ms-playwright/<browser>-<rev>/, thentouch INSTALLATION_COMPLETE.- WebKit needs system libraries and therefore root.
sudo apt-get install libicu74 libxml2 libflite1, orsudo npx playwright install-deps. Without them WebKit fails at launch while Chromium and Firefox are fine, so a suite looks two-thirds healthy rather than blocked.
Where things live
| Thing | Path |
|---|---|
| Stage vocabulary | packages/api/src/lib/stages.js |
| Planning + scheduling | packages/api/src/lib/task-plan.js |
| Push gate | packages/app/src/hooks/gate.mjs |
| Worktrees | packages/app/src/daemon/task-worktree.js |
| Deploy pipeline | scripts/deploy-stage.mjs, scripts/deploy-prod.mjs |
| The branch subscription Cloudflare builds | declared in scripts/verify-build-trigger.mjs, held in the Cloudflare account |
| Rollback on failed verification | scripts/deploy-rollback.mjs |
| Quality ratchet | quality/gate.mjs |
| Agreed build order | planning/workflow-implementation-plan.md |
The human’s role
Planning, iterating on UI, defining requirements, acceptance. Never QA, testing, organising work, babysitting agents, standards enforcement, or auditing — those scale with work in flight, and a human doing them is a ceiling that gets worse as the system gets more productive.
If the reason to message a human is so they can say “keep going”, that is babysitting: the system is broken, not the human.