The run lifecycle
Echo turns one board column into a pipeline. A /echo:tick sweep
(tick.sh) walks every enabled project, and each item
you drop into Ready moves through a fixed sequence of board statuses driven
by deterministic scripts and one detached worker. The status names below are
the statuses map in config — the defaults are shown, but
every name is configurable.
The state flow
Section titled “The state flow”stateDiagram-v2
state "Ready" as R
state "Planning" as P
state "In progress" as IP
state "AI reviewing" as AR
state "Building & Deploying" as BD
state "Done" as D
state "Needs Human" as NH
[*] --> R
R --> P: dispatch_one — prevalidate proceeds, flip
P --> IP: worker Step 4a — plan written
IP --> AR: worker Step 7a — PR opened
AR --> AR: worker adds echo:merge-ready (Step 9)
AR --> BD: gated_merge merges, reconcile (verify.type != none)
AR --> D: reconcile (verify.type = none)
BD --> D: deploy-watch healthy
D --> [*]
R --> NH: prevalidate needs_human
P --> NH: brainstorm bail (Step 2)
IP --> NH: unrecoverable failure
AR --> NH: gated_merge holds
BD --> NH: deploy-watch failed / stood down
NH --> R: Discord reply routes guidance
Ready → Planning — dispatch
Section titled “Ready → Planning — dispatch”Every sweep, tick.sh runs the capacity-independent steps first
(gated_merge.sh, then reconcile.sh), then checks the
per-project fast paths: is the global worker cap reached, are this project’s
workers.slots (default 2) full, is the Ready column empty? If there’s a free
slot and Ready work, it hands off to dispatch_one.sh.
dispatch_one.sh is the per-project pipeline:
-
Acquire the dispatcher lock (
lock.sh). A second concurrent firing bails aslock_held— dispatch is single-flight per project. -
Select the top item (
select_next_item.sh) — the highest-priority Ready item. -
Pre-validate it (
prevalidate_item.sh). A small model classifies the itemproceed/needs_human/defer. Aneeds_humanverdict flips the item straight to Needs Human; adeferre-queues it to the bottom of Ready. Onlyproceedcontinues. -
Flip Ready → Planning via a targeted GraphQL mutation. The pipeline deliberately flips to Planning, not straight to In progress — the worker owns the In-progress flip once it has actually written a plan, so the Planning window stays observable. A
gh authpreflight (run withGH_TOKENscrubbed, matching the worker’s environment) routes to Needs Human instead of spawning a worker that can’t talk to GitHub. -
Spawn the worker (
spawn_worker.sh) detached, then post a “worker spawned” issue comment and release the lock. If the spawn fails, the item is flipped back to Ready so the next tick re-selects it.
dispatch_one.sh prints exactly one JSON status line —
spawned_worker · no_eligible_items · lock_held · deferred ·
needs_human · error — which tick.sh folds into its per-project summary.
Planning → In progress → AI reviewing — the worker
Section titled “Planning → In progress → AI reviewing — the worker”The spawned worker is a headless claude -p session driven by the invariant
worker prompt prefix.
It runs a fixed protocol and flips its own status as it goes:
- Step 2 — brainstorm. It aligns on what “done” means before writing code.
Brainstorming runs on every lane; only review ceremony scales with size
and risk (see Models & lanes). If the spec is
underspecified, needs hardware / secrets / destructive infra, or is blocked on
an open dependency, it escalates via
flip_to_needs_human.shand exits. - Step 4a — Planning → In progress. Once the plan is written, the worker flips the status itself so observers see the phase change.
- Steps 5–7 — execute, open the PR. The PR body carries
Refs #N(neverCloses— the issue stays open through the deploy window). - Step 7a — In progress → AI reviewing. Right after the PR opens.
- Step 7b — CI gate. A red PR is not reviewed.
- Step 8 — adversarial review. A multi-lens, refute-verified review whose findings are advisory — posted and filed, never a merge decision.
- Step 9 — label
echo:merge-ready. This is the worker’s genuine last mutation of the PR, added only after any lesson /CLAUDE.mdcommits have been pushed. It then chain-triggers the next dispatcher run and exits with the PR open in AI reviewing.
AI reviewing → merge → Done — the dispatcher closes the loop
Section titled “AI reviewing → merge → Done — the dispatcher closes the loop”Two capacity-independent steps run at the top of every sweep, in this order:
-
gated_merge.shselects AI-reviewing items whose open worker PR carriesecho:merge-ready, re-runs the deterministic gates, and either squash-merges the PR (--delete-branch) or holds it for a human. It runs before reconcile so a PR it merges this sweep is seen as merged in the same sweep. -
reconcile.shis the universal post-merge trigger. For every in-flight item (Needs Human or AI reviewing) whose worker branch (agent/issue-<N>) has a merged PR, it routes byverify.type:verify.type: none(the default) → flip straight to Done, comment, write the vault run note, and archive.- any other
verify.type→ flip to Building & Deploying and spawn a detached deploy-watch (spawn_watcher.sh), but only if the watcher actually launched (proved by a livewatcher.<N>.pid).
Detection is a branch match, not GitHub’s close-linkage — so it still finds a
merged PR under the Refs #N convention, independent of any Closes keyword.
Building & Deploying → Done — deploy-watch
Section titled “Building & Deploying → Done — deploy-watch”When verify.type is not none, a detached read-only watcher (a claude -p
session driven by the deploy-watch skill, capped at verify.timeout, default
1200s, on verify.watchModel, default sonnet) confirms the merge reached
production. It calls finalize_watcher.sh with a verdict:
--target done→ flip to Done and close the issue (read-back confirmed — never a false Done).--target needs-human→ flip to Needs Human, comment, labelecho:deploy-checked(so reconcile doesn’t re-trigger on the still-merged branch PR), and queue a Discord escalation.
spawn_watcher.sh refuses to launch if verify.command isn’t read-only (a
mutating-verb regex is the guardrail) and escalates instead. If the watcher dies
without finalizing, watcher-run.sh’s death backstop forces a guard-checked
terminal flip.
Escalations to Needs Human
Section titled “Escalations to Needs Human”Needs Human is the single human-in-the-loop column. An item lands there from several points:
| From | Trigger |
|---|---|
| Ready | pre-validate verdict needs_human, or defer-cap reached, or gh unauthenticated |
| Planning / In progress | worker brainstorm bail, unfixable test/build, token budget hit, 3 review rounds without convergence |
| AI reviewing | gated_merge.sh held the PR on a gate (see Merge gates) |
| Building & Deploying | deploy-watch failed, or refused a non-read-only verify.command |
Every escalation routes through flip_to_needs_human.sh, which flips the status,
comments the reason on the issue, and queues an outbox file for
Discord. An operator reply re-queues the item to Ready
with the reply text as guidance — the one path out of Needs Human.
See also
Section titled “See also”- Merge gates — the deterministic gates
gated_merge.shenforces. - Discord ops — escalations out and approvals back in.
- Models & lanes — how pre-validate and size/risk routing pick the worker’s model and review ceremony.
- Command reference · Config reference.