Card Pipeline: Technical One-Pager
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.
- Code:
packages/agent-pipeline(Node 22, ESM, zod, SQLite throughnode:sqlite); role prompts inprompts/<role>.md, read on every run. - Where it runs: the owner's laptop. GitHub Actions runs CI and deploys; Trello is the only interface.
- First real card: IEL-4 went from To-Do to merged PR #27 in 10 minutes for $0.56 of agent work, and was live in production at 20:03 on 31 Oct 2026.
Architecture
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
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.
| Role | Works in | Tools | Turns / timeout / spend cap | Returns |
|---|---|---|---|---|
| Planner | Detached worktree at origin/main | Read, Glob, Grep, git log | 30 / 15 min / $2 | Plan JSON, zod-validated, one corrected retry |
| Implementer | Card branch from origin/main | Read, Edit, Write, the repo's test, lint, typecheck and drizzle commands, read-only git | 40 / 30 min / $6 | Free text; NEEDS_REPLAN: line to stop |
| Verifier | Card branch, detached | Read, Glob, Grep, read-only git, the repo's checks | 30 / 20 min / $3 | Review JSON: criteria with evidence, issues |
| Fixer | Card branch (or fix/...-prod-regression off main) | As implementer | 25 / 20 min / $4 | FIXED n / NOT_FIXED / NEEDS_HUMAN / NEEDS_REPLAN lines |
- Isolation: one worktree per run, removed in
finally; each card's tests use their own migrated database,test_iel_<n>, and CI's dummy settings. The agent process gets onlyPATH,HOME,LANGand those test values, never the pipeline's tokens. - File contract: the plan lists every file it may touch (a trailing
/covers generated output such aspackages/db/drizzle/). The runner stages the diff and sends anything outside the list back for a re-plan. - Deterministic gates first: the verifier re-runs the plan's test commands (only
pnpm test|lint|typecheckshapes are accepted),pnpm lintand the full typecheck, rejects added.skip/.only/.todoand deleted test files, then reviews. The verdict is computed in code: every criterion met with evidence and no blocking issue. - Card text is data: title, description, checklists and comments are fenced in a randomly named tag and labelled
humanorpipeline. In testing, a planted "ignore all previous rules" comment was left out and flagged to the owner. - Only the runner commits and pushes, with the plan's Conventional Commit subject,
feat(IEL-n): <title>.
Safety and concurrency
Every guard below is enforced in the orchestrator, not asked of an agent.
| Guard | How |
|---|---|
| One orchestrator | Exclusive-create pidfile in DATA_DIR; a stale one from a dead process is replaced. |
| One step per card | SQLite lock taken in one INSERT … ON CONFLICT … WHERE expired statement, 60-minute TTL; all locks cleared at startup. |
| Parallel cap | MAX_PARALLEL (2) cards in Planning, Building, Verifying or Fixing. |
| Shared files | A 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 work | A plan with risk: high or migrations: true waits for an approved label before Building. |
| Fix budget and loop guard | At most MAX_FIX_ATTEMPTS (2) fixes; the same blocking file or unmet criterion two rounds running blocks at once. |
| Circuit breaker | Three cards failed in a row pause all new work until pnpm --filter @app/agent-pipeline resume. |
| Daily budget | New agent work stops when today's summed total_cost_usd reaches DAILY_BUDGET_USD (10). |
| Kill switch | PIPELINE_PAUSED=1, or a card named STOP in To-Do or Bug. Steps in flight finish; nothing new starts. |
| Dry run | DRY_RUN=1 (the default) runs agents but only logs pushes, PRs, merges, deploys and Trello writes. |
| Restart | Card 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.
- 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, rungh pr update-branchand 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 latestmain. - 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 onmainand any run whileDEPLOY_FREEZEis on, unlessallow_frozen=true(the fix that ends a freeze). - Deploy. Typecheck, lint and the db tests on that commit, OIDC into AWS,
sst deployscoped bydeploy-scope.shagainst theproduction-last-goodtag,pnpm test:smoke(API health, a public Postgres-backed route, the platform shell), then 20 health checks over 10 minutes. - Outcome. Success moves
production-last-goodto 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 setsDEPLOY_FREEZE=trueand sends the card to Fixing (source: prod) with a fresh fix budget. The freeze clears when that fix is live. - Reporting. The workflow POSTs
{card, env, status, sha, log}to/webhook/deploywith thex-pipeline-secretheader. Each run is namedDeploy 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.
| To | Do |
|---|---|
| Start | pnpm --filter @app/agent-pipeline start (listens on 127.0.0.1:8787; polls Trello every 2 minutes) |
| Ship a card | Drag it from Awaiting approval to Approved |
| Let risky work build | Add the approved label |
| Unblock | Answer its questions in a comment, move it back to To-Do |
| Pause / go on | Add a card named STOP to To-Do / remove it |
| Resume after the breaker | pnpm --filter @app/agent-pipeline resume |
| Deploy by hand | gh workflow run deploy.yml --ref main -f stage=production -f sha=<commit on main> |
| End a freeze by hand | gh 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:
- Trello: lists Planning, Building, Verifying, Fixing, Deploying, Awaiting approval, Approved, Done, Blocked beside To-Do and Bug; an API key and token; a label named
approved(still to add). - GitHub: repo variable
DEPLOY_FREEZE=false; on theproductionenvironment,SMOKE_ROOT,PIPELINE_SECRET,ORCHESTRATOR_WEBHOOKbeside the existing OIDC roleAWS_DEPLOY_ROLE_ARN; "always suggest updating PR branches" and "delete head branches" on; theproduction-last-goodtag seeded. - Host: Node 22, git,
ghlogged in, Claude Code logged in, Docker for the test Postgres, and an ngrok static domain forwarding to port 8787 for the deploy webhook. Trello events arrive by polling; the Pipedream forward was skipped.
Status and open items
The pipeline has shipped one real card to production; the rollback path has only run in tests.
| Check | Result |
|---|---|
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 card | IEL-4 To-Do to Done in 5 minutes, $1.17, correct progress file |
| Dry run, three cards | Two built in parallel on separate branches; the third waited on a shared file; $2.79 |
| Live PR trial | PR #25 opened, CI read as green and up to date, closed unmerged |
| Real run | IEL-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 |
- Exercise rollback and the freeze against a real failed deploy (only unit-tested).
- Add the
approvedlabel on the board. - Run the orchestrator and ngrok as services that start with the laptop, and write the runbook.
- Retire the older interactive card agents and the
run-cardskill, and update the Trello lists line inCLAUDE.md.
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.