agent-orchestrator/backend/internal/cli
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
..
client.go feat(config): persist per-project agent config and resolve it at spawn (#154) 2026-06-08 21:35:29 +05:30
client_test.go Add `ao spawn` + `ao project add` (spawn a real worker end-to-end) (#77) 2026-06-02 18:39:13 +05:30
completion.go fix(cli): harden daemon control surface and stop CLI from writing the store 2026-06-01 01:55:14 +05:30
doctor.go fix(codex): reliable activity signals — session-flag hooks, trust bypass, no_signal watchdog (#170) 2026-06-11 10:36:45 +05:30
doctor_test.go fix(codex): reliable activity signals — session-flag hooks, trust bypass, no_signal watchdog (#170) 2026-06-11 10:36:45 +05:30
dto_drift_e2e_test.go feat: add workspace project registration foundation (#165) 2026-06-10 16:10:14 +05:30
e2e_test.go feat(cli): enrich ao doctor (#90) (#99) 2026-06-03 16:50:54 +05:30
exitcode_test.go test(cli): pin ExitCode mapping (usage=2, runtime=1, nil=0) 2026-06-01 02:38:25 +05:30
hooks.go fix(codex): reliable activity signals — session-flag hooks, trust bypass, no_signal watchdog (#170) 2026-06-11 10:36:45 +05:30
hooks_test.go fix(codex): reliable activity signals — session-flag hooks, trust bypass, no_signal watchdog (#170) 2026-06-11 10:36:45 +05:30
orchestrator.go feat: add light backend CLI commands (#98) 2026-06-03 16:18:00 +05:30
orchestrator_test.go feat: add light backend CLI commands (#98) 2026-06-03 16:18:00 +05:30
output.go feat(backend): add cobra cli foundation 2026-06-01 01:44:43 +05:30
pr_ref.go feat: ao session claim-pr + spawn --claim-pr wiring (#101) 2026-06-06 00:01:03 +05:30
process.go refactor: simplify session lifecycle and zellij runtime (#62) 2026-06-01 08:42:49 +05:30
process_unix.go refactor: simplify session lifecycle and zellij runtime (#62) 2026-06-01 08:42:49 +05:30
process_windows.go refactor: simplify session lifecycle and zellij runtime (#62) 2026-06-01 08:42:49 +05:30
project.go feat: add workspace project registration foundation (#165) 2026-06-10 16:10:14 +05:30
project_test.go feat(cli): add ao project ls/get/rm (#90) (#91) 2026-06-03 04:46:55 +05:30
root.go [codex] add ao hooks activity command (#113) 2026-06-06 20:29:00 +05:30
root_test.go refactor: simplify session lifecycle and zellij runtime (#62) 2026-06-01 08:42:49 +05:30
send.go fix: prefix ao send messages with sender session (#85) 2026-06-02 21:39:21 +05:30
send_test.go fix: prefix ao send messages with sender session (#85) 2026-06-02 21:39:21 +05:30
session.go feat: ao session claim-pr + spawn --claim-pr wiring (#101) 2026-06-06 00:01:03 +05:30
session_test.go feat: ao session claim-pr + spawn --claim-pr wiring (#101) 2026-06-06 00:01:03 +05:30
spawn.go fix(sessions): remove agent rules spawn path (#159) 2026-06-09 00:10:28 +05:30
spawn_test.go fix: 7 bugs from discussion #149 smoke walk (envelope, spawn, CDC, observer) (#153) 2026-06-07 07:35:46 +05:30
start.go refactor: simplify session lifecycle and zellij runtime (#62) 2026-06-01 08:42:49 +05:30
status.go refactor: simplify session lifecycle and zellij runtime (#62) 2026-06-01 08:42:49 +05:30
stop.go refactor: simplify session lifecycle and zellij runtime (#62) 2026-06-01 08:42:49 +05:30
stop_test.go refactor: simplify session lifecycle and zellij runtime (#62) 2026-06-01 08:42:49 +05:30
version.go refactor: simplify session lifecycle and zellij runtime (#62) 2026-06-01 08:42:49 +05:30