* fix(cdc): emit pr_review_thread_resolved on replace polls (#152 bug 5) writePRRows was DELETE-then-UPSERT on the Replace path, so every poll's upserts hit the INSERT branch and the AFTER UPDATE trigger that emits pr_review_thread_resolved never fired in production. Replaces the blanket delete with a set-diff: upsert observed threads first (so unchanged thread_ids go through ON CONFLICT DO UPDATE and fire the UPDATE trigger when resolved flips), then delete orphans whose thread_id is not in the observed set, all inside the existing tx. Adds DeletePRReviewThread query (sqlc-generated form hand-edited; no sqlc binary available locally — sqlc generate from backend/ produces an identical file). Tests: TestPRReviewThreadsCDC_EmitsResolvedOnReplacePoll (regression — fails without fix) and TestPRReviewThreadsReplace_PrunesOrphansWithoutReinserting. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(observe): emit scm-disabled log on startup with no subjects (#152 bug 7) checkCredentials lived only inside Poll, which short-circuits when discoverSubjects is empty. On a fresh daemon with no tracked PRs the documented "scm observer disabled: provider credentials unavailable" warn was unreachable, leaving users with no signal that the SCM observer was a no-op. Calls checkCredentials once in Observer.loop before the first Poll. The existing credentialsChecked guard preserves once-per-process semantics; provider construction still uses SkipTokenPreflight so daemon readiness doesn't block on gh. Test: TestStart_LogsDisabledWarningWhenNoTokenAndNoSubjects with a race-safe syncBuffer for capturing slog from the observer goroutine. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(api,spawn): typed errors + project/branch/binary preflight (#152 bugs 1-4,6) Closes the long tail of opaque-500-and-orphan-row failures that discussion #149's smoke walk surfaced. The common shape: spawn created the session row before validating preconditions, and the underlying errors weren't typed, so toAPIError defaulted to INTERNAL_ERROR. Bug 1 (orphan row + opaque 500 on unknown projectId): Service.Spawn / SpawnOrchestrator now call store.GetProject first and return apierr.NotFound("PROJECT_NOT_FOUND", ...) before manager.Spawn, eliminating the create-row-then-fail-workspace ordering. Bug 2 (Restore opaque 500 on half-spawned/terminated session): Manager.Restore gained the ErrIncompleteHandle guard that Kill has at manager.go:189-193. toAPIError now maps both restore and kill to the same SESSION_INCOMPLETE_HANDLE 409 envelope. Bug 3 (--branch unfetched / checked-out-elsewhere → opaque 500): gitworktree pre-checks listRecords for branch-in-other-worktree, falls back to refs/tags on missing local/remote head, and emits two new port sentinels (ErrWorkspaceBranchCheckedOutElsewhere, ErrWorkspaceBranchNotFetched) mapped to BRANCH_CHECKED_OUT_ELSEWHERE (409) and BRANCH_NOT_FETCHED (400). Bug 4 (orphan terminated row on claim-pr rollback): Adds Store.DeleteSession gated to seed-state rows only (preserves the no-resurrection guarantee for live sessions), transactional change_log cleanup, Manager.RollbackSpawn (delete-then-fallback-to-kill), a new POST /sessions/{id}/rollback endpoint, and rewires cli/spawn.rollbackSpawnedSession to use it. The exit-0 sub-symptom was unreproducible from current source and is left unaddressed. Bug 6 (agent binary not on PATH → silent idle session): Drops the "return name, nil" anti-pattern from all 21 agent adapters and returns the new ports.ErrAgentBinaryNotFound on exec.LookPath miss. Manager.Spawn gained a validateAgentBinary pre-flight (with injectable LookPath so tests don't need real binaries on PATH) that aborts before runtime.Create. Mapped to AGENT_BINARY_NOT_FOUND (400). Integration tests in internal/integration/ stub LookPath to /usr/bin/true. Tests cover each bug end-to-end. OpenAPI regenerated for /rollback. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * chore: gofmt + regen frontend schema.ts for /rollback CI fixes for #153: - gofmt/goimports on kilocode and kiro adapters that the bug 6 audit left mis-grouped. - openapi-typescript regen against the new /rollback endpoint added in the Lane A commit. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> * fix(store): guard change_log delete behind seed probe + regen sqlc (#152, PR #153 review) Addresses @greptile-apps P1 and P2 review feedback on PR #153. P1 (CDC events deleted for live sessions in rollback fallback): DeleteChangeLogForSession ran unconditionally inside the transaction before DeleteSeedSession's seed-state predicates filtered the session delete to a no-op. For a live session reaching DeleteSession (the delete-then-kill fallback path inside RollbackSpawn), the seed delete returned 0 rows but the session_created/session_updated CDC events had already been purged. Now probes via a new SessionIsSeed query first and short-circuits the whole tx — including the change_log cleanup — when the row is not in seed state. P2 (regen sqlc): installed sqlc 1.31.1 and ran `sqlc generate` from backend/, replacing the hand-edited pr_review_threads.sql.go (and producing minor format-only churn in models.go, pr.sql.go, sessions.sql.go, changelog.sql.go). The regen surfaced two issues: 1. GetPR / ListPRsBySession had their return types hand-changed to gen.PR by the previous PR; sqlc actually emits GetPRRow / ListPRsBySessionRow when queries enumerate columns. Fixed by collapsing those two queries to `SELECT * FROM pr` so sqlc returns gen.PR (which is what the store's prRowFromGen converter expects), and pr.last_nudge_signature now lands in the result alongside the existing 37 columns. 2. sqlc 1.31.1's SQLite parser silently strips trailing `?` placeholders and string literals from DELETE statements (reproduced with sqlc.arg, IFNULL, rowid subquery, and second-predicate workarounds — all eaten). DeleteSeedSession and DeleteChangeLogForSession both tripped it. They are now run as plain tx.ExecContext calls inside Store.DeleteSession, inside the same write transaction as SessionIsSeed; both queries are removed from the queries/ directory and the workaround context is documented inline in queries/sessions.sql and queries/changelog.sql to keep future contributors from re-adding them. Verified: go build ./... clean, go test -race ./... 1097/1097 pass. |
||
|---|---|---|
| .github/workflows | ||
| backend | ||
| docs | ||
| frontend | ||
| test/cli | ||
| .envrc | ||
| .gitignore | ||
| AGENTS.md | ||
| CLAUDE.md | ||
| README.md | ||
| flake.lock | ||
| flake.nix | ||
| package-lock.json | ||
| package.json | ||
README.md
ReverbCode
A Go-backed agent orchestration daemon for supervising parallel coding-agent
sessions, with an ao CLI today and an Electron supervisor planned. The Go
module and packages remain agent-orchestrator; "ReverbCode" is the public name.
See docs/architecture.md for the backend mental model
and AGENTS.md for the contributor / worker contract.
What's shipped today
- Loopback-only HTTP daemon (
backend/internal/httpd) controlling projects,
sessions, orchestrators, and hook callbacks over127.0.0.1. aoCobra CLI (backend/cmd/ao) — a thin client over the daemon for daemon
control, project/session/orchestrator management, and worker spawning.- Worker/orchestrator spawn into isolated
git worktreeworkspaces, launched
inside azellijruntime adapter. - Live per-PR observation via the provider-neutral SCM observer
(backend/internal/observe/scm/): polling loop, ETag guards, semantic
diffing, CI/check/review-thread tracking, and lifecycle nudges for CI
failures, review feedback, and merge conflicts. - Agent adapters under
backend/internal/adapters/agent/:claude-code,
codex,opencode,grok,cursor,qwen,copilot,kimi, plus
shared activity-dispatch / hook utilities. - SQLite store (
backend/internal/storage/sqlite/) with sqlc-generated
queries, DB-triggered change-data-capture intochange_log, and a CDC
poller/broadcaster (backend/internal/cdc/) feeding in-process subscribers
and an SSE replay endpoint. - Session lifecycle manager + reaper (
backend/internal/lifecycle/,
backend/internal/observe/reaper/): runtime/activity/PR facts reduced into
the small durable session state, display status derived at read time.
Quick start
Requirements: Go 1.25+, zellij on PATH for the
runtime adapter, and gh (or GITHUB_TOKEN) if you want the SCM observer to
authenticate against GitHub. The SQLite driver is the pure-Go
modernc.org/sqlite — no system SQLite library is required.
cd backend
go build -o /tmp/ao ./cmd/ao
# Start the daemon and wait for /readyz.
/tmp/ao start
# Register a local git repo as a project. The id defaults to the lowercased
# base of --path; pass --id explicitly when the directory name doesn't match.
/tmp/ao project add --path /path/to/your/repo --id your-repo --name your-repo
# Spawn a worker session running the default agent.
/tmp/ao spawn --project your-repo --prompt "Refactor the auth module"
# Inspect what's running.
/tmp/ao status
/tmp/ao session ls
CLI surface
The CLI is intentionally thin: every product command resolves to a daemon HTTP
route. Run ao <command> --help for the authoritative flag shape; the table
below groups what's on main today.
| Lane | Command | Purpose |
|---|---|---|
| Daemon | ao start |
Start the daemon in the background and wait for /readyz. |
| Daemon | ao stop |
Graceful shutdown via loopback POST /shutdown. |
| Daemon | ao status |
Report PID/port/health/readiness from running.json. |
| Daemon | ao daemon |
Hidden internal entrypoint used by ao start. |
| Project | ao project add |
Register a local git repo as a project. |
| Project | ao project ls |
List registered projects. |
| Project | ao project get <id> |
Fetch one project. |
| Project | ao project rm <id> |
Remove a project. |
| Session | ao spawn |
Spawn a worker session in a registered project. |
| Session | ao session ls |
List sessions (filter by project, include terminated). |
| Session | ao session get <id> |
Fetch one session. |
| Session | ao session kill <id> |
Terminate a session. |
| Session | ao session rename <id> <name> |
Rename a session. |
| Session | ao session restore <id> |
Relaunch a terminated session. |
| Session | ao session cleanup |
Reclaim eligible workspaces for terminated sessions. |
| Session | ao session claim-pr <session> <pr> |
Attach an existing PR to a session. |
| Orchestrator | ao orchestrator ls |
List orchestrator sessions. |
| Messaging | ao send |
Send a message to a running agent session. |
| Utility | ao doctor |
Local health checks (config, data dir, DB, git, zellij). |
| Utility | ao completion <shell> |
Generate bash/zsh/fish/powershell completions. |
| Utility | ao version |
Print build metadata. |
| Internal | ao hooks <agent> <event> |
Hidden adapter hook callback. |
See docs/cli/ for the daemon-control intent and command shape.
Configuration
All configuration is env-driven; the daemon takes no config file. The bind
host is hard-coded to 127.0.0.1 — the daemon has no auth, CORS, or TLS, and
exposing it beyond loopback would be a security regression.
| Var | Default | Purpose |
|---|---|---|
AO_PORT |
3001 |
Bind port; daemon fails fast if taken. |
AO_REQUEST_TIMEOUT |
60s |
Per-request timeout (Go duration). |
AO_SHUTDOWN_TIMEOUT |
10s |
Graceful-shutdown hard cap. |
AO_RUN_FILE |
<UserConfigDir>/agent-orchestrator/running.json |
PID + port handshake path. |
AO_DATA_DIR |
<UserConfigDir>/agent-orchestrator/data |
SQLite DB, WAL files, managed state. |
AO_AGENT |
claude-code |
Default agent adapter id used by ao spawn. |
AO_SESSION_ID |
(unset) | Set inside spawned sessions; read by ao send and ao hooks. |
GITHUB_TOKEN |
(unset) | Used by the GitHub SCM and tracker adapters. Falls back to gh auth token. |
Health check:
curl localhost:3001/healthz
curl localhost:3001/readyz
Architecture
The daemon is a long-running supervisor. Adapters observe external facts (PR
state, agent activity, runtime liveness); the lifecycle manager reduces those
into a small set of durable session facts (activity_state, is_terminated,
PR rows). Display status is derived from those facts at read time — it is
never stored. SQLite triggers append every user-visible change to change_log,
and the CDC poller broadcasts those events to in-process subscribers and an
SSE stream.
Full mental model and load-bearing rules: docs/architecture.md.
Package-by-package ownership: docs/backend-code-structure.md.
Testing
The local gate is the backend Go build and race-enabled test suite:
cd backend && go build ./... && go test -race ./...
GitHub Actions is the authoritative pre-merge gate; mirror its commands here
when in doubt. See AGENTS.md for the regen workflow when
touching the daemon API surface (npm run sqlc, npm run api).
Status and roadmap
Current main ships the CLI surface above, the SCM observer end-to-end
(issues #75,
#108,
#109), the agent
adapter platform, and the CDC pipeline with SSE replay. The Tracker observer
(#112) and live
pr_* event consumers (#110)
are in flight. The Electron supervisor under frontend/ is still a placeholder
shell — daemon logic stays in the Go backend.
Tracking milestone:
rewrite on GitHub.
Contributing
Repo layout and the worker contract live in AGENTS.md. Keep
changes surgical, follow the package boundaries documented in
docs/backend-code-structure.md, and prefer
adding daemon HTTP routes over leaking storage / runtime into the CLI.