Agentic orchestrator for parallel coding agents — plans tasks, spawns agents, and autonomously handles CI fixes, merge conflicts, and code reviews.
Go to file
yyovil e25b2ad4de
feat(agent): opencode adapter + activity plugin hooks (#80)
* feat(agent): add opencode adapter + activity plugin hooks

Add an opencode (sst/opencode) agent adapter implementing the 6-method
ports.Agent interface and register it in the daemon's agent resolver, so a
session with harness "opencode" spawns and restores a real opencode worker.

opencode diverges from the claude-code/codex adapters in two ways the adapter
bridges:

- No native command-hook config. Unlike Claude Code (.claude/settings.local.json)
  and Codex (.codex/hooks.json), opencode has no "run this command on event"
  config (see sst/opencode#5409). Its only lifecycle surface is a JS/TS plugin
  loaded from .opencode/plugins/. GetAgentHooks therefore //go:embeds an
  AO-owned plugin (assets/ao-activity.ts) and writes it atomically; install is an
  idempotent overwrite and uninstall is a sentinel-guarded delete, so
  user-authored plugins are never touched. The plugin maps opencode events onto
  AO's three normalized activity events: session.created -> session-start,
  message.updated/message.part.updated -> user-prompt-submit, and
  session.status(idle) -> stop (NOT the deprecated session.idle, which is
  unreliable under `opencode run`). It shells `ao hooks opencode <event>` via a
  guarded sh -c so a missing `ao` binary is a silent no-op.

- A single approval flag. opencode exposes only --dangerously-skip-permissions
  (no graduated accept-edits/auto) and no system-prompt flag, so those map to a
  bypass-only permission flag and a documented no-op for the system prompt
  (deferred to opencode's own config).

Launch uses the interactive TUI (`opencode --prompt <p>`); restore continues a
captured native session via `opencode --session <id>`.

opencode_test.go mirrors codex_test.go (12 tests) and the daemon wiring test now
asserts the opencode harness resolves.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(agent): address Greptile review on opencode adapter (#80)

- Dispatch all opencode plugin hooks synchronously (Bun.spawnSync). The
  session.created handler previously fired session-start via an async
  Bun.spawn; if opencode does not await the event handler, a following
  message.updated -> user-prompt-submit (sync) could complete before the
  in-flight async session-start, so AO would see the prompt before the session
  was registered. A sync spawn blocks opencode's single-threaded event loop, so
  events are now reported strictly in dispatch order. Removes the now-unused
  async callHook helper.
- Fix package/doc comments that said .opencode/plugin/ (singular) to match the
  plural .opencode/plugins/ the adapter actually writes to.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(agent): surface opencode hook failures instead of swallowing them

The activity plugin previously discarded every failure: callHookSync ignored
the subprocess exit code/stderr and both catch blocks were empty, so a failing
`ao hooks` invocation or a malformed event payload was completely invisible.

Now failures are reported through opencode's structured logger (client.app.log)
while still never crashing opencode:
- callHookSync pipes stderr and checks result.success; a non-zero exit (a real
  `ao hooks` failure — the `command -v ao` guard makes a missing binary exit 0)
  is logged with its exit code and stderr.
- spawn exceptions (e.g. no `sh`) are caught and logged.
- the event-handler catch logs the offending event type instead of swallowing.
- logHookFailure is itself best-effort (optional-chained, rejection swallowed),
  so logging can never throw back into opencode.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(agent): address opencode adapter review — install guard, prompt dedup, hook timeout

Maintainer review on #80 surfaced three pre-merge issues:

- GetAgentHooks could clobber a user file: install overwrote
  .opencode/plugins/ao-activity.ts unconditionally while uninstall was
  sentinel-guarded. Install now refuses (loud error) to overwrite a file that
  isn't AO-managed; absent/AO-managed targets still write idempotently.

- Empty-prompt report poisoned the dedup: message.updated fired
  user-prompt-submit with an empty prompt AND marked the message seen, so the
  text from the following message.part.updated was deduped away and never
  reached AO — breaking title-from-prompt. reportUserPrompt now reports at most
  twice: an optional early empty report (keeps run-mode flows active) that does
  NOT block a later text report, and a text report that is terminal.

- Bun.spawnSync had no timeout, so a hung `ao hooks` could block opencode
  indefinitely. Each spawn is now time-boxed at 30s, matching the claude-code
  and codex hook timeouts.

Adds TestGetAgentHooksRefusesToClobberForeignFile and a spawn-timeout assertion.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: harden opencode activity hooks

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: harshitsinghbhandari <24b4506@iitb.ac.in>
2026-06-03 17:06:32 +05:30
.github/workflows chore(backend): add golangci-lint with a strong ruleset and clear the tree 2026-06-01 04:58:41 +05:30
backend feat(agent): opencode adapter + activity plugin hooks (#80) 2026-06-03 17:06:32 +05:30
docs Add agent adapters and wire per-session agents into the session manager (#65) 2026-06-02 16:51:32 +05:30
frontend chore: scaffold backend/ and frontend/ skeletons for rewrite 2026-05-26 15:22:36 +05:30
test/cli refactor: simplify session lifecycle and zellij runtime (#62) 2026-06-01 08:42:49 +05:30
.envrc Add agent adapters and wire per-session agents into the session manager (#65) 2026-06-02 16:51:32 +05:30
.gitignore Add agent adapters and wire per-session agents into the session manager (#65) 2026-06-02 16:51:32 +05:30
AGENTS.md docs: add AGENTS.md (#93) 2026-06-03 04:24:04 +05:30
CLAUDE.md docs: add AGENTS.md (#93) 2026-06-03 04:24:04 +05:30
README.md refactor: move session status assembly to service (#62) (#67) 2026-06-01 23:31:21 +05:30
flake.lock Add agent adapters and wire per-session agents into the session manager (#65) 2026-06-02 16:51:32 +05:30
flake.nix Add agent adapters and wire per-session agents into the session manager (#65) 2026-06-02 16:51:32 +05:30
package.json refactor: simplify session lifecycle and zellij runtime (#62) 2026-06-01 08:42:49 +05:30

README.md

agent-orchestrator

Rewrite of the agent-orchestrator: a long-running Go backend daemon (backend/)
paired with a placeholder Electron + TypeScript frontend shell (frontend/).

See docs/ for architecture and status — start with the
Lifecycle Manager + Session Service lane in docs/architecture.md.

Backend daemon

The Go backend now has a Cobra-based ao CLI in backend/cmd/ao.
The CLI controls the HTTP daemon — a loopback-only sidecar the Electron
supervisor will also use. The daemon skeleton includes the chi router,
middleware stack (recoverer → request-id → logger → real-ip), /healthz +
/readyz, atomic running.json PID/port handshake, graceful shutdown on
SIGINT/SIGTERM, SQLite storage, CDC polling, and lifecycle/reaper wiring.

Run

cd backend
go run ./cmd/ao start             # start the daemon and wait for readiness
go run ./cmd/ao status            # inspect PID/port/health/readiness
go run ./cmd/ao stop              # gracefully stop the daemon
go run ./cmd/ao daemon            # internal daemon entrypoint

go run .                          # compatibility wrapper; starts the daemon
AO_PORT=3019 go run ./cmd/ao start # override per invocation

Health check:

curl localhost:3001/healthz       # includes status/service/pid
curl localhost:3001/readyz        # includes status/service/pid

Configuration (env only)

The bind host is always 127.0.0.1: the daemon is a loopback-only sidecar
and binding any other interface would be a security regression, so the host
is intentionally not env-configurable.

Var Default Purpose
AO_PORT 3001 bind port; 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, and managed state

Test

npm run lint
# optional deeper backend pass:
cd backend && go test -race ./...