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

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. One rule, one place. When several models describe one reality, delete copies until there is one.
  6. A guard needs a reachable failure. Name the input that reaches it, or it is dead code.
  7. Nothing leaves a worktree untested. packages/app/src/hooks/gate.mjs denies 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-prod mints 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. unreleased is 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 onto production — 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 /health and /build-info.json, which report release and buildVersion separately. buildVersion is package.json’s number and is not the release.

Traps that have cost real time

  • git add -A in a worktree stages the node_modules symlinks 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 --project filter passed that way is silently dropped and the full matrix runs.
  • wrangler needs the credential from .env. The deploy scripts call process.loadEnvFile() first; a bare wrangler invocation 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.ts records UNMEASURED instead. To actually look at the diffs, npm run repro:visual; nothing else may set VISUAL_REPRO, and a test fails if anything starts to.
  • pm2’s version column is package.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.
  • networkidle never fires on pages holding a connection open. Wait on the element.
  • playwright install times out where curl does 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>/, then touch INSTALLATION_COMPLETE.
  • WebKit needs system libraries and therefore root. sudo apt-get install libicu74 libxml2 libflite1, or sudo 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.