Commit Graph

16 Commits

Author SHA1 Message Date
neversettle da30da5a45
feat(review): configurable AO code review backend (V1) (#192) (#197)
* feat(review): configurable AO code review backend (V1)

Add per-project configurable code review of a worker's PR. A reviewer
agent runs one-shot over the worker's own worktree and posts its result
to the PR; the worker picks the feedback up through the existing SCM
observer review-nudge path.

- domain: ProjectConfig.reviewers (+ default reviewer harness), Review /
  ReviewRun types and verdict/status vocab.
- storage: review + review_run tables (0011), sqlc queries, store methods.
- service/review: rewrite the in-memory stub as a persisted ReviewService
  (Trigger/Submit/List) with a reviewer Runner over agent resolver +
  runtime; ports.PRReviewPoster implemented on the GitHub adapter.
- http: session-scoped routes POST /sessions/{id}/reviews/trigger,
  POST .../submit, GET .../reviews; regenerated OpenAPI + TS types.
- cli: ao review trigger|submit|list.
- frontend: adapt ReviewDashboard to the per-worker reviews API.

Closes #192

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(review): address review — drop submit/poster/CLI, default reviewer to worker harness

Per PR #197 review feedback:
- Reviewer agent posts its review to the PR itself, so remove the
  ports.PRReviewPoster port, the GitHub review poster, the submit HTTP
  route + DTO, and the service Submit method (#1, #4, #7).
- Trigger spawns the reviewer agent over the worker's worktree with its
  own review prompt, mirroring the session launch flow (resolve agent by
  harness -> argv -> runtime.Create) (#8, #9).
- Default reviewer harness reuses the worker's harness when supported,
  falling back to claude-code; reviewer config stays independent of the
  worker override (#5, #6).
- Drop the `ao review` CLI for this PR's scope (#2, #3).

Regenerated OpenAPI + TS types.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(review): restore ao review submit (records verdict+body in AO)

Per maintainer request, bring back `ao review submit`. AO records the
reviewer's verdict and body on the review_run and marks the pass complete;
it does not post to GitHub — the reviewer agent posts its review to the PR
itself.

- storage: add review_run.body (0011), persist via Insert/UpdateReviewRunResult.
- service: restore Submit (no SCM poster) storing verdict + body.
- http: restore POST /sessions/{id}/reviews/submit + SubmitReviewInput.
- cli: ao review submit [worker] --verdict --body (worker from arg/--session/$AO_REVIEW_WORKER).
- runner: reviewer prompt instructs posting to GitHub and recording via ao review submit.

Regenerated OpenAPI + TS types.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(review): move reviewer runner to its own package; sharpen prompt

Per PR #197 review:
- Move the concrete reviewer runner out of the service layer into a new
  internal/review_runner package (package reviewrunner), beside other
  orchestration packages like session_manager. The service keeps only the
  Runner interface + RunSpec it depends on; the agent-resolver + runtime
  launch flow lives in review_runner.
- Sharpen the reviewer prompt: tell the agent to diff against the PR base,
  focus on high-confidence findings, post via `gh pr review`, and record
  the result with `ao review submit`; review-only (no commits/edits).
- Add unit tests for the runner.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(review): simplify review_run schema; provider-agnostic reviewer prompt

Per PR #197 review:
- review_run: status default 'running' (drop 'pending'), drop CHECK
  constraints on status/verdict, drop the updated_at column and the
  session/iteration index. Propagated through queries, domain, store,
  service, and tests.
- Reviewer prompt no longer hardcodes GitHub/gh commands — it instructs the
  agent to use whatever review tooling the provider offers, keeping the
  flow extensible across SCM providers.

Regenerated sqlc + OpenAPI/TS.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(review): launch reviewer before persisting the run

Trigger now spawns the reviewer agent first and then writes the review_run
with a status derived from the launch outcome (running on success, failed
if it never started), instead of inserting a running row and correcting it
to failed afterwards.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(review): pluggable reviewer registry distinct from worker harnesses

Reviewers are now their own pluggable adapter set, separate from the worker
agent registry — adding a reviewer (claude-code today, greptile tomorrow) is
a one-line registration that does not widen the worker harness vocabulary,
and a worker harness does not automatically become a valid reviewer.

- domain.ReviewerHarness: a distinct vocabulary (AllReviewerHarnesses) with
  its own IsKnown; ReviewerConfig/Review/ReviewRun use it. ResolveReviewerHarness
  reuses the worker harness only when it is itself a supported reviewer, else
  falls back to claude-code.
- ports.Reviewer: a reviewer-specific contract (ReviewCommand → argv + env)
  that models one-shot / non-prompt CLIs natively instead of forcing every
  reviewer through the worker's interactive GetLaunchCommand(Prompt:...).
- internal/adapters/reviewer: a separate registry + resolver (mirrors the
  worker agent registry) with the claude-code reviewer adapter, which owns the
  review prompt and reuses the worker claude-code launch construction.
- review_runner resolves via the reviewer registry (not the worker
  AgentResolver) and merges AO_REVIEW_WORKER into the adapter's env.
- daemon wires the reviewer resolver. Registry/domain parity is test-enforced.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(review): cover run-scoped reviewer submit

* fix(api): update generated review submit schema

* refactor(review): split core engine (internal/review) from API service

Move the review orchestration (Trigger/Submit/List, run-id generation,
deps, RunSpec/Runner, sentinels) into a transport-independent core package
internal/review (Engine). internal/service/review is now a thin API-flow
boundary: the controller-facing Manager interface + a Service that delegates
to the engine + error re-exports.

This keeps the service layer to API concerns and lets the same engine back a
future in-process CLI trigger without going through HTTP. review_runner now
depends on the core package; daemon builds the engine and wraps it in the
service. No API/schema changes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(review): commit-aware trigger, reviewer handle for UI, no env vars

Reworks the review trigger lifecycle and drops env-based coupling:

- review_run gains target_sha (the reviewed commit) and drops iteration.
  A repeat trigger for the same PR head short-circuits to the existing run.
- review gains reviewer_handle_id: the live reviewer pane's runtime handle,
  reused across passes and exposed in the reviews API so the UI can attach
  its terminal over /mux.
- Trigger flow: if a live reviewer pane exists and a new commit arrived,
  message it to re-review; otherwise spawn a fresh reviewer. The run is
  recorded only after the reviewer is launched.
- No environment variables: the reviewer adapter embeds the explicit
  `ao review submit --session <w> --run <id>` command in the spawn prompt
  and the re-review message. CLI submit requires --run/--session (no env
  fallbacks).
- Merge review_runner into internal/review as a Launcher (spawn/notify/alive).
- Trigger returns 201 for a new pass, 200 when reusing an existing run.

Regenerated sqlc + OpenAPI/TS.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(review): author the reviewer prompt centrally, not in the adapter

Mirror the worker model (session_manager builds the prompt; adapters just
place it via LaunchConfig.Prompt). The reviewer prompt now lives in
internal/review/prompt.go and is passed through ports.ReviewInvocation.Prompt;
the claude-code reviewer adapter just feeds inv.Prompt to its launch command
and returns it as the re-review message. One-shot CLI reviewers may ignore it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(review): split reviewer prompt into system+task, mirroring buildSpawnTexts

Mirror session_manager.buildSpawnTexts for the reviewer: a standing role goes
in the system prompt, the per-pass task (PR/commit + exact `ao review submit`
command) goes in the user prompt. internal/review/prompt.go now returns
(prompt, systemPrompt); both flow through ports.ReviewInvocation and the
claude-code adapter places them via LaunchConfig{Prompt, SystemPrompt}. The
re-review message reuses the per-pass prompt (role already established in the
running pane).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Vaibhaav <user@example.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-06-15 01:17:17 +05:30
Khushi Diwan a197ff6d88
fix(spawn): persist the resolved default agent on the session (#221)
A spawn with no explicit harness ran the daemon default (claude-code) but
stored an empty harness: effectiveHarness returned "", seedRecord persisted it,
and the empty->default resolution lived only inside agentRegistry.Agent. The API
then omitted harness and the UI defaulted to "codex" — mislabelling a Claude
Code session.

Inject the daemon's default agent (AO_AGENT / config.DefaultAgent) into the
session manager and resolve an unspecified harness to it before the seed row is
written, so the stored/returned harness matches the agent that actually runs.

Closes #220
2026-06-15 00:24:20 +05:30
Adil Shaikh 17df922652
feat: add notifications v1 (#181)
* feat: add notifications v1

* fix: address notification review feedback

* fix: require passing CI for merge-ready notifications

* fix: simplify notification listing

* fix: ignore missing sessions for scm notifications

* fix: project notifications from cdc

* fix: stream notifications without cdc
2026-06-14 20:02:32 +05:30
Ashish Huddar 8d0c53ec1d
fix(codex): reliable activity signals — session-flag hooks, trust bypass, no_signal watchdog (#170)
* fix(codex): deliver activity hooks via -c session flags, trust worktree at launch

Codex (0.136+) never loads hook config from AO's per-session worktrees:
project-local .codex/ layers only load from trusted directories, and for
linked git worktrees codex sources hook declarations from the matching
folder in the root checkout — so the workspace-local .codex/hooks.json AO
wrote was dead config and codex sessions never reported activity.

Deliver the hooks on the launch/resume command instead:
- -c 'hooks.<Event>=[...]' session-flag config for SessionStart,
  UserPromptSubmit, PermissionRequest, and Stop; the session-flags layer
  is not trust-gated and aggregates with the user's own hooks. The
  existing --dangerously-bypass-hook-trust flag lets them run without a
  persisted trust hash.
- -c 'projects={"<worktree>"={trust_level="trusted"}}' (inline-table
  form; the dotted projects."<path>".trust_level key is corrupted by
  codex's naive -c dot-split) so spawns into never-trusted repos don't
  hang invisibly on the interactive directory-trust prompt. Both the
  literal and symlink-resolved worktree paths are trusted.
- -c notice.hide_rate_limit_model_nudge=true so the "switch to a cheaper
  model?" dialog can't hang a headless pane and swallow the spawn prompt.

GetAgentHooks no longer writes workspace files (worktrees stay clean); it
only strips entries older AO versions left in .codex/hooks.json,
preserving user hooks. UninstallHooks/AreHooksInstalled now operate on
those legacy files only.

Verified with a real spawn into a fresh untrusted repo: activity
transitions idle -> active -> idle hands-free, no .codex dir in the
worktree, no hook delivery failures.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(sessions): activity-signal watchdog + hook delivery hardening

A codex upgrade broke activity tracking silently: sessions showed a
confident "idle" forever while the agent worked. This bundle makes hook
delivery verifiable end to end and makes any future breakage loud
instead of invisible.

Watchdog (no_signal status):
- sessions.first_signal_at (migration 0010) records the FIRST hook
  callback per spawn/restore — raw signal receipt, independent of the
  derived activity state. lifecycle.ApplyActivitySignal stamps it (and
  writes through same-state repeats until stamped, e.g. Codex
  SessionStart reporting idle on an idle-seeded row); MarkSpawned clears
  it so every relaunch re-proves its hook pipeline.
- deriveStatus downgrades a live session with no receipt to the new
  no_signal display status after a 90s grace, instead of idle.
  Terminated/PR-derived statuses still win. The sessions CDC update
  trigger now also fires on first-signal receipt so the dashboard
  transition is pushed live.
- frontend maps no_signal -> needs_you (a human should look at the pane).

Hook callback hardening (re-landed from the closed redesign PR #156):
- the session manager pins each spawned session's PATH with the daemon
  executable's directory first, so the bare `ao` in hook commands
  resolves to the daemon that installed them, with a spawn-time warning
  when the pin cannot apply.
- `ao hooks` failures append to $AO_DATA_DIR/hooks.log (size-capped);
  `ao doctor` gains a hooks-log check that warns on failures from the
  last 24h, and an ao-binary identity check.

Codex launch-surface canary:
- `ao doctor` gains codex-launch-flags: it runs probes exported by the
  codex adapter (built from the same flag builders as the real spawn
  argv) against the installed binary, warning when codex rejects the
  hook-trust bypass flag or AO's -c session-flag overrides.
- codex hook callback timeout drops 30s -> 5s so a hung daemon cannot
  stall the agent's turn.

Docs: the agent PRD callback section now describes the implemented flow
(derive state, POST /sessions/{id}/activity, hooks.log) instead of the
unbuilt SQLite/metadata merge, and notes that hook-derived metadata
persistence (codex resume) is still not implemented.

Frontend note: main's renderer test suite has 7 pre-existing failing
files and a vite-config typecheck error unrelated to this change;
workspace.test.ts (the only frontend file touched) passes 26/26.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(store): restore TestSessionWorktreesRoundTrip lost in the re-landing port

The branch ported store_test.go wholesale from the closed redesign
branch, whose copy predates #165 — silently dropping the
session-worktrees round-trip test #165 added. Restore main's file and
re-apply only this branch's addition (TestSessionFirstSignalRoundTrip).
No other ported file lost main-side content (audited per-file against
main; the remaining deletions are this branch's intended refactors).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(status): only derive no_signal for harnesses that have a hook pipeline

Review finding: the no_signal downgrade had no harness-capability gate, but
first_signal_at can only ever be stamped by an `ao hooks` callback. Ten
spawnable harnesses (amp, aider, crush, grok, kimi, devin, auggie, continue,
vibe, pi) install no hooks at all, so every live session of theirs would have
flipped from idle to a permanent no_signal -> needs_you after the 90s grace.

The session service now takes a SignalCapable predicate; daemon wiring injects
activitydispatch.SupportsHarness (the deriver registry is the source of truth
for "this harness can signal"). Left nil, the service never claims no_signal.
A new dispatch test pins that every deriver token is a known harness name.

Also from the same review:
- lifecycle/manager.go and the 0010 migration claimed Codex's SessionStart
  reports idle as the first signal; both codex and claude-code derivers
  deliberately return no signal for session-start, so the comments now cite a
  real case (a lost "active" POST followed by a Stop hook landing idle).
- docs/agent/README.md documents the gate and the restore caveat: a restored
  session the user never prompts has nothing to signal, so it shows no_signal
  after the grace until a receipt-only session-start signal exists.
- 0010 migration uses DROP TRIGGER IF EXISTS per house style.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 10:36:45 +05:30
yyovil 3152cdc948
feat(agents): agent platform — registry, activity hooks, harness allowlist (#119)
Introduces the shared platform that per-agent adapters plug into, wired for the
three shipped harnesses (claude-code, codex, opencode):

- adapters/agent/registry: single source of truth for shipped adapters
  (Constructors), consumed by the daemon to resolve a session's harness.
- adapters/agent/activitydispatch + 'ao hooks' command: maps an agent's native
  hook callbacks onto AO activity states (active/idle/waiting/...).
- claudecode/codex/opencode: emit SessionStart/UserPromptSubmit/Stop activity.
- HTTP + OpenAPI: report session activity state.
- db: single migration widening sessions.harness to all shipped harnesses, so
  adding an adapter needs no further migration.
- domain: harness constants + --agent alias for 'ao spawn'.

Adding a new agent is now one adapter package plus a line in Constructors().

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Harshit Singh Bhandari <claudeagain@pkarnal.com>
2026-06-07 00:52:40 +05:30
Harshit Singh Bhandari 3413acca33
feat: ao session claim-pr + spawn --claim-pr wiring (#101)
* feat: add session PR claiming CLI and API

* fix: tighten PR claim rollback and CDC facts

* fix: align PR claim branch with latest main
2026-06-06 00:01:03 +05:30
Harshit Singh Bhandari bfb6e9860b
feat(scm): wire observer messenger + RepoOriginURL + persist dedup (#108) (#114)
* feat(daemon): thread runtime messenger into Lifecycle Manager (#108)

The daemon used to construct the LCM with a nil messenger, so every
SCM-driven nudge dropped silently inside sendOnce. Move newSessionMessenger
above startLifecycle and pass the real messenger through, so CI-failure,
review-feedback, and merge-conflict nudges actually reach the agent.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(project): populate RepoOriginURL at add + lazy observer backfill (#108)

project.Add now shells out to `git -C path remote get-url origin` and
captures the result on the new project row, so the SCM observer can parse
it on the first poll. A missing remote falls back to "" rather than failing
project add — non-git roots and remoteless repos stay registerable.

To cover projects added before this change, the observer's discoverSubjects
lazily backfills RepoOriginURL via the same shell-out and persists it
through UpsertProject, so subsequent polls skip the fork-exec.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(lifecycle): persist reaction-dedup signatures across restart (#108)

Add migration 0005 with `pr.last_nudge_signature TEXT NOT NULL DEFAULT ''`
and two scoped sqlc queries (Get/UpdatePRLastNudgeSignature). Lifecycle
serialises the per-PR slice of its seen/attempts maps to that column as a
small JSON document; sendOnce loads it lazily on first touch of each PR
and persists after every successful send.

This closes the post-restart re-nudge gap: the daemon used to lose the
seen map on bounce, so a still-failing CI re-prompted the agent on the
first post-restart observer poll even when it had already been told.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(lifecycle): silence nilerr on intentional corrupt-payload swallow

golangci-lint's nilerr flagged the `if err := json.Unmarshal(...); err != nil { return nil }`
path in loadPRSignaturesLocked. The swallow is deliberate (a corrupt persisted
payload should not crash the lifecycle write path), so compare against nil
directly so no `err` is bound and the lint goes quiet.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix: silence nilerr, address reviewer notes, drop task-tagged comments

- reactions.go: discard the json.Unmarshal error explicitly via `_ =` so
  golangci-lint's nilerr stops flagging the intentional corrupt-payload
  swallow; behavior unchanged.
- reactions.go: document the Send → memory → persist order in sendOnce so
  the "one extra nudge on restart after a transient persist failure"
  trade-off is explicit (vs. the inverse risk of losing a real nudge).
- service.go: stop reaching for slog.Default() in resolveGitOriginURL;
  align with the observer's identical helper that just returns "" on git
  failure rather than logging through the global logger.
- tests: drop "issue #108" / "guards the regression from #X" framing in
  test docstrings — explain WHAT the test asserts, not the PR context.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-05 23:20:02 +05:30
Adil Shaikh 19b6ca5093
feat: add provider-neutral SCM observer (#76)
* feat: add provider-neutral scm observer

* fix: satisfy scm batch query lint

* fix: avoid scm token preflight on startup

* fix: bound github scm review refresh

* docs: clarify scm observer fields

* fix: gate scm observer credentials lazily

* fix: preserve scm review threads in legacy pr writes

* fix: harden scm observer state refresh

* fix: retry scm lifecycle after persistence

* fix: persist scm lifecycle acknowledgement cursor

* fix: tune scm graphql pagination

* chore: remove scm observer no-op code

* fix: tighten scm bot and log-tail handling

* fix(scm): emit CDC events for pr_review_threads

* fix(db): renumber scm observer migration

* fix: preserve cdc triggers during scm migration

* fix: preserve scm comments across legacy writes

* fix: retry transient scm credential checks

* fix: preserve review rows during lifecycle ack

* fix: harden scm observer review follow-ups

---------

Co-authored-by: harshitsinghbhandari <24b4506@iitb.ac.in>
2026-06-04 22:26:07 +05:30
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
Harshit Singh Bhandari 5435246c9a
feat(cli): add minimal ao send (#83)
* feat(messenger): ao send + live zellij pane ping (live agent nudges)

Replace the daemon's noopMessenger stub with a composite AgentMessenger
that writes a durable inbox file (primary) and types a live pointer into
the running zellij pane (best-effort secondary), plus the `ao send` CLI
that drives the existing POST /api/v1/sessions/{id}/send route.

- composite: fans Send to inbox then panep, pinning one timestamp so both
  derive the same filename; a secondary failure is logged at WARN and
  swallowed (the file is on disk), a primary failure aborts the call.
- inbox: writes <workspace>/.ao/inbox/<rfc3339nano>_<hash>.md.
- panep: types "new message at .ao/inbox/<file>" + Enter via a new narrow
  zellij WriteChars seam (RuntimePaneWriter), kept off ports.Runtime.
- wiring: newSessionMessenger composes inbox+panep over the shared store;
  startSession takes the messenger instead of the noop stub.

Carries across @aa-43's work from PR #74 (staging), adapted to main's
post-#65/#77 daemon wiring shape.

Closes #79

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(inbox): use O_EXCL so a filename collision errors instead of clobbering

os.WriteFile opens with O_CREATE|O_WRONLY|O_TRUNC, which silently overwrites
an existing file. The doc comment already stated the intent ("we do not retry
on EEXIST"), but O_TRUNC never yields EEXIST — two identical messages sent on
the same composite-pinned nanosecond would produce the same filename and the
second Send would silently lose the first message. Switch to
O_CREATE|O_EXCL|O_WRONLY so a collision surfaces as an error; O_EXCL also
refuses to follow a symlink at the final path component. Add a regression test.

Addresses greptile review on PR #83.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(inbox): remove the freshly-created file when write or close fails

The O_EXCL switch creates the inbox file before writing its body; if
WriteString or Close then fails, the empty/partial .md was left on disk and
the agent's next inbox scan would pick up a truncated ghost message. Remove
the file on those error paths. O_EXCL guarantees the file did not exist before
this call, so the cleanup can only delete our own partial write, never a
legitimate earlier message.

Addresses greptile review on PR #83.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(messenger): reduce ao send to live pane delivery

* fix(send): preserve messages and map lookup errors

* fix(send): reject terminated sessions
2026-06-02 20:02:47 +05:30
yyovil 57bb63701d
Add `ao spawn` + `ao project add` (spawn a real worker end-to-end) (#77)
* Add `ao spawn` and `ao project add`; resolve project repos for worktrees

Make a registered project spawnable end-to-end from the CLI:

- DB-backed RepoResolver: the daemon resolves a project's on-disk repo
  path from the projects table (replacing the empty StaticRepoResolver
  that failed every lookup), so a session's worktree is cut from the
  right repo.
- session_manager defaults an empty spawn branch to ao/<session-id> — a
  fresh, unique branch per session, since gitworktree can't reuse a
  branch already checked out elsewhere (e.g. main).
- `ao project add --path <repo>`: register a local git repo (POST /api/v1/projects).
- `ao spawn --project <id> [--harness] [--branch] [--prompt] [--issue]`:
  spawn a worker session (POST /api/v1/sessions); harness defaults to the
  daemon's AO_AGENT.
- Shared postJSON daemon client (reads the run-file for the port, surfaces
  the API error envelope).

Stacked on #65, which lands the agent-adapter + session-manager wiring
this depends on.

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

* Address Copilot review on #77

- `ao spawn` no longer prints a branch the sessions API doesn't return
  (session metadata is json:"-"), so the output is no longer misleading.
- Unregistered/archived/no-path projects now surface a 400
  PROJECT_NOT_RESOLVABLE with an actionable message instead of a generic
  500: a new sessionmanager.ErrProjectNotResolvable sentinel the resolver
  wraps and writeSessionError maps.
- postJSON reuses the injected Deps.HTTPClient (cloned, with a longer
  timeout) instead of a fresh client, keeping HTTP behaviour stubbable.
- postJSON treats a stale run-file (dead PID) as "not running" via
  ProcessAlive, matching its docstring.

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

* Assert the project-not-resolvable sentinel in the resolver test

Greptile review: harden TestProjectRepoResolver to verify the unregistered
-project error wraps ErrProjectNotResolvable, so a future regression in the
sentinel wrapping (which the HTTP 400 mapping relies on) is caught.

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

* Fix ao spawn 500 on long session ids (zellij socket-path overflow)

Root cause: the daemon built the zellij runtime with an empty SocketDir,
so zellij fell back to its $TMPDIR-based default (long on macOS). That
left almost none of the ~103-byte unix-socket-path budget for the session
name, so a long session id (e.g. "aoagents-agent-orchestrator-1", derived
from a long project id) was rejected by zellij with "session name must be
less than 0 characters". runtime.Create failed, the spawn 500'd, and the
worktree was rolled back (leaving an orphan ao/ branch).

- New zellij.DefaultSocketDir(): a short, stable per-user socket dir
  (/tmp/ao-zellij-<uid>); the daemon uses it (and MkdirAll's it).
- ao spawn's attach hint now prefixes ZELLIJ_SOCKET_DIR so it stays
  copy-pasteable against the daemon's socket dir.
- Regression test guards that the socket dir leaves >= 48 bytes for the
  session name within the 103-byte limit.

Verified: ao spawn against a long-id project now succeeds (session live,
worktree created) where it previously 500'd.

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

* test(cli): guard CLI/daemon DTO drift with an e2e round-trip

The CLI keeps its own request structs (spawnRequest, addProjectRequest)
separate from the daemon's canonical DTOs (controllers.SpawnSessionRequest,
project.AddInput). Nothing verified the JSON field names agreed, so a renamed
tag on either side would compile but break at runtime.

Drive `ao spawn` and `ao project add` through the real httpd router and
controllers (fakes only at the service layer) over a real loopback round trip
via postJSON, asserting each field decodes into the right SpawnConfig/AddInput
field. Runs in the normal test lane (no extra ports/processes).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(cli,daemon): address review findings on ao spawn

- spawn: print the sanitised zellij session name (zellij.SessionName) in the
  attach hint; a long/non-conforming session id is registered under a different
  name, so the raw id sent users to a missing session.
- client: surface the daemon error envelope's requestId so a failed command can
  be correlated with daemon logs.
- daemon: don't swallow the zellij socket-dir MkdirAll error — log it, since a
  failure otherwise surfaces later as an opaque socket-bind error on every spawn.
- project: reject an embedded ".." in a project id up front; it passed the id
  pattern but yielded an invalid branch (ao/a..b-1) and an opaque 500 at spawn.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: harshitsinghbhandari <24b4506@iitb.ac.in>
2026-06-02 18:39:13 +05:30
yyovil 3346c6cb6c
Add agent adapters and wire per-session agents into the session manager (#65)
* feat(plugin): add agents plugin (first iteration)

Faithful copy of the agents plugin implementation from yyovil/better-ao
(internal/plugin/ -> backend/internal/plugin/) plus its PRD
(prds/plugins/agents/PRD.md), as a first-iteration proposal for review.

Imports are left at their original github.com/yyovil/better-ao/... paths and
are NOT yet reconciled to this repo's module; see PR description for the
integration deltas (module path, missing internal/utils dependency).

Co-authored-by: Claude <noreply@anthropic.com>

* Move agent adapters under backend adapters

* Keep daemon ports and session out of adapter move

* Remove Better-AO naming from flake

* Keep flake as dev shell only

* Use goimports for local formatting

* Wire session manager to per-session agent adapters

Move the Agent port into internal/ports and have the claude-code and
codex adapters implement it directly, alongside their workspace-local
activity hooks and a manifest-keyed adapter registry. Rename
RuntimeConfig.LaunchCommand to Argv and update the tmux and zellij
runtimes to match.

The session Manager now resolves a real agent adapter per session via a
new ports.AgentResolver: from cfg.Harness on Spawn and the stored harness
on Restore, so one daemon runs claude-code and codex sessions side by
side. The daemon backs the resolver with the registry; AO_AGENT selects
the default harness (default claude-code), validated at startup. Removes
the temporary noopAgent stub.

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

* docs(agent): point the agent contract at internal/ports/agent.go

The Agent interface moved from internal/adapters/agent to internal/ports;
update the PRD's Goal and Agent Contract sections (and the SessionInfo
references) to match the code.

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

* Wire the session service into the daemon

daemon.Run now builds the controller-facing session service — a session
manager over the zellij runtime, a gitworktree workspace, the shared
store + LCM, and the per-session agent resolver (AO_AGENT default,
validated at startup) — and mounts it at httpd APIDeps.Sessions, so the
session REST routes are backed by a real service. startLifecycle moves
ahead of the HTTP server so both share one LCM.

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

* Address Greptile review: complete the live spawn path

- Spawn and Restore now install workspace-local activity hooks
  (GetAgentHooks) and run the adapter's optional PreLaunch step before
  launch, via a shared prepareWorkspace helper. PreLaunch is how Claude
  Code records workspace trust, so its interactive "trust this folder?"
  dialog can't hang the headless pane; the spawned env now also carries
  AO_DATA_DIR so the installed hook commands find the store.
- claudecode and codex hook/config writes are now atomic (temp + rename)
  instead of os.WriteFile, so a crash mid-write can't leave a partial
  file the agent fails to parse.
- ensureWorkspaceTrusted serializes its read-modify-write under a package
  mutex, so concurrent spawns to different workspaces don't drop each
  other's ~/.claude.json trust entries.

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

* test(ports): pin MetadataKeyAgentSessionID to domain.SessionMetadata json tag

The equality between ports.MetadataKeyAgentSessionID and the json tag on
domain.SessionMetadata.AgentSessionID is a hand-maintained invariant; this
test fails loudly if either side drifts.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* refactor(adapters): use ports.MetadataKeyAgentSessionID in claudecode + codex

The native session id metadata key is defined in ports for cross-package
consumption; drop the duplicated literals in each adapter so the constant
has one home.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* test(codex): cover ensureCodexHooksFeatureEnabled TOML edge cases

The helper is a string editor over config.toml; pin its content
transformation for missing/empty files, existing [features] blocks,
the no-op case, and the legacy codex_hooks=true migration paths.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* docs(adapters): document Registry concurrency contract

Registry registration runs at daemon boot before any goroutine calls Get,
so the underlying map needs no lock; pin that contract in the doc comment
so a future change doesn't quietly introduce a race.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* style(codex): gofmt codex_test.go after constant rename

The previous commit (7c5b2a9) replaced codexAgentSessionIDMetadataKey with
ports.MetadataKeyAgentSessionID inside a map literal; the longer key threw
off gofmt's column alignment on the adjacent codexTitleMetadataKey /
codexSummaryMetadataKey lines. Caught by agent-ci's Check formatting step.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

Co-authored-by: harshitsinghbhandari <dev@theharshitsingh.com>
2026-06-02 16:51:32 +05:30
prateek a34094e7d8
refactor: simplify session lifecycle and zellij runtime (#62)
* refactor: remove canonical lifecycle state

* refactor: move sqlite stores into subpackage (#62)

* refactor: strengthen sqlite generated model types (#62)

* refactor: remove lifecycle notifications (#62)

* docs: remove notification cleanup leftovers (#62)

* refactor: narrow lifecycle manager scope (#62)

* refactor: keep PR nudges in lifecycle (#62)

* refactor: trim unused storage and lifecycle contracts (#62)

* refactor: align storage and runtime observation surfaces (#62)

* refactor: remove stale daemon and adapter bloat (#62)

* test: fix terminal ring race assertion (#62)

* refactor: trim lifecycle and http boilerplate (#62)

* refactor: expose sqlite CDC source directly (#62)

* refactor: share process liveness checks (#62)

* test: trim lifecycle store fake surface (#62)

* refactor: separate PR observations from storage rows (#62)

* refactor: trim remaining cleanup surfaces (#62)

* refactor: narrow observation and PR display APIs (#62)

* refactor: move PR write DTOs out of domain (#62)

* refactor: normalize PR domain storage types (#62)

* refactor: remove unused session port interface (#62)

* fix: reject unexpected CLI arguments (#62)

* refactor: use session metadata for spawn completion (#62)

* refactor: narrow session runtime dependency (#62)

* fix: validate zellij version in doctor (#62)

* refactor: split observation port DTOs (#62)

* chore: add sqlc generation script (#62)

* refactor: clarify terminal mux naming (#62)

* fix: tolerate nil loggers (#62)

---------

Co-authored-by: itrytoohard <ayetrytoohard@gmail.com>
2026-06-01 08:42:49 +05:30
prateek 8df074b1c9 chore(backend): add golangci-lint with a strong ruleset and clear the tree
Introduces backend/.golangci.yml (27 linters across correctness, dead-code/
boilerplate, style, and security), wires it into CI as a blocking job, and
fixes every finding so the tree starts at zero.

Config:
- 27 linters: errcheck, govet, staticcheck, errorlint, bodyclose,
  sqlclosecheck, rowserrcheck, nilerr, makezero, unused, unparam, unconvert,
  wastedassign, copyloopvar, prealloc, dupl, revive (incl. exported-symbol doc
  comments), gocritic, misspell, usestdlibvars, predeclared, nakedret, gosec, …
- Tuned for signal over noise: govet/shadow and gocritic hugeParam/rangeValCopy/
  unnamedResult disabled (idiomatic-Go false positives); sqlc-generated code and
  tests get scoped exclusions; gosec G304 excluded (paths are config/run-file/
  worktree-derived, not user input); nilerr excluded in cli/status.go (probe
  failures are the reported status, not a command error).

CI:
- New blocking lint job (golangci-lint-action, latest binary for Go-version
  compatibility).
- go-version now read from go.mod (was pinned 1.22 while go.mod declares 1.25).

Cleanup to reach zero (no behavior change):
- errcheck: wrap deferred/inline Close()/Remove()/Rollback() with `_ =`.
- gosec: tighten dir/file perms (0755->0750, 0644->0600).
- unparam: drop always-nil error return from startLifecycle; drop unused
  shellPath param (zellij PowerShell) and always-500 fallbackStatus param
  (writeProjectError).
- gocritic: regexp \d, s != "", switch->if, combined appends.
- revive: doc comments on all exported symbols; rename project.ProjectRow ->
  project.Row (stutter); rename `max` locals shadowing the builtin.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 04:58:41 +05:30
prateek 28e1205d28 refactor(backend): collapse duplicate PR row types into one domain definition
Each PR-child table (pr / pr_checks / pr_comment) had three near-identical
structs — gen.* (generated), sqlite.*Row, and ports.* — with wiring.Adapter
copying field-by-field between them. Collapse to one shared definition per
table in domain (PRRow / PRCheckRow / PRComment), used by both the PRWriter
port and the sqlite store; gen.* stays sealed inside the storage layer.

- *sqlite.Store now satisfies ports.SessionStore + ports.PRWriter directly,
  so the entire wiring.Adapter package is deleted (lifecycle.New(store, store)).
- The bool PR state <-> single state column, int<->int64, and enum-default
  translation now lives only at the gen<->domain boundary in pr_store.go.
- WritePRObservation renamed WritePR to match the port; the integration test
  and composition root drop their adapter copies.

Net -280 lines, behaviour unchanged. go test -race ./... green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 03:40:33 +05:30
Dhruv Sharma 4671d27307 feat(backend): add cobra cli foundation 2026-06-01 01:44:43 +05:30