* 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> |
||
|---|---|---|
| .github/workflows | ||
| backend | ||
| docs | ||
| frontend | ||
| test/cli | ||
| .envrc | ||
| .gitignore | ||
| AGENTS.md | ||
| CLAUDE.md | ||
| README.md | ||
| flake.lock | ||
| flake.nix | ||
| package.json | ||
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 ./...