moeedbhatti.dev 31 Oct 2026 · Moeed Bhatti
Agent orchestration · IELTS platform

One Trello card.
Shipped to production.

I built a local service that carries each card on my Trello board from To-Do to production. Four headless agents plan, build, check and fix the work. The agents report, plain code decides, and I approve what ships.

To-Do→Planning→Building→Verifying⇄Fixing→Deploying→Awaiting approval→Approved→Done
10 minfirst real card, To-Do to merged PR
$0.56of agent work for that card, 25 turns
20:03live in production after one drag to Approved
45unit tests on every transition, lock and the merge queue
The one rule

Agents report. Plain code decides.

I didn't want an agent deciding when its own work is "done", merging it, or touching production. So no agent's word counts on its own. Each one returns structured data, and a small, pure function turns that data into the card's next list.

1

An agent reports

One headless claude -p run with one job. It returns JSON: a plan, a verdict with evidence, or a fix result.

2

Code decides

rules.js reads that report and computes the move. Fix budgets, loop guards, re-plans and blocks are all decided here, not by an agent.

3

A person approves

A merge ships nothing. Production deploys only when I drag the card to Approved, and then only that exact commit.

// rules.js: the verifier's verdict becomes the next list
if (role === "verifier") {
  if (s === "pass") return { state: "merging", … };
  return toFixer(failure, signature(out), note);
}

// toFixer: two fixes at most, and no third round on the same issue
if (card.fixAttempts >= maxFixAttempts) return block("fix attempts exhausted");
if (repeated.length) return block("same issue as last round");
return { state: "fixing", … };
"Pass" isn't the verifier's word either. Before any review, code reruns the plan's tests, lint and the full typecheck, and rejects any added .skip, .only or .todo and any deleted test file. A criterion without evidence (a file and line, or a test name) counts as unmet.
The lifecycle

The Trello lists are the states

A card moves one step at a time. It waits for a person only in three lists. Everything else, including the loops back to Fixing, runs without me.

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
agent or CI worksa person actslive in production
The architecture

One service moves everything

A Node 22 service on my laptop (packages/agent-pipeline, zod, SQLite through node:sqlite) polls the board every two minutes. Agents never touch git remotes, GitHub or Trello. The orchestrator does every push, merge, deploy dispatch and card move. The deploy workflow reports back by webhook.

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
the only part that decidesinfrastructure it drives
Least privilege, per role

Four agents, one job each

Each role is a separate headless Claude Code run. It gets only the tools it needs, no MCP servers, only the project's settings, and a cap on turns, time and spend. Anything outside its allowlist is refused.

read-only

Planner

Reads the card and the code. Returns a plan: the files it may touch, steps, testable acceptance criteria, test commands and a risk level. More than 8 files or 6 steps, and it splits the card. If the card is vague, it asks questions.

Tools
Read, Glob, Grep, git log
Caps
30 turns · 15 min · $2
Returns
Plan JSON, zod-validated
edits files

Implementer

Makes the change on the card's own branch, in its own worktree, against its own Postgres test database. If the change needs files outside the plan, it stops and asks for a re-plan.

Tools
Read, Edit, Write, the repo's test, lint and typecheck
Caps
40 turns · 30 min · $6
Returns
Free text, or NEEDS_REPLAN:
read-only

Verifier

Fresh context, so it doesn't share the implementer's assumptions. It checks the diff against every acceptance criterion and cites evidence for each one.

Tools
Read, Glob, Grep, read-only git, the repo's checks
Caps
30 turns · 20 min · $3
Returns
Criteria with evidence, issues
edits files

Fixer

Handles what the verifier, red CI or a failed production deploy found. Then a new verifier checks again. It's allowed to refuse a made-up issue it can't reproduce.

Tools
Same as the implementer
Caps
25 turns · 20 min · $4
Returns
FIXED / NOT_FIXED / NEEDS_HUMAN

Orchestrator

The only part that commits, pushes, opens and merges PRs, moves cards and dispatches deploys. Plain code, over SQLite.

File contract

The plan lists every file the change may touch. The runner stages the diff and sends anything outside that list back for a re-plan.

Card text is data

Titles, descriptions and comments are fenced in a randomly named tag. In testing, a planted "IGNORE ALL PREVIOUS RULES" comment was left out of the plan and flagged to me.

No secrets in reach

An agent process gets only PATH, HOME, LANG and the test settings, never the pipeline's tokens.

Try it

Run the board yourself

This simulation runs the pipeline's real decision rules, ported from rules.js line for line. The agents and GitHub are scripted stand-ins. Add a card that breaks production, drop a STOP card, or approve a deploy, and watch the rules move the cards.

The first real card · IEL-4

Count sentences in a Writing answer

The first real card was small on purpose: add a countSentences helper beside countWords. It was 2 files in packages/shared, +27 lines (a 6-line helper and 21 lines of tests).

  1. Planned

    Ready, risk low, 8 turns

    $0.16
  2. Built

    On its own branch, 12 turns

    $0.23
  3. Verified

    Pass with 2 nits, 5 turns; PR #27 opened

    $0.17
  4. Merged

    CI green, squash-merged as 7c56a27

  5. Approved

    I dragged it to Approved; deploy dispatched

  6. Live

    Checks, sst deploy, smoke test and the 10-minute watch passed

10 minTo-Do to merged. The 50 minutes before 19:50 were the card waiting for me, by design.
$0.56Agent work for the whole card, across 25 turns and three agents.
2 real nitsA decimal point counts as a sentence ending ("3.5 is it." gives 2), and … isn't treated as an ellipsis. That's a follow-up card, not a blocker.
Safety and concurrency

Every guard lives in the orchestrator

None of these are asked of an agent. They're enforced in code.

Isolation

One git worktree per run, removed afterwards, and one migrated test database per card (ielts_iel_<n>). Two cards build in parallel without touching each other.

Shared files wait

A planned card waits while another card that lists any of its files is building, verifying, fixing or merging. It then builds on top of that change.

Risky work waits

A plan with risk: high or migrations waits for the approved label before it builds: sign-in, payments, schema.

Fix budget

Two fixes at most. If the same file or criterion fails two rounds running, the card goes straight to Blocked instead of burning a third round.

Circuit breaker

Three failed cards in a row pause all new work until I run resume.

Daily budget

New agent work stops at a $10 API-equivalent cap each day, on top of each run's own spend cap.

Kill switch

PIPELINE_PAUSED=1, or a card named STOP in To-Do or Bug. Steps in flight finish; nothing new starts.

Locks

One orchestrator per machine (pidfile), one step per card (an SQLite lock with a 60-minute TTL), and at most 2 cards working at once.

Dry run by default

DRY_RUN=1 runs the agents but only logs pushes, PRs, merges, deploys and Trello writes.

Merge and deploy

Shipping without a merge queue

The repo is private on GitHub Free: no merge queue, no auto-merge, no required reviewers. The orchestrator stands in for the queue, and Trello's Approved list is the approval gate.

  1. Serial merge

    Open the PR, wait for green CI on a branch that's up to date with main, then squash-merge one card at a time.

  2. You approve

    A merge ships nothing. Dragging the card to Approved dispatches a deploy of that exact merge commit.

  3. Checks again

    Typecheck, lint and the database tests on that commit, then OIDC into AWS.

  4. Deploy + smoke

    sst deploy, then a smoke test: API health, a Postgres-backed route, the platform shell.

  5. 10-minute watch

    20 health checks over ten minutes. Survive it, and production-last-good moves to the commit.

If it fails

The deploy redeploys the production-last-good tag, DEPLOY_FREEZE stops anything else shipping, and the card goes back to Fixing with a fresh budget. The freeze clears when that fix is live. Schema migrations are expand-only, so a rolled-back release still runs on the newer schema.

What broke while building it

The tests and dry runs earned their keep

Every one of these was caught before a real card went through.

Found by: unit tests (they hung)

A busy loop

A waiting card relaunched itself on every tick. In production, that would have hammered the Trello API.

only re-tick when a step changed something.

Found by: unit tests

A circuit breaker that counted late

A failure was recorded only after an awaited save, so a fourth card could start before the third failure counted.

record the failure before the save.

Found by: the first dry run

A crash on first start

A child process that exited without reading its stdin threw an unhandled EPIPE and took the orchestrator down.

open stdin only when there's input; handle the stream error.

Found by: the three-card dry run

Overlap released too early

A card waiting on shared files started building as soon as the other card passed review, on a main that didn't have that change yet.

overlapping cards wait until the other is merged.

Found by: the runner trial

A verifier that couldn't verify

It couldn't run lint or typecheck, so it marked "the checks pass" as unmet.

allow both; the planner no longer writes "checks pass" as a criterion.

Found by: the three-card dry run

"Blocked: success"

My Claude login switched accounts mid-run, every agent call failed, and cards were blocked with the wrong field as the reason. The circuit breaker still paused everything.

report the run's error text, not its subtype.

What it costs

Runs on a laptop. No new bills.

Claude ProAgents run under my existing login; no per-token bill.
$10 / dayAPI-equivalent cap that stops a runaway card eating my usage.
~28 minof GitHub Free's 2,000 Actions minutes a month per card: CI twice, plus the deploy.
Free ngrokA static domain carries the deploy webhook back to the laptop.
PollingTrello is polled every 2 minutes instead of paying for a forwarder.

The $ figures are API-equivalent estimates that count against the plan's usage limits, not a bill.

Where a person steps in

Four moments. Everything else runs on its own.

To-DoWrite the card

Title, description, the acceptance you care about.

BlockedAnswer it

Reply to the planner's questions in a comment, move it back to To-Do.

PlanningLabel risky work

Add approved before sign-in, payments or migrations get built.

ApprovedDrag to ship

The only way anything reaches production.

Status

Proven so far, and still open

CheckResult
Unit tests45 pass, also in CI: transitions, lock expiry, overlap, the file contract, the merge queue against a scripted gh, webhooks
Runner trialPlanted instruction ignored and flagged; the verifier found a real bug; the fixer refused a made-up issue, then fixed the real one ($3.50)
Dry run, one cardTo-Do to Done in 5 minutes, nothing written outside the laptop ($1.17)
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; live in production at 20:03 on 31 Oct 2026
  • Exercise rollback and the freeze against a real failed deploy. So far they've only run in tests.
  • Run the orchestrator and the tunnel as services that start with the laptop, and write the runbook.
  • Retire the older interactive card agents.