Skip to content

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 gh calls and some kill -0 PID 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 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.

  1. Global gates (once). Read the registry; check the GitHub GraphQL budget is above the floor (default 1000 — a cheap gh api rate_limit that costs nothing); count live workers across all projects against the globalWorkerCap (default 4). A low budget skips the whole sweep; the sweep never crashes on one bad project.

  2. 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_merge runs before reconcile, 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 by reconcile in the same sweep and routed onward (to deploy-watch, or straight to Done) — no wasted interval.
  • Only a real spawn consumes capacity. dispatch_one.sh emits a spawned_worker status 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.

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.json
  • skills/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.

Echo has no database. Its state lives in three human-legible places.

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 progressAI reviewing as it plans, implements, and opens its PR.
  • gated_merge.sh merges the reviewed PR; reconcile.sh routes 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.

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.

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.

  • 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.json surface and how the three layers merge.