Card Pipeline: Technical One-Pager

31 Oct 2026 · Moeed Bhatti

Overview

A local Node service carries each card on the Trello board from To-Do to production. Four headless agents do the work in isolated git worktrees; the orchestrator, plain code over SQLite, reads their structured output and moves every card. Production ships only when a person drags the card to Approved.

Architecture

The orchestrator alone moves cards, merges and deploys Laptop: packages/agent-pipeline Trello board To-Do, Bug: new work Approved: ship it polled and moved by trello.js (REST) server.js webhooks, 2-minute poll, one-instance pidfile orchestrator.js + rules.js state machine: decides from agent JSON store.js: SQLite cards, runs, locks, events progress.js: progress files, card comments runners.js + prompts/ one claude -p run per role, tool allowlist own git worktree and branch per card own Postgres test database per card github.js gh: PRs, serial merge, freeze, dispatch GitHub PR checks (ci.yml) deploy.yml DEPLOY_FREEZE AWS via SST sst deploy smoke, 10-min watch rollback to tag result gh deploy
Architecture · laptop service, GitHub, AWS

Agents never touch git remotes, GitHub or Trello: the orchestrator does every push, merge, deploy dispatch and card move, and the deploy workflow reports back through /webhook/deploy (an ngrok static domain). .github/scripts/smoke.mjs is the smoke test both the deploy and a person can run.

State machine

A card waits for a person only in the highlighted lists To-Do / BugPlanningBuilding VerifyingDeployingFixing BlockedAwaitingapproval ApprovedDeployingDone you add itplannerimplementer verifierPR, CI, mergefixer, 2 tries to production you answer,back to To-Do you drag it herelive, tag moved re-plan once fail fixed red CI merged questions, split 2 failed fixes or same issue twice fails: rolled back retry
State machine · Trello lists as states; yellow lists are where a person acts

Every move is computed in rules.js from a runner's structured outcome (ready, built, pass/fail, fixed, merged, ci_failed, conflict); the highlighted lists are where a person acts. A planned card also waits in Planning while another card holds its files or, for risky work, until the approved label is on. Implementer failures retry once; a merge conflict rebuilds once on the latest main. A failed production deploy returns the card to Fixing with a fresh budget and freezes deploys until that fix is live.

Agent runs

Each role is one claude -p --output-format json run with only the tools it needs, no MCP servers (--strict-mcp-config) and only the project's settings (--setting-sources project). Anything outside its allowlist is refused headless.

RoleWorks inToolsTurns / timeout / spend capReturns
PlannerDetached worktree at origin/mainRead, Glob, Grep, git log30 / 15 min / $2Plan JSON, zod-validated, one corrected retry
ImplementerCard branch from origin/mainRead, Edit, Write, the repo's test, lint, typecheck and drizzle commands, read-only git40 / 30 min / $6Free text; NEEDS_REPLAN: line to stop
VerifierCard branch, detachedRead, Glob, Grep, read-only git, the repo's checks30 / 20 min / $3Review JSON: criteria with evidence, issues
FixerCard branch (or fix/...-prod-regression off main)As implementer25 / 20 min / $4FIXED n / NOT_FIXED / NEEDS_HUMAN / NEEDS_REPLAN lines

Safety and concurrency

Every guard below is enforced in the orchestrator, not asked of an agent.

GuardHow
One orchestratorExclusive-create pidfile in DATA_DIR; a stale one from a dead process is replaced.
One step per cardSQLite lock taken in one INSERT … ON CONFLICT … WHERE expired statement, 60-minute TTL; all locks cleared at startup.
Parallel capMAX_PARALLEL (2) cards in Planning, Building, Verifying or Fixing.
Shared filesA planned card waits while another card in Building, Verifying, Fixing or merging lists any of its files, so it builds on top of that change.
Risky workA plan with risk: high or migrations: true waits for an approved label before Building.
Fix budget and loop guardAt most MAX_FIX_ATTEMPTS (2) fixes; the same blocking file or unmet criterion two rounds running blocks at once.
Circuit breakerThree cards failed in a row pause all new work until pnpm --filter @app/agent-pipeline resume.
Daily budgetNew agent work stops when today's summed total_cost_usd reaches DAILY_BUDGET_USD (10).
Kill switchPIPELINE_PAUSED=1, or a card named STOP in To-Do or Bug. Steps in flight finish; nothing new starts.
Dry runDRY_RUN=1 (the default) runs agents but only logs pushes, PRs, merges, deploys and Trello writes.
RestartCard states come back from SQLite; worktrees are pruned; a merge first checks GitHub for an already-merged PR.

On this setup the agents use the owner's Claude Pro login, so total_cost_usd is an API-equivalent estimate charged to the plan's usage limits, not a bill.

Merge and deploy

The repo is private on GitHub Free, which has no merge queue, auto-merge or required reviewers; github.js stands in for the queue and Trello's Approved list is the approval gate.

  1. Serial merge. Reuse or open the card's PR (title = commit subject; body = card link, goal, criteria, verifier nits). Wait for green CI. If the head is behind main, run gh pr update-branch and wait again. Squash-merge with --match-head-commit, one card at a time behind an in-process lock. Red CI goes to the fixer with the failed log; a conflict rebuilds the plan once on the latest main.
  2. Dispatch-only production. A merge ships nothing. When the card reaches Approved, the orchestrator runs gh workflow run deploy.yml -f stage=production -f sha=<merge commit>. The workflow refuses a commit not on main and any run while DEPLOY_FREEZE is on, unless allow_frozen=true (the fix that ends a freeze).
  3. Deploy. Typecheck, lint and the db tests on that commit, OIDC into AWS, sst deploy scoped by deploy-scope.sh against the production-last-good tag, pnpm test:smoke (API health, a public Postgres-backed route, the platform shell), then 20 health checks over 10 minutes.
  4. Outcome. Success moves production-last-good to the commit, and every card whose merge commit is an ancestor of it goes to Done. A failed deploy, smoke test or watch redeploys the tag; the orchestrator sets DEPLOY_FREEZE=true and sends the card to Fixing (source: prod) with a fresh fix budget. The freeze clears when that fix is live.
  5. Reporting. The workflow POSTs {card, env, status, sha, log} to /webhook/deploy with the x-pipeline-secret header. Each run is named Deploy production <sha>, so the orchestrator also polls for the result; a cancelled or superseded run sends the card back to Awaiting approval.

Schema migrations run inside the deploy (the Migrator Lambda) and are expand-only, so a rolled-back release still runs on the newer schema.

Configuration and operations

Settings live in packages/agent-pipeline/.env (gitignored; .env.example lists them) and are validated with zod on start: REPO_DIR, WORKTREE_ROOT, DATA_DIR, TRELLO_KEY, TRELLO_TOKEN, TRELLO_BOARD_ID, PIPELINE_SECRET, GITHUB_REPO, TEST_DATABASE_URL, MAX_PARALLEL, MAX_FIX_ATTEMPTS, DAILY_BUDGET_USD, PIPELINE_PAUSED, DRY_RUN, PORT.

ToDo
Startpnpm --filter @app/agent-pipeline start (listens on 127.0.0.1:8787; polls Trello every 2 minutes)
Ship a cardDrag it from Awaiting approval to Approved
Let risky work buildAdd the approved label
UnblockAnswer its questions in a comment, move it back to To-Do
Pause / go onAdd a card named STOP to To-Do / remove it
Resume after the breakerpnpm --filter @app/agent-pipeline resume
Deploy by handgh workflow run deploy.yml --ref main -f stage=production -f sha=<commit on main>
End a freeze by handgh variable set DEPLOY_FREEZE --body false
Read a card's history$DATA_DIR/progress/IEL-<n>.md, and its [pipeline] comments

One-time setup, done for this repo:

Status and open items

The pipeline has shipped one real card to production; the rollback path has only run in tests.

CheckResult
Unit tests (pnpm --filter @app/agent-pipeline test, also in CI)45 pass: transitions, lock expiry, overlap, file contract, merge queue against a scripted gh, webhooks
Dry run, one cardIEL-4 To-Do to Done in 5 minutes, $1.17, correct progress file
Dry run, three cardsTwo built in parallel on separate branches; the third waited on a shared file; $2.79
Live PR trialPR #25 opened, CI read as green and up to date, closed unmerged
Real runIEL-4: PR #27 merged as 7c56a27 for $0.56; deploy passed checks, smoke test and the 10-minute watch; live at 20:03 on 31 Oct 2026

Known limits: an infra error (git, Trello, gh) leaves a card in place and is retried every tick with no cap; a timed-out agent run reports no cost, so the daily budget misses it (the per-run cap still applies); two orchestrators started at the same instant over a stale pidfile could both run.