Skip to content

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.

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

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:

  1. Acquire the dispatcher lock (lock.sh). A second concurrent firing bails as lock_held — dispatch is single-flight per project.

  2. Select the top item (select_next_item.sh) — the highest-priority Ready item.

  3. Pre-validate it (prevalidate_item.sh). A small model classifies the item proceed / needs_human / defer. A needs_human verdict flips the item straight to Needs Human; a defer re-queues it to the bottom of Ready. Only proceed continues.

  4. 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 auth preflight (run with GH_TOKEN scrubbed, matching the worker’s environment) routes to Needs Human instead of spawning a worker that can’t talk to GitHub.

  5. 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.sh and 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 (never Closes — 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.md commits 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:

  1. gated_merge.sh selects AI-reviewing items whose open worker PR carries echo: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.

  2. reconcile.sh is 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 by verify.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 live watcher.<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, label echo: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.

Needs Human is the single human-in-the-loop column. An item lands there from several points:

FromTrigger
Readypre-validate verdict needs_human, or defer-cap reached, or gh unauthenticated
Planning / In progressworker brainstorm bail, unfixable test/build, token budget hit, 3 review rounds without convergence
AI reviewinggated_merge.sh held the PR on a gate (see Merge gates)
Building & Deployingdeploy-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.