Architecture
Echo takes a GitHub Project board and works it autonomously: items move from a column into a detached headless worker that implements them, opens a PR, reviews it, and — through a trusted merge step — lands and verifies the change. The shape of the system follows from one principle.
Deterministic scripts, LLM only where judgment is needed
Section titled “Deterministic scripts, LLM only where judgment is needed”Echo is mostly bash. The sweep that decides what happens next — which
project to look at, whether there’s free capacity, whether a PR is safe to merge,
which item to pick — is deterministic shell that you can read, test, and predict.
The LLM is spent only where the work genuinely needs judgment: a worker (an
independent claude process) does the brainstorming, planning, implementation,
and adversarial review of a single backlog item.
This split is deliberate, and it is the through-line for the rest of Echo’s design:
- Cheap and predictable where it can be. Sweeping ten idle projects costs a
few
ghcalls and somekill -0PID checks — no tokens. Model spend happens only when there is real work to implement. - Trustworthy where it must be. Security-relevant decisions (may this PR merge? does this change touch auth?) are made by deterministic scripts a worker cannot talk its way past — never by an LLM judging its own output. The trust model is built entirely on this line.
- Auditable. Every decision boundary is a shell script with a header comment and a test, not a paragraph of prompt an autonomous agent may or may not honor.
The rule of thumb: if a step can be a script, it is a script. The worker is reserved for the one thing scripts can’t do — write and review the code.
The tick sweep
Section titled “The tick sweep”The hub runs one command on an interval — /echo:tick
(tick.sh) — and it sweeps every enabled registered
project. A worker’s exit chain-fires the next tick, so completions wake
scheduling immediately instead of waiting for the interval.
Each sweep runs global gates once, then a per-project fast-path loop.
-
Global gates (once). Read the registry; check the GitHub GraphQL budget is above the floor (default
1000— a cheapgh api rate_limitthat costs nothing); count live workers across all projects against theglobalWorkerCap(default4). A low budget skips the whole sweep; the sweep never crashes on one bad project. -
Per project, in order:
gated_merge.sh # trusted merge of review-done PRs (capacity-independent)reconcile.sh # route merged / held items onward (capacity-independent)── global worker cap reached? → skip the rest for this project ──fast-path 1: worker slots full? (filesystem PID checks only)fast-path 2: Ready column empty? (one cheap board read)dispatch_one.sh # prevalidate + spawn one detached worker
Two properties are worth calling out, both grounded in tick.sh:
gated_mergeruns beforereconcile, and both run before the capacity gate. They spawn no workers, so they must not be blocked by the worker cap. Running the merge step first means a PR merged this sweep is seen as merged byreconcilein the same sweep and routed onward (to deploy-watch, or straight to Done) — no wasted interval.- Only a real spawn consumes capacity.
dispatch_one.shemits aspawned_workerstatus line only when it actually launched a worker; deferred, needs-human, no-eligible-item, and lock-held outcomes leave the global cap untouched, so a non-spawn can’t phantom-starve later projects in the sweep.
The whole loop is a state machine over the board — the lifecycle guide walks the full item journey with a diagram.
Config-driven, zero hardcoding
Section titled “Config-driven, zero hardcoding”Echo is a plugin installed once; it ships with no baked-in project values.
Everything project-specific — the owner/repo, the Project number, the status
column names, worker slot counts, merge policy, gated paths — comes from
configuration that is merged over the plugin’s defaults.json.
Resolution is a three-way deep merge, rightmost wins:
plugin defaults.json ← machine registry ← per-project config.jsonskills/echo-core/config/defaults.json— the plugin’s shipped defaults. The complete surface is the config reference.~/.config/echo/registry.json— the machine’s sweep list and globals (vault path,globalWorkerCap, model tiers).<repo>/.claude/echo/config.json— committed, per-project overrides only; it stores the keys you changed during/echo:setup, not a full copy.
A single resolver, echo-config, is the keystone: every other script obtains
every path, name, and value through echo-config --project <root> --print <key>.
That one choke point is what turns a would-be pile of hardcoded scripts into one
parameterized system, and it’s the primary unit-test surface. Crucially, config
stores paths to secret files (e.g. credsEnvPath), never secret values — so
open-sourcing the plugin is a docs task, not a scrub.
The state model
Section titled “The state model”Echo has no database. Its state lives in three human-legible places.
The board is the state machine
Section titled “The board is the state machine”The GitHub Projects v2 board is Echo’s state machine. Each item sits in exactly one status column, and the scripts advance it:
- Ready → dispatch picks it, prevalidates, flips it into Planning.
- The worker moves it through In progress → AI reviewing as it plans, implements, and opens its PR.
gated_merge.shmerges the reviewed PR;reconcile.shroutes it to Building & Deploying (when deploy verification is configured) and on to Done.- Any unrecoverable step flips it to Needs Human.
The status names are just the statuses map in config —
every name is configurable; the transitions are fixed. The
run lifecycle guide has the full diagram.
The notification outbox
Section titled “The notification outbox”Escalations don’t post to Discord inline from wherever they happen. A step that
needs a human writes a record into a per-project outbox under
.claude/echo/state/; the hub sweep drains that outbox and posts to the
project’s Discord channel, deduped against notification-state files so a
concurrent sweep can’t double-post. This keeps the one Discord gateway (held by
the hub) as the only thing that talks to Discord, and makes escalation a durable
file rather than a fire-and-forget call. See Discord ops.
The vault is the human record
Section titled “The vault is the human record”Everything Echo does is also written, write-side-only, to an Obsidian-native
vault of plain markdown (~/echo-vault/ by default): a note per project, per
run, per open escalation, and promoted lessons. Nothing breaks if Obsidian is
never opened — it is a browsable audit trail, not a dependency. The
vault guide covers what gets written.
See also
Section titled “See also”- The run lifecycle — the item journey through the board, with a state diagram.
- Trust model — the deterministic-vs-advisory boundary that this architecture makes possible.
- Config reference — the complete
defaults.jsonsurface and how the three layers merge.