Commit Graph

51 Commits

Author SHA1 Message Date
Dushyant Singh Hada de9e90df45
fix: resolve duplicate migration version 20 collision (#2294)
Two PRs (#2193, #2200) merged ~2h apart each added a migration
file numbered 0020, since each branch only saw one such file at
CI time and neither was rebased against the other before merge.
Renumbers 0020_pr_reviews.sql to 0021_pr_reviews.sql.

TestMigrationVersionsAreUnique already existed and is correct;
it didn't catch this because it only runs against each PR's
branch state, not the merged result of both PRs together.
2026-06-30 11:23:54 +05:30
Adil Shaikh a31cf1b582
fix: deep-link PR attention actions (#2200)
* fix: deep-link PR attention actions

* chore: format with prettier [skip ci]

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-06-29 21:16:35 +05:30
neversettle cb456bb288
feat: show multi-PR review status (#2193)
* feat: support multi-pr review runs

* fix: avoid review state status stutter

* feat: batch review delivery by trigger

* test: update review inspector mocks

* chore: fold review batch migration into 0020

* fix: submit multi-pr reviews as one batch

* fix: make queued reviews autonomous

* fix: clarify multi-pr review submit prompt

* feat: show multi-pr review status

* chore: format with prettier [skip ci]

* feat: simplify multi-pr review summary

* chore: format with prettier [skip ci]

* fix: match multi-pr review card design

* chore: format with prettier [skip ci]

* fix: remove review session label

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Vaibhaav <user@example.com>
2026-06-29 18:20:28 +05:30
Harshit Singh Bhandari 572093de9d
feat: dashboard legacy-migration popup + app-state migration marker (#2219)
* docs: design for dashboard legacy-migration popup + app-state marker

Spec for the app-side migration trigger (Approach A): projects-only import
daemon API + a migration marker in ~/.ao/app-state.json, with a launch-time
popup (Proceed / Skip / Don't Migrate). Settings redo path deferred to #2205.
Includes the projects-only import-offer backend plan it consumes.

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

* docs: implementation plan for legacy-migration popup + marker

Part A reuses the projects-only import API (import-offer plan) with an
availability-only Status; Part B adds the app-state migration marker (schema v2),
IPC, the useMigrationOffer gate, and the MigrationPopup (Proceed/Skip/Don't
Migrate). Settings redo path deferred to #2205.

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

* refactor(legacyimport): scope import to projects + settings only

Remove orchestrator/transcript import code (orchestrator.go, claude.go,
session_import_store.go and their tests). Trim Store, Options, Report to
projects-only fields. Drop defaultClaudeProjectsDir and projectSessionsDir
from paths.go. Add yaml.TypeError robustness in config.go. Update cli/import.go
confirm prompt and summary. Update importer_test.go to projects-only assertions.

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

* feat(importer): availability probe + projects-only run

Create service/importer.Manager with Status (physical availability check only,
no DB heuristic) and Run (delegates to legacyimport.Run). The app-state.json
marker governs whether to prompt; this service only answers whether legacy data
is physically present.

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

* feat(httpd): import controller + DTOs (GET/POST /api/v1/import)

Add ImportStatusResponse/ImportRunResponse DTOs to dto.go. Create
ImportController with GET (status probe) and POST (run) handlers, both
returning 501 when Svc is nil. Wire APIDeps.Import + API.imports in api.go.
Add controller tests (status, status error, run, run error).

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

* feat(apispec): describe /api/v1/import; regenerate openapi + schema.ts

Add import tag, importOperations() (GET + POST /api/v1/import), and schema
name mappings (ImportStatusResponse, ImportRunResponse, ImportReport) to
build.go. Regenerate openapi.yaml and frontend/src/api/schema.ts. Route-spec
parity test passes. Restore the nil-svc-501 import controller test now that
the spec includes the operation.

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

* feat(daemon): mount import service on the API

Wire importsvc.New(importsvc.Deps{Store: store}) into APIDeps.Import in
daemon.go so the daemon serves GET/POST /api/v1/import backed by the live
sqlite store. Projects-only; no DataDir.

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

* chore(cli): drop resolved §6.4 first-boot-import TODO

The legacy import is now a daemon API (GET/POST /api/v1/import) served by
the importer service and the desktop app handles the popup prompt via the
app-state.json migration marker. The TODO comment is resolved.

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

* chore(legacyimport): remove dead isDir helper

isDir was only used by the deleted projectSessionsDir function.

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

* chore: update package-lock.json after npm install for api:ts

openapi-typescript was missing from root node_modules; npm install
populated it so npm run api:ts could regenerate schema.ts.

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

* fix(httpd): drop em-dash comment + unused ImportStatusResult alias

Review findings M1/M2 from G1 task review.

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

* feat(app-state): migration marker (schema v2) + updateMigration

Bump SCHEMA_VERSION to 2, add MigrationStatus/MigrationState types and
migration? field to AppStateMarker. Extract atomicWriteMarker helper and
reuse it. writeAppStateMarker now preserves an existing migration block
across launch writes. Add updateMigration (IPC setter) and readMigrationState
(IPC getter) exports. TDD: tests added first (red), then implementation (green).

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

* feat(ipc): expose app-state migration getter/setter to the renderer

Add appState:getMigration / appState:setMigration IPC handlers in main.ts.
Add ao.appState.getMigration / setMigration to preload.ts (typed via AoBridge).
Add appState preview fallback in bridge.ts and test setup so AoBridge stays
satisfied in both browser preview and test environments.

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

* test(app-state): cover corrupt-marker case + clean up temp dirs

G2 review findings I2 (corrupt-JSON branch untested) and m1 (temp dirs not cleaned).

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

* feat(renderer): useMigrationOffer gate (marker + availability)

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

* feat(renderer): MigrationPopup (Proceed / Skip / Don't Migrate)

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

* feat(renderer): surface MigrationPopup on the dashboard

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

* chore: format with prettier [skip ci]

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-06-27 02:24:36 +05:30
Harshit Singh Bhandari a96143b502
Zellij to tmux + ConPTY runtime, session save/restore, crash-proof reconcile (port #404) (#2183)
* Zellij to Tmux and some other fixes. (#404)

* feat(runtime): add tmux adapter package

Adds backend/internal/adapters/runtime/tmux implementing ports.Runtime via
the tmux CLI. Drop-in replacement for the zellij adapter on Darwin/Linux.

Key design points:
- Handle is a plain session id string (no pane-id split needed for tmux).
- Exact-match session targeting via = prefix for kill-session and has-session.
- Keep-alive shell appended to launch command so sessions survive agent exit.
- send-keys -l chunked for literal text delivery (no key-name interpretation).
- IsAlive distinguishes definitive-dead (missing/no-server output) from probe
  errors so the reaper never kills a session on a transient tmux failure.
- 34 tests pass: 32 unit tests via fakeRunner seam, 2 integration tests on
  real tmux 3.6b (TestRuntimeIntegration, TestRuntimeIntegrationExactSessionParsing).

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

* fix(tmux): address four code-review findings in tmux runtime adapter

- Remove em dash from tmux_test.go:462 (project hard rule); replace with semicolon
- Derive integration test session IDs from t.Name() so concurrent runs do not collide on the same tmux session
- Remove dead scaffolding variables (r/fr, r2/fr2) in TestCreateDestroysAndReturnsErrorWhenNotAlive
- Quote \${SHELL:-/bin/sh} in buildLaunchCommand and update all asserting tests

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

* feat(runtime): wire tmux on Darwin/Linux via runtimeselect, keep zellij on Windows

- New package runtimeselect: Runtime union interface (ports.Runtime +
  SendMessage/GetOutput/AttachCommand) with compile-time assertions for
  both adapters. New(log) returns tmux on non-Windows, zellij on Windows
  (replicating the old daemon socket-dir setup).
- daemon.go: replace zellij-specific socket-dir block with
  runtimeselect.New(log); update comment to be runtime-neutral.
- lifecycle_wiring.go: startSession param changed from *zellij.Runtime
  to runtimeselect.Runtime.
- cli/doctor.go: runtime-aware checkTerminalRuntime (tmux on Darwin/Linux,
  zellij on Windows); added checkTmux.
- cli/spawn.go: attach hint prints tmux attach -t <name> on non-Windows,
  keeps zellij attach hint on Windows.
- wiring_test.go: startSession test uses runtimeselect.New(nil); zellij
  direct tests retained for zellij-specific coverage.
- doctor_test.go: replaced three zellij tool tests with tmux equivalents.

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

* chore: tidy runtime-neutral comments and doctor import grouping

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

* refactor(tmux): drop unused runner.Start seam

tmux creates sessions detached via new-session -d, so the Start method
(carried over from the zellij runner shape, where it backs the Windows
fire-and-forget spawn) is never called. Remove it from the interface and
its implementations to shrink the seam.

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

* feat(conpty): add protocol codec and output ring buffer (pure Go, OS-agnostic)

Ports the ConPTY named-pipe binary framing protocol and rolling output
buffer from pty-host.ts to Go. Implements EncodeMessage, MessageParser
(handles arbitrary chunk boundaries, payload copy guarantee), and Ring
(MaxOutputLines=1000, ANSI-safe, concurrent Append+Snapshot). All 15
unit tests pass on Darwin; GOOS=windows build is also clean.

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

* test(conpty): harden copy-safety and add concurrent ring test

Strengthen TestParserPayloadIsCopy to catch internal-buffer aliasing:
feed frame1, capture its payload, feed frame2 of the same length so the
parser's buffer overwrites the frame1 region, then assert frame1's bytes
are unchanged. The prior test only mutated the input slice post-Feed and
did not exercise the real aliasing risk.

Add TestRingConcurrent: 10 writer goroutines (Append) and 10 reader
goroutines (Snapshot + Tail) running concurrently with a WaitGroup. The
test is meaningful only under the race detector and catches any missing
mu coverage on Ring's exported methods.

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

* feat(ptyregistry): port Windows pty-host sideband registry to Go

Adds package ptyregistry under backend/internal/adapters/runtime/conpty/ptyregistry.
Ports windows-pty-registry.ts: defensive read, atomic temp+rename write,
delete-on-empty, register-replaces-same-ID, and auto-pruning List.
PID liveness isolated behind build tags (syscall.Kill on Unix,
OpenProcess on Windows). 10 tests all green on Darwin.

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

* chore(sdd): phase B briefs and progress for B1-B3

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

* feat(conpty): add pty-host serve engine with loopback TCP transport (B3)

Ports pty-host.ts behavior to Go: ptyConn interface seam, Serve engine
with ring replay, fan-out broadcast, MSG_* handlers, PTY-exit keep-alive,
and graceful shutdown (ConPTY dispose first, 50ms grace, then clients and
listener). Real conptyConn is Windows-only via build tag; non-Windows stub
keeps the package importable on Darwin/Linux. Tests use a fake ptyConn
with real loopback sockets and the B1 MessageParser, passing with -race.

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

* fix(conpty): deliver scrollback snapshot and register client atomically

Review of Task B3 found one Important bug and two minors.

Important: in handleConn the ring Snapshot and the client registration
ran under two separate h.mu acquisitions. A PTY chunk arriving in that
gap was in neither the snapshot nor that client's broadcast, so it was
silently dropped (a hole in the client's stream). Now take the snapshot,
write it to the conn, and add the conn to the clients set all under a
single h.mu hold; broadcast also takes h.mu so it cannot interleave.
Added TestScrollbackLiveOrdering_NoDrop, which emits a contiguous
numbered stream while a client connects and asserts the client's stream
has no internal gap. It reliably fails against the old two-step code and
passes under -race -count=20.

Minor (faithfulness): conptyConn.Close() now also best-effort
Process.Kill() (nil-guarded) so a child that ignores ConPTY EOF still
exits and Done() fires, mirroring pty.kill() in pty-host.ts.

Minor (simplify): use os.Environ() instead of
exec.Command(shellCmd).Environ() for the child env.

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

* chore(sdd): B4 brief

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

* feat(conpty): add runtime adapter with loopback pty-client and session management (B4)

Implements the conpty Runtime adapter: injectable spawn seam, loopback
TCP client helpers (SendMessage/GetOutput/IsAlive/Kill), and Runtime
methods (Create/Destroy/IsAlive/SendMessage/GetOutput). Session resolution
uses an in-memory map with B2 registry fallback for daemon-restart
recovery. Windows-only detached spawn in spawn_windows.go; stub errors
on other OSes. All adapter methods are unit-tested on Darwin against an
in-process B3 Serve and fakePTY. 48 tests pass, all three GOOS builds
succeed, vet clean.

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

* fix(conpty): split IsAlive dead-vs-transient for reaper safety

clientIsAlive collapsed every probe failure (dial timeout, read-deadline
expiry, write error, connection-refused) to false, which the reaper turns
into ProbeDead and the LCM can promote to a permanent reap. A single
transient 2s loopback timeout would spuriously kill a live idle session.

Now clientIsAlive returns (alive bool, transientErr error): a refused dial
is definitively gone (false, nil); a timeout or any connected-then-failed
I/O error is transient (false, err) so the reaper records ProbeFailed and
retries. Wire IsAlive to propagate it. Add regression test covering both
the refused-is-gone and timeout-is-transient paths.

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

* chore(sdd): B5 brief + ledger

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

* feat(terminal): stream-based Attach for tmux/zellij/conpty

Evolve the terminal layer from argv-based attach (PTYSource.AttachCommand
+ injected spawnFunc) to stream-based attach (Source embedding
ports.Attacher). tmux/zellij keep spawning their attach CLI on a local
PTY via the new shared ptyexec.Spawn; conpty attaches by dialing its
loopback pty-host directly with a loopbackStream over the B1 framing
protocol. Reattach/backoff/size/SIGWINCH/detach semantics are unchanged.

- ports: add Stream + Attacher.
- ptyexec: new shared package holding the creack/pty (unix) and ConPTY
  (windows) spawn, moved verbatim from terminal with its tests.
- terminal: PTYSource -> Source, drop spawnFunc/WithSpawn, run loop calls
  src.Attach and uses ports.Stream.
- tmux/zellij: add Attach (argv via ptyexec.Spawn); conpty: add Attach
  (loopbackStream); ports.Attacher assertions on all three.
- runtimeselect: union embeds ports.Attacher in place of AttachCommand.
- tests migrated; new conpty attach_test against in-process Serve+fakePTY.

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

* style(ptyexec): replace em dashes carried from moved pty files

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

* chore(sdd): B6 brief + B5 ledger

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

* feat(runtime): select conpty on Windows, register pty-host subcommand, delete zellij

- runtimeselect.New: Windows branch now returns conpty.New(conpty.Options{}) instead
  of zellij; compile-time assertion updated to conpty.Runtime.
- cli/ptyhost.go: new hidden "ao pty-host" subcommand (DisableFlagParsing so agent
  shell args with leading dashes survive); calls conpty.RunHost and exits with its code.
- cli/root.go: wires newPtyHostCommand alongside newLaunchCommand.
- cli/doctor.go: Windows terminal-runtime check replaced with a static ConPTY
  built-in pass; zellij import and checkZellij function removed.
- cli/spawn.go: Windows attach hint updated to dashboard message (ConPTY has no
  CLI attach); zellij import removed.
- daemon/lifecycle_wiring.go: stale zellij comment updated to tmux/conpty.
- daemon/wiring_test.go: zellij import and TestDaemonZellijSocketDir test removed;
  TestWiring_StartLifecycleThreadsMessengerIntoLCM now uses tmux.New.
- terminal/attachment_integration_test.go: re-pointed at real tmux
  (TestAttachmentStreamsRealTmuxPane + TestAttachmentReattachAdoptsNewSize);
  sessions cleaned up in t.Cleanup.
- internal/adapters/runtime/zellij: deleted entirely.

All three GOOS builds pass; go test -race ./... 1607 passed; go vet clean;
grep -rn "runtime/zellij" returns nothing.

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

* docs(daemon): correct terminal-runtime comment to conpty on Windows

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

* docs(ptyexec): drop stale zellij reference in Windows spawn comment

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

* chore(sdd): final phase B ledger

* build(desktop): support local keychain signing for macOS builds

Bridge forge.config.ts to accept the local keychain flow (APPLE_SIGNING_IDENTITY
identity + AO_NOTARY_PROFILE notarytool profile) in addition to the existing CI
secrets path (CSC_LINK + APPLE_ID/app-specific-password). Enables a signed +
notarized macOS build from a developer Mac without exporting a .p12 or the Apple
ID app-specific password.

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

* fix(daemon): default TERM so Finder-launched tmux attach works

A Finder/Dock launch starts the supervisor under launchd with no
controlling tty, so TERM is unset. The daemon inherits that, and its
tmux attach client (spawned with env=nil, inheriting the daemon env)
dies immediately with "open terminal failed: terminal does not support
clear" — the orchestrator terminal pane never opens.

Seed TERM=xterm-256color (what the renderer's xterm.js emulates) as the
base of buildDaemonEnv, the same place PATH is reconstructed for the same
class of "Finder launch lacks a terminal's env" bug. A real TERM from the
shell/process env still wins.

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

* docs(lifecycle): plan for save-on-close/restore-on-open sessions

Captures the intended daemon lifecycle: on shutdown save every running
session (worker and orchestrator) plus its gitignore-respecting uncommitted
work to refs/ao/preserved/<id>, then force-remove worktrees; on boot recreate
worktrees, replay the preserved work, and restore all sessions. Reuses
existing SQLite state, session_worktrees.preserved_ref, manager.Restore, and
the /shutdown endpoint (no new file, migration, or route).

Also gitignore the built daemon binary copied into frontend/daemon/.

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

* chore(frontend): sync regenerated pnpm-lock and routeTree

Working-tree regeneration of the pnpm lockfile and TanStack Router generated
route tree. No hand edits; generated output only.

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

* feat(workspace): add ForceDestroy for shutdown-path worktree removal

Adds ForceDestroy(ctx, info) to ports.Workspace and the gitworktree
adapter. It runs `git worktree remove --force`, then prune, then
os.RemoveAll as a backstop. A new worktreeForceRemoveArgs builder in
commands.go emits --force; the existing worktreeRemoveArgs is untouched
so Destroy still refuses dirty worktrees via ErrWorkspaceDirty.

TDD: test first creates a dirty worktree, confirms Destroy refuses with
ErrWorkspaceDirty, then confirms ForceDestroy succeeds and the path is
gone and deregistered. All 1609 backend tests pass.

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

* feat(workspace): add StashUncommitted and ApplyPreserved for session lifecycle

Implements the correctness-critical save-on-close / restore-on-open pair
in the gitworktree adapter:

- StashUncommitted: captures uncommitted work (tracked edits and new
  non-ignored files) via a temp GIT_INDEX_FILE into a real commit stored
  at refs/ao/preserved/<session-id>. Never touches the real index or
  stash stack. Returns empty string for clean worktrees. Logs the count
  of .gitignore-skipped paths.
- ApplyPreserved: replays the preserve commit onto a freshly re-added
  worktree via "git checkout <SHA> -- .". Deletes the ref on clean
  success; keeps it and returns ErrPreservedConflict (wrapped) on
  content conflicts.
- Adds both methods to ports.Workspace interface and stubs them in
  integration and session_manager test doubles.

TDD: wrote two failing tests first (RED confirmed via build failure on
undefined methods), then implemented to GREEN. All 39 adapter tests pass.

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

* fix(workspace): replace path-checkout with cherry-pick in ApplyPreserved

git checkout <sha> -- . is a path-checkout that always exits 0 for
content divergence, making ErrPreservedConflict unreachable. Replace
with git cherry-pick --no-commit which performs a true three-way merge,
leaves textual conflict markers on conflict, and exits non-zero so the
sentinel is correctly returned. Conflict detection now uses exit code
only (locale-independent). Add TestWorkspaceIntegrationApplyPreservedConflict
to assert: error is ErrPreservedConflict, preserve ref is kept, conflict
markers appear in the file. All 40 tests pass.

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

* feat(session-manager): add SaveAndTeardownAll and RestoreAll for shutdown lifecycle

Implements Task 3: capture-then-destroy on shutdown and restore-all on startup.

- Adds ErrPreservedConflict to ports as a named sentinel; gitworktree aliases it
  (following the same pattern as ErrBranchCheckedOutElsewhere).
- Extends the Store interface with UpsertSessionWorktree and ListSessionWorktrees
  so the session manager can write the shutdown-saved marker and read it back.
- SaveAndTeardownAll: for every live session with a workspace path, stash
  uncommitted work, write the session_worktrees row (DB commit before worktree
  removal, crash-safety invariant), mark terminated, destroy runtime, force-remove
  the worktree. Best-effort per session; no kind filter.
- RestoreAll: for every terminated session that has a session_worktrees row (the
  marker written by SaveAndTeardownAll), re-create the worktree, apply any
  preserved ref (conflict logs and continues), then relaunch via the existing
  single-session Restore. Sessions killed by the user before shutdown (no row)
  are skipped. Best-effort per session; no kind filter.
- TDD: 9 new tests (RED confirmed via build failure, GREEN confirmed 63 pass).
  Full suite: 1621 tests across 77 packages.

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

* fix(terminal): enable tmux mouse scroll and fix link clicking

On macOS the runtime is tmux, but two mouse interactions were broken in
the embedded terminal while copy/paste kept working:

- Scroll: the renderer drives scrolling by writing SGR mouse-wheel
  reports into the pane (the zellij `--mouse-mode true` model), but tmux
  ignores those reports unless mouse mode is on. Create only set `status
  off`, never `mouse on`, so wheel scrolling silently no-opped. Enable
  `set-option -t <id> mouse on`, mirroring the existing status-off step.

- Link clicking: the default WebLinksAddon handler calls window.open()
  with an empty URL and then assigns location.href. Electron's
  setWindowOpenHandler denies every window.open and only forwards the URL
  passed to it, so the empty open is dropped and clicks no-op. Pass the
  matched URL to window.open directly so the main process routes it to
  shell.openExternal (the OS browser).

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

* test(session-manager): assert UpsertSessionWorktree precedes ForceDestroy

Add a shared ordered call log (sharedLog *[]string) to both fakeStore
and fakeWorkspace. TestSaveAndTeardownAll_CaptureOrderAndMarker now
wires both fakes to the same slice and asserts upsertIdx < forceIdx,
enforcing the crash-safety invariant that the DB write is committed
before the worktree is force-destroyed.

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

* feat(daemon): wire RestoreAll/SaveAndTeardownAll into boot/shutdown sequence

Exposes session manager through a minimal sessionLifecycle interface
(RestoreAll, SaveAndTeardownAll) returned from startSession, then calls
RestoreAll (best-effort) before srv.Run and SaveAndTeardownAll with a
fresh 30s-bounded context after srv.Run returns. Both SIGTERM and POST
/shutdown funnel through srv.Run returning, so the single save call site
covers both paths.

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

* test(daemon): fix seam-test tautology and lifecycle variable shadow

Finding 1: dispatch both sessionLifecycle methods through an interface
variable (var sl sessionLifecycle = fake) so the runtime body exercises
interface dispatch, not just direct struct method calls.

Finding 2: rename local variable 'lifecycle' to 'lc' in
TestWiring_StartSessionBuildsSessionService to remove the shadow of the
imported lifecycle package.

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

* feat(frontend): call POST /shutdown before killing daemon on quit

In before-quit, POST /shutdown (8s AbortSignal.timeout) so the daemon
saves sessions gracefully before the SIGTERM kill. Adds a re-entrancy
guard (quitting flag) so a concurrent app.quit() cannot double-preventDefault.
Falls back to killDaemon on fetch failure or timeout: quit is never blocked.
Keeps the process.on('exit') SIGTERM fallback intact.

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

* fix(storage): guard session_worktrees.state against empty-string CHECK violation; add ponytail comments

The save path (saveAndTeardownOne) never sets domain.SessionWorktreeRecord.State,
so it arrives at UpsertSessionWorktree as "". The generated upsert includes state
in the INSERT column list, so the DB default ('active') is never applied and the
CHECK constraint (state IN ('active', ...)) would fire at the first real shutdown.

Fix: default to 'active' in the store adapter when row.State is "". No schema
change, no migration, no gen edit.

Also add ponytail: comments on the State field (domain type), the write path, and
the read path, documenting that state is unused multi-repo scaffolding and that the
upgrade path is to wire a real value when multi-repo worktree lifecycle states ship.

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

* test(storage): add real-SQLite test for empty-State guard in UpsertSessionWorktree

Adds TestUpsertSessionWorktreeEmptyStateDefaultsToActive to the store
test file. It inserts a SessionWorktreeRecord with State at zero value
"" via UpsertSessionWorktree against a real SQLite DB, then reads the
row back and asserts State == "active". This directly exercises the
guard added in the prior commit and would fail if the guard were
removed (the CHECK constraint rejects ""). Mirrors the helpers and
setup pattern of TestSessionWorktreesRoundTrip exactly.

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

* fix(comments): correct shutdown-mechanism and task-ref inaccuracies

Fix 1: daemon.go comment near SaveAndTeardownAll now correctly states
that POST /shutdown closes the shutdownRequested channel (not cancel ctx).
Also tighten the RestoreAll comment to remove the inaccurate claim.

Fix 2: remove "Task 2's" phrasing from ForceDestroy ponytail comment in
workspace.go; condition still references StashUncommitted by name.

Fix 3: add note in main.ts that the 8s fetch timeout is shorter than the
daemon's 30s save bound, so a SIGTERM after fetch abort does not cut the
in-flight save short.

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

* chore: remove .superpowers workflow scratch from repo

These SDD workflow artifacts (task briefs, agent reports, progress ledger,
review packages) were committed by accident in prior work, against the
.superpowers/sdd/.gitignore intent. Remove them from the repo; they remain
local-only scratch.

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

* docs(spec): graceful restore + post-failure orchestrator recreate

Fix the opaque 500 when restoring an un-resumable session (typed 409
SESSION_NOT_RESUMABLE), and add a post-failure popup that offers to recreate a
fresh orchestrator on the same branch (cleaning the worktree, preserving
committed history). Orchestrators only; recreate fires only after a restore
attempt confirms the session cannot be resumed.

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

* docs(plan): restore-recreate orchestrator; reuse existing /orchestrators clean=true

Planning discovery: the recreate capability already ships via POST /orchestrators
(clean=true), which kills the dead orchestrator and re-spawns on the canonical
branch (addWorktree reattaches an existing branch). So the feature collapses to a
typed-error fix plus a frontend popup. Spec updated to match.

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

* fix(session): return typed SESSION_NOT_RESUMABLE instead of 500 on un-resumable restore

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

* feat(renderer): offer recreate-orchestrator popup when a session cannot be restored

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

* docs(spec): drop stale OpenAPI-regen note (feature adds no route)

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

* fix(ci): gofmt/goimports, golangci-lint hygiene, and Windows-aware doctor tests

Formatting: ran gofmt and goimports (with local-prefixes) on the 8 listed
files plus ptyexec/spawn_unix.go which the linter also flagged.

Lint (25 issues fixed):
- gosec G115: EncodeMessage now returns ([]byte, error) with an explicit
  bounds check before the int->uint32 conversion; all callers updated.
- govet nilness: removed dead `if lastErr == nil` branch in clientIsAlive;
  lastErr is provably non-nil at that point (real bug).
- nilerr: extracted runAcceptLoop helper so Accept-error-on-close is not
  flagged; listener close is normal shutdown, not a caller error.
- staticcheck SA4010: removed dead `full = append(...)` loop in host_test.
- revive var-declaration: `var prev int = -1` -> `prev := -1`.
- revive redefines-builtin-id: deleted local `min` helper; builtin covers it.
- unparam (2): dropped always-nil env return from attachCommand; dropped
  unused shellPath param from buildLaunchCommand; updated callers.
- errcheck (8): deferred Close/Remove calls wrapped in func(){_ = ...}();
  type assertion in host_main.go uses ok-form; fmt.Fprintf to stdout uses
  _, _ = pattern; workspace.go tmpIdx.Close() uses _ =.
- gocritic nestingReduce: inverted if+continue in runtime.go resolve loop.

Windows E2E: skip TestDoctorChecksTmuxVersion,
TestDoctorChecksTmuxVersionFailsOnError, TestDoctorWarnsWhenTmuxMissing on
windows (ao doctor emits a conpty check there, not tmux).

Verified: gofmt -l . clean, golangci-lint 0 issues, go build ok,
go test -race 1624/1624 pass.

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

* test(ci): set git identity in worktree clone fixture; loosen tmux reattach timeouts

The preserve round-trip/conflict tests commit inside a worktree of the cloned
repo, which had no git identity; CI runners cannot auto-derive one, failing with
"empty ident name". Set user.email/user.name on the clone in setupOriginClone so
its worktrees inherit it.

The tmux reattach test drives a real shell and parses stty output, which is slow
under -race on CI; raise its echo-write and SIZE-output waits.

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

* test(terminal): resend size probe on tmux reattach until the shell answers

Bumping timeouts was the wrong fix: a 30s wait still failed, so the probe output
deterministically never appeared, not slowness. onOpen signals the stream accepts
input, not that the reattached sh -i is at a prompt, so the first echo keystroke
can be dropped. Resend the probe each poll until SIZE output lands, and on timeout
dump the captured pane buffer so a remaining failure is self-explaining.

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

* test(terminal): set TERM for real-tmux attach tests so they run in CI

Root cause (from the buffer dump the prior commit added): with TERM unset on CI
runners, tmux refuses to attach a client and prints "open terminal failed:
terminal does not support clear", so the pane never runs the size probe. The
daemon defaults TERM in production; the tests bypass it. Set TERM=xterm-256color
in both real-tmux tests. Reproduced locally with `env -u TERM` (fails the same
way) and verified the fix passes under it.

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

* docs(spec): crash-proof session reconcile design

Boot-time reconcile makes live tmux + worktree state match the DB on every
daemon start, so a SIGKILL/crash/force-quit that skips SaveAndTeardownAll no
longer leaks an orphaned daemon, tmux sessions, or worktrees. Adopt
crash-surviving tmux sessions, preserve-and-terminate dead ones, reap
in-namespace orphans, and add a frontend kill+replace branch for a wedged
orphan daemon.

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

* docs(spec): simplify reconcile to per-session IsAlive, drop ListSessions

Every leak in the incident maps to a DB row, so orphan-reap is a per-session
IsAlive+Destroy over terminated rows; no runtime enumeration, no ports/conpty/
runtimeselect changes. Reaping a tmux session with no DB row is deferred (YAGNI).

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

* docs(plan): crash-proof session reconcile implementation plan

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

* feat(session): reconcile live pass (adopt alive, stash+terminate dead)

* feat(session): reconcile reap pass and Reconcile entry point

* feat(daemon): run Reconcile on boot in place of bare RestoreAll

* test(integration): reconcile terminates dead-live sessions and reaps leaked tmux

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(integration): correct misleading CreateSession comment in reconcile test

* feat(frontend): kill+replace a wedged orphan daemon on launch

When both inspectExistingDaemon and resolveDaemonFromPort return null but
a process still holds the daemon port (a crashed/orphaned daemon), spawning
a new Go child would collide on the port and exit 1. Detect this case, SIGTERM
the holder (via the run-file PID, falling back to the probe PID), poll until the
port is free (up to 8s), clear the stale run-file, then proceed to spawn fresh.
The healthy-daemon reuse path is unchanged.

Pure helper: src/shared/daemon-takeover.ts (planDaemonTakeover)
Unit tests:  src/shared/daemon-takeover.test.ts (3 tests, TDD red-green)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(frontend): fire orphan-daemon takeover when a holder actually exists

Replace planDaemonTakeover (inverted logic: ran kill block only when probe
was null) with shouldReplacePortHolder(probe, holderPidAlive) which returns
true when a real holder exists: non-null probe (rejected responder) OR a
run-file PID that is still alive (hung holder). Update main.ts call site to
compute PID liveness before gating the kill block. Update tests to cover all
three distinct outcomes non-vacuously.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* docs+test: accurate takeover comments, reconcileLive probe-error test, Reconcile doc

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(session): restore promptless orchestrators and crash-orphaned sessions

The orchestrator was abandoned on every app open: a fresh orchestrator
spawned each launch and the prior conversation appeared lost (it was not;
the transcript stays in ~/.claude, resumable by the deterministic
--session-id AO pins). Two defects combined:

1. Restore's guard rejected any session with no agentSessionId AND no
   prompt as ErrNotResumable. But Claude resumes via a deterministic
   session id regardless of those fields, so promptless orchestrators
   were perfectly resumable yet always rejected. Workers slipped through
   only because they carry a prompt. Move the resumability decision to the
   adapter: restoreArgv returns ErrNotResumable only when GetRestoreCommand
   reports it cannot resume AND there is no prompt to fresh-launch from.

2. reconcileLive marked a crash-orphaned (dead-runtime) session terminated
   without a restore marker, so RestoreAll skipped it and it stayed dead.
   It now saves-and-tears-down to the same end state a graceful shutdown
   produces (capture work, write the session_worktrees marker, terminate,
   remove the worktree), so RestoreAll relaunches it on the same boot,
   resuming history. Crash recovery now matches graceful restart. If work
   capture fails it terminates without a marker rather than risk losing
   un-preserved work.

Tests: promptless orchestrator restores via adapter resume; promptless
session with a non-resuming adapter still returns ErrNotResumable;
reconcileLive writes the marker + tears down the worktree. Full backend
suite green (1632), gofmt/vet clean.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit 9ae05735d6f06ac989857534bae2766392772c71)

* chore: format with prettier [skip ci]

* docs: scrub stale zellij references after tmux/conpty migration (#409)

PR #404 migrated the runtime adapter from Zellij to tmux (Darwin/Linux)
plus conpty (Windows), selected via runtimeselect, but ~30 stale zellij
references lingered in comments and docs describing zellij as the current
runtime. This is a comments/docs-only cleanup with no behavioral change:
comments now say tmux (or tmux/conpty when both platforms are relevant),
terminal/doc.go and docs/backend-code-structure.md are rewritten to
reflect the tmux + conpty + runtimeselect attach model, and the daemon
environment, STATUS, stack, architecture, and CLI docs are updated.

Also gitignore the local .codegraph/ and .cursor/ tooling dirs.

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

* chore: format with prettier [skip ci]

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-06-25 15:05:41 +05:30
Harshit Singh Bhandari 8fa403c480
fix(preview): add clear, reuse defaults, force refresh, local files (#379) (#380)
* fix(preview): add clear, reuse defaults, force refresh, local files (#379)

`ao preview` had four issues that made the desktop browser panel awkward
during sessions. This addresses all four:

1. No way to clear the panel. Adds `ao preview clear` (DELETE
   /sessions/{id}/preview) which empties the stored target; the panel
   loads about:blank and returns to its empty state.

2. Bare `ao preview` always autodetected index.html. It now reuses the
   session's existing preview target (so each agent/context keeps its own
   default), falling back to index.html only when nothing was previewed.

3. Re-running `ao preview <same-url>` never refreshed. The preview_url
   alone could not distinguish a real re-run from a CDC replay of an
   unrelated session update. A new monotonic preview_revision (bumped on
   every set, migration 0018, added to the sessions_cdc_update trigger)
   gives the renderer a per-command identity to key navigation on, so a
   re-run always re-navigates while unrelated updates are ignored.

4. Local files could not be previewed. `ao preview ./dist/index.html`
   (and other workspace-relative paths) now resolve server-side to the
   preview/files proxy URL when the file exists; non-file targets stay
   verbatim.

Backend, CLI, and renderer all covered by tests; OpenAPI spec and the
frontend schema are regenerated for the new DELETE route and field.

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

* fix(cdc): include previewRevision in sessions update event payload

The CDC trigger watched preview_revision changes but didn't include it
in the JSON payload, so the frontend couldn't detect same-URL preview
refreshes via SSE events. This broke the core purpose of the feature.

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

* fix(migration): renumber to 0019 to resolve conflict with main

Main branch now has migration 0018 (review_run_delivered_at), causing
a duplicate version conflict when CI merges the PR branch with main.
Renamed 0018_add_session_preview_revision.sql to 0019.

Also fixed the Down migration to properly restore the CDC trigger state
after migration 0017 (with previewUrl but without previewRevision).

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Vaibhaav <user@example.com>
2026-06-22 16:04:29 +05:30
neversettle f85b0d2ffe
Refactoring Reviews feature code and idempotency check (#377)
* fix: dedupe AO review change nudges

* fix: satisfy review delivery lint

* fix: renumber review delivery migration

* fix: gate review trigger idempotency on verdict

* fix: preserve running review trigger idempotency

* fix: fail stale running review runs

* Update review.go

* fix: avoid review context import ambiguity

* docs: clarify defensive review idempotency branch

* fix: keep review submit persistence single-sourced

* fix: remove unused review nudge send result

* fix: preserve existing pr review nudge copy

* fix: simplify merge conflict nudge return
2026-06-22 15:40:00 +05:30
Vaibhaav Tiwari c6d9692d37
feat(frontend): add live browser panel (#375)
* feat(frontend): add live browser panel

* chore: format with prettier [skip ci]

* feat: preserve and auto-open browser previews

* fix: retry browser preview after session updates

* fix: wait for browser view before preview navigation

* fix: reopen preview after session switches

* fix: preserve browser views across session switches

* chore: format with prettier [skip ci]

* feat: add `ao preview` command to drive the session browser panel

Replaces the browser panel's auto-detect with an explicit, session-scoped
command. `ao preview [url]` runs inside a session (derives the target from
AO_SESSION_ID; rejects when unset or when the session is unknown):
- with a url, opens it verbatim (file://, http, https; no sanitization for now)
- with no url, autodetects index.html in the workspace as before

The resolved target is persisted as a new `previewUrl` session field and fans
out over the existing CDC /events stream (the sessions update trigger now fires
on preview_url and carries previewUrl in its payload). The desktop browser panel
reflects session.previewUrl: it opens, switches the center pane to the browser,
and navigates, re-navigating only when the target changes.

ponytail: file:// preview targets are accepted unsanitized; agent-trusted for now.

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

* docs(cli): document the `ao preview` command

Add `ao preview` to the CLI command tables in README.md and docs/cli/README.md,
noting it resolves its session from AO_SESSION_ID and its no-arg autodetect vs
explicit-URL behavior.

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

* chore: format with prettier [skip ci]

* fix(frontend): reveal `ao preview` in the inspector Browser tab, not the center pane

`ao preview` set session.previewUrl, and SessionView surfaced it by
popping the browser into the center pane, replacing the terminal. Reveal
it in the inspector rail's Browser tab instead (opening the rail if it is
collapsed); the manual pop-out button still expands it to the center.

Lifts the inspector's active tab to an optional controlled prop so
SessionView can drive it, and adds a regression test asserting the center
pane keeps the terminal while the rail switches to Browser.

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

* docs: instruct agents to `ao preview` when showing frontend changes

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

---------

Co-authored-by: Vaibhaav <user@example.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Harshit Singh Bhandari <dev@theharshitsingh.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 04:14:18 +05:30
Adil Shaikh 8146629ad6
feat: add desktop notifications v1 (#262)
* feat: add desktop notifications v1

* fix: remove duplicate notification icon

* fix: humanize notification type labels

* Revert "fix: humanize notification type labels"

This reverts commit b6ebe6913753c863d6a1f246954f6c66e61f08b5.
2026-06-22 01:14:04 +05:30
neversettle 0e5891df91
fix(review): notify worker on changes_requested instead of relying on SCM poll (#337) (#340)
* fix(review): message worker on changes_requested instead of relying on SCM poll (#337)

review.Engine.Submit previously only persisted the verdict/body and left the
worker to learn about requested changes via the SCM poll loop, which is gated on
GitHub's reviewDecision and never reaches CHANGES_REQUESTED for self-reviews or
COMMENT-state reviews. Submit now nudges the worker's live pane directly via
ports.AgentMessenger (the same mechanism lifecycle uses) whenever the verdict is
changes_requested.

Extended flow: the reviewer reads back the GitHub review id it posted and passes
it through `ao review submit --review-id`; the id is stored on the review_run row
(new column + migration 0016) and included in the worker message so the worker
knows exactly which review to address and reply to.

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

* fix(review): mark worker nudge as AO internal review, ask to reply + resolve

Distinguish the AO internal review nudge from the external GitHub-reviewer
feedback the lifecycle SCM loop relays. For an AO review the worker is now asked,
once it has pushed its fix, to reply on the review referencing its id with what
it changed and resolve the inline review comment threads it addressed (the
reviewer posts inline comments, so the per-finding threads are resolvable via
resolveReviewThread; the top-level review object is not, hence the reply).

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

* fix(review): generalise the changes-requested worker nudge wording

Drop the "not an external GitHub PR reviewer" aside and the assumption that the
worker pushes a fix — it may resolve the feedback without code changes. The nudge
now reads "Review the feedback below and address it" and asks the worker to reply
with how it addressed the review and resolve the threads it addressed.

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

* fix(review): harden the review-id read-back against array order and empty results

The reviewer read the just-posted review id with `--jq '.[-1].id'`, which trusts
the REST API to return reviews in ascending submission order and errors when no
review exists. Review ids are monotonic, so select the highest id instead and
emit nothing when the list is empty: `--jq 'map(.id) | max // empty'`. Update the
matching `--review-id` flag help.

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

* fix(review): post the review via gh api and capture its id from that response

The reviewer must use `gh api --method POST .../reviews` to attach inline
comments anyway (`gh pr review` cannot), and that response already contains the
created review's id. Capture `.id` from that single call instead of a second
read-back, dropping the array-ordering/pagination heuristics entirely — the id is
the exact review just created.

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

* fix(review): send the review as a JSON body so inline comments are a real array

gh api -f/-F cannot build an array of objects: comments[][path] is sent as a
literal key, so the inline comments are dropped — defeating the reason for using
gh api over gh pr review. Post the review via --input JSON instead, keeping the
.id capture and the approve/COMMENT fallback.

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

* chore: format with prettier [skip ci]

* chore(review): drop accidentally committed reviewer scratch, write review out of tree

review.md was the reviewer agent's own writeup, swept onto the worker branch by a
stray `git add -A` in 5df20c9. Remove it, gitignore `/review.md` as a backstop,
and change the reviewer prompt to write its review to a temp file outside the
checkout instead of into the worktree (where it could be committed onto the
worker's branch).

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

* fix(review): message the worker before marking the run complete

If messenger.Send failed after UpdateReviewRunResult had already flipped the run
to complete, a retried `ao review submit` tripped the status='running' guard and
could never record the result. Send first; only mark the run complete once the
worker has been notified, so a failed send leaves the run retryable. A landed
message followed by a failed DB write degrades to one extra nudge on retry — the
same trade lifecycle's sendOnce makes.

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

* feat(review): accept the review body on stdin so the reviewer writes no file

`ao review submit --body -` now reads the review from stdin, and the reviewer
prompt pipes its writeup via a heredoc instead of writing a file. Previously the
reviewer wrote review.md into its checkout to pass as --body, which could be
committed onto the worker's branch (as it just was). A file path is still
accepted for backward compatibility.

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

* fix(review): always post approvals as COMMENT, drop the APPROVE attempt

The reviewer posts from the PR author's own GitHub account, so event=APPROVE
always 422s. Drop it: request changes with REQUEST_CHANGES, approve with a
COMMENT-event review whose body states it is an approval.

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

* fix(review): post every review as event=COMMENT (author can't APPROVE or REQUEST_CHANGES own PR)

The reviewer posts from the PR author's own account, where GitHub rejects both
APPROVE and REQUEST_CHANGES. Always post a COMMENT-event review and state the
verdict in the body; the machine-readable verdict still reaches AO via
`ao review submit --verdict`.

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

* fix(cli): accept underscore flag names on `ao review submit`

Reviewer agents routinely invoke the submit command with --review_id
instead of --review-id, which cobra rejected as an unknown flag and
dropped the GitHub review id from the worker notification. Normalize
underscores to hyphens on the command's flags so both spellings resolve
to the same flag.

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

* fix: sanitize review id in worker notifications

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Vaibhaav <user@example.com>
2026-06-21 19:52:46 +05:30
neversettle c53c4af8bd
test(storage): guard against duplicate goose migration version prefixes (#336)
* test(storage): guard against duplicate goose migration version prefixes

Statically scans embedded migration filenames for repeated numeric
version prefixes and fails with a clear message, catching the class
of bug from #333 (two PRs adding 0014_*.sql independently) before
goose.Up() would panic at runtime.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(storage): dedupe migration versions by goose's parsed int64, not raw string

versionPrefix compared raw filename prefixes, so 014_x.sql and 0014_y.sql
were treated as distinct even though goose.NumericComponent parses both
to version 14 and panics on the collision. Use goose.NumericComponent
directly so the test enforces goose's actual collision rule.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(storage): trim PR-specific framing from migration version test comment

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-20 12:49:59 +05:30
Khushi Diwan f98c5e56cc
fix(storage): renumber telemetry migration to 0015 to resolve 0014 version collision (#334)
* fix(storage): renumber telemetry migration to 0015 to resolve goose version collision

* chore: format with prettier [skip ci]

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-06-20 11:23:13 +05:30
Vaibhaav Tiwari 7037f4aa4e
fix: surface missing reviewer harness (#318)
* fix: surface missing reviewer harness

* fix: satisfy review lint

---------

Co-authored-by: Vaibhaav <user@example.com>
2026-06-20 10:19:50 +05:30
Laxman 43ee6c9b02
Feat/backend telemetry v0 (#307)
* feat(backend): add telemetry event plumbing

* feat(backend): emit session telemetry events

* feat(backend): add http and cli telemetry export

* feat(backend): add onboarding and dwell telemetry

* feat(frontend): add renderer posthog telemetry

* feat(frontend): bundle posthog project defaults

* feat(telemetry): add canonical active-user event

* fix(telemetry): repair cli test expectations

* fix(telemetry): sanitize remote payloads and respect event toggles
2026-06-18 22:00:25 +05:30
Harshit Singh Bhandari 5fff98087a
feat(import): rewrite-side legacy → rewrite first-boot import (#314)
* feat(import): rewrite-side legacy → rewrite first-boot import

Port the legacy-side TS reader (AgentWrapper #2144/#2129) to Go and run the
migration inside the rewrite as an opt-in import, per the FINAL v2 plan. Reads
the legacy flat-file store (~/.agent-orchestrator) read-only and writes the
rewrite's own SQLite DB via the native storage layer; legacy files are never
touched, and a re-run skips existing rows, so a declined or failed import loses
nothing.

What's included:
- internal/legacyimport: Go reader + field mappers (issue #247). Lifecycle
  double-decode (lifecycle key or statePayload+stateVersion:"2"),
  role/orchestrator detection, sessionPrefix fallback (first 12 chars of id),
  8→4 activity-state map, per-harness resume-id selection, permission/harness
  remap, and the claude transcript slug + relocation to the rewrite's
  orchestrator worktree path ({DataDir}/worktrees/{id}/orchestrator/{prefix}-orchestrator).
- store.ImportSession: verbatim session insert (explicit id/num, ON CONFLICT
  DO NOTHING) so the orchestrator lands at id "{prefix}-orchestrator", num 0.
- `ao import`: explicit, idempotent import with --from/--dry-run/--yes/--json.
  Refuses while a live daemon owns the run-file (the daemon is sole writer; the
  import runs offline, matching the #2129 reference).
- First-boot opt-in: `ao start` offers the import before launching the daemon
  when legacy data is present and the rewrite DB has no projects yet. Declining
  or any failure is non-fatal; a non-interactive boot prints a hint instead of
  auto-importing.

Scope (gist §6): all projects + per-project settings, and the single
non-terminated orchestrator session per project (claude-code/codex/opencode;
aider skipped with a note). Workers are not imported (they respawn fresh).

Resume-id mapping (#247 §2.2): agent_session_id carries claudeSessionUuid /
codexThreadId / opencodeSessionId by harness. codexModel and
restoreFallbackReason have no rewrite column, so they are dropped and surfaced
as import notes — codex resumes from the thread id alone, the rest is forensic.

Gate: `go build ./... && go test -race ./...` green (1423 tests).

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

* fix(import): resolve golangci-lint errcheck/gocritic/nilerr findings

- start.go: check fmt.Fprint* returns in the first-boot import path
- project.go: combine same-typed return params (gocritic paramTypeCombine)
- claude.go: use a pathExists helper so a missing transcript source is a normal
  skip, not an err-then-return-nil (nilerr)
- importer.go: fold best-effort transcript relocation into a switch so the
  non-fatal path no longer returns nil from an error branch (nilerr)

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

* fix(import): resolve transcript dest path like the daemon; harden lifecycle parse

Code-review follow-ups on the legacy importer:

- claude.go: compute the Claude transcript DESTINATION slug from the
  symlink-resolved orchestrator worktree path (new resolvePhysical, mirroring
  gitworktree.physicalAbs), not the literal path. The daemon resolves that cwd
  through physicalAbs before `claude --resume` runs, so a literal-path slug
  missed the resume bucket whenever any component of AO_DATA_DIR was a symlink
  (custom data dir, macOS /tmp→/private/tmp, symlinked $HOME) — the orchestrator
  would have resumed without its prior context. Source slug now uses the same
  resolver for symmetry.
- orchestrator.go: accept a numeric stateVersion (JSON 2 → float64) as well as
  the string "2" when falling back to statePayload, so a V2 record carried only
  in statePayload is not misparsed as stateless.
- orchestrator.go: build the dropped-resume-metadata note as a joined list
  instead of string concatenation.

Tests: added a symlinked-data-dir dest-slug test and a numeric-stateVersion
fallback test. Gate green: `go build ./... && go test -race ./...` (1425).

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 19:54:51 +05:30
Pritom Mazumdar 3986d488c8
feat(session): support multiple PRs per session (#230)
* feat(session): support multiple PRs per session

A session can now own several pull requests (a root plus stacked
children) instead of being capped at one. The SQLite schema was already
1-session->many-PR (pr.url PK, session_id a plain FK), so this is a
behavioural change across the observe -> persist -> derive -> react
pipeline, not a migration.

- observe: the SCM observer discovers every open PR whose source branch
  matches a session branch or descends from it ("branch/..." stacking),
  attributing each to the owning session; the longest matching branch
  wins so a child session claims its own stacked PRs.
- derive: session status is a worst-wins aggregate over all owned PRs,
  with a stack model (B is a child of A iff B.target == A.source and A is
  open) exposed via prs[] on every session read DTO.
- react: per-PR reactions; a stacked child blocked by an open parent is
  exempt from the rebase/merge-conflict nudge (only the bottom of the
  stack is eligible), and the session completes only when no PR is open
  and at least one merged.
- tests: unit coverage across stack/status/observer/lifecycle, a
  real-SQLite ListPRFactsForSession test for the stacked-PR read path,
  and a functional end-to-end integration test driving the real store +
  lifecycle + observer through attribution, completion, and stacked-child
  nudge suppression.

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

* fix(scm): ignore fork heads in PR attribution and persist discovered siblings before completion

Branch-prefix attribution now requires a discovered PR's head branch to live
in the project repo. A fork PR can reuse a session's branch name while its
commits live in the fork, so the previous code could auto-claim foreign work.
Carry head repo full_name from the REST list response and skip any PR whose
head repo is not the base repo.

discoverNewPRs also writes each newly discovered PR as an open baseline row
before the refresh/lifecycle pass runs. A session can own several PRs, and a
terminal observation triggers a completion check that reads all of the
session's PRs from the store. Without the early write, an open sibling found
in the same poll was not yet durable and the session could terminate while
that PR was still open.

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

* fix(session): surface actionable signals from blocked stacked children; clarify worker prompt

Status aggregation previously dropped any open PR blocked by an open parent,
hiding actionable child signals (failing CI, draft, requested changes,
unresolved comments) behind the parent's status. A blocked child still cannot
merge, so its readiness signals (mergeable/approved/review-pending/open) stay
suppressed, but its problem signals now contribute to the worst-wins aggregate.
The all-blocked fallback is preserved so a session never goes dark.

The worker multi-PR prompt said independent PRs could branch off the base
branch as usual, which conflicts with branch-prefix attribution. Clarify that
a PR may target the base branch, but its source branch must stay under the
session branch namespace for AO to track it.

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

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-17 17:31:28 +05:30
Khushi Diwan 96d1649e46
fix(review): serialize concurrent triggers per worker to stop reviewer double-spawn (#246)
* fix(review): serialize concurrent triggers per worker to stop double-spawn

Engine.Trigger was a read-then-write (idempotency check -> reviewer spawn ->
InsertReviewRun) with no serialization and no backing constraint. Two near-
simultaneous triggers for the same worker at the same head SHA both passed the
GetReviewRunBySessionAndSHA check, both spawned a reviewer against the same
deterministic review-<id> handle, and both inserted a running run for one commit.

Add a per-worker keyed mutex (lockWorker) held across the whole Trigger body, so
the loser re-reads the freshly-recorded run and short-circuits to Created:false
instead of spawning. Back it with a partial unique index on
review_run(session_id, target_sha) (migration 0013) as a cross-restart safety
net; rows with an empty target_sha (head not yet observed) are excluded so they
are not blocked.

Adds a concurrency test asserting N simultaneous triggers spawn once and record
one run.

Closes #242

* fix(review): make migration 0013 dedup-safe and handle the unique conflict in Trigger

Pre-#242 daemons can already hold duplicate (session_id, target_sha)
review_run rows, on which CREATE UNIQUE INDEX fails and wedges startup.
Migration 0013 now collapses each duplicate group to a single survivor
(a completed pass over a still-running one, then newest by created_at)
before building the index.

Trigger now treats a unique-constraint hit as a fallback rather than an
error: InsertReviewRun maps it to the new domain.ErrDuplicateReviewRun
sentinel, and Trigger re-reads GetReviewRunBySessionAndSHA and returns
that run with Created:false instead of surfacing a raw error after the
reviewer may already have launched.
2026-06-17 00:43:10 +05:30
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
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
Harshit Singh Bhandari 5244015802
feat: add workspace project registration foundation (#165)
* feat: add workspace project registration schema

* fix: satisfy workspace registration lint

* fix: harden workspace registration edge paths

- Reject linked-worktree and bare parents via validateWorkspaceParent before any mutation
- Roll back git init/.gitignore on failure in initWorkspaceParent so retries are clean
- Reject child repos named __root__ (reserved PK in session_worktrees)
- Serialise Service.Add with addMu to eliminate TOCTOU on concurrent same-path calls
- Fix ensureWorkspaceGitignore permission 0o600 -> 0o644
- Improve guardNoGitlinks suggestedFix with actionable git rm --cached guidance
- Remove dead CASE/__root__ ordering from ListWorkspaceRepos SQL (regenerated via sqlc)
- Resolve RepoOriginURL once per code path in Add (workspace vs single-repo)
- Add 7 tests covering the new edge paths
2026-06-10 16:10:14 +05:30
neversettle 7698c24931
feat(config): persist per-project agent config and resolve it at spawn (#154)
* feat(config): persist per-project agent config and resolve it at spawn

Each project can now carry its own agent config (model, permissions,
adapter-specific keys) that survives daemon restart and is resolved into
the launch command when a session spawns.

- storage: add nullable projects.agent_config JSON column (migration 0008);
  marshal/unmarshal in the store so the domain carries map[string]any
- resolution: session manager loads the project row and populates
  LaunchConfig.Config before GetLaunchCommand
- validation: claude-code declares a ConfigSpec (model, permissions) and
  rejects unknown keys / bad types / bad enums at spawn; it applies the
  model override and config-driven permission mode (explicit Permissions
  still wins)
- surface: PUT /projects/{id}/agent-config + `ao project set-config`
  (--set/--config-json/--clear), config shown in `ao project get`

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

* fix(claudecode): validate string-list/required config keys and unhandled types

Address review on per-project agent config validation:
- handle ConfigFieldStringList (list of strings) explicitly
- reject unhandled ConfigFieldType via a default case rather than
  silently passing
- enforce Required fields are present

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

* refactor(config): make per-project agent config a typed struct

Replace the free-form map[string]any agent config with a typed
domain.AgentConfig{Model, Permissions} so values are validated when set
(CLI/API) instead of silently dropped at spawn, and the OpenAPI/TS schema
and UI get real typed fields.

- domain: AgentConfig struct + Validate(); PermissionMode moves to domain
  and ports re-exports it as a type alias (zero adapter churn)
- storage: marshal/unmarshal the typed struct (IsZero → SQL NULL)
- service: validate on Add and SetAgentConfig; read-model exposes a typed
  *AgentConfig
- claudecode: read typed cfg.Config.Model/.Permissions; drop the
  map/spec-based validateConfig in favor of the typed Validate()
- cli: typed `ao project set-config --model/--permission/--clear`
- docs: add docs/design/per-project-config.md blueprint sequencing the
  remaining # Projects fields toward fully typed per-project config

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

* feat(config): full typed per-project ProjectConfig (store, resolve, surface)

Expand per-project config from agentConfig-only to the full legacy
`projects.<id>` surface, modeled as one typed domain.ProjectConfig
persisted in a single projects.config JSON column.

Wired end-to-end at spawn:
- defaultBranch  → base branch for the session worktree (ports.WorkspaceConfig.BaseBranch)
- env            → merged into the runtime env (AO-internal vars still win)
- symlinks       → repo files linked into the workspace
- postCreate     → commands run in the workspace (OS-agnostic shell)
- agentRules / agentRulesFile / orchestratorRules → merged into the prompt
- worker/orchestrator role overrides → harness + agent-config resolution

Stored + validated + surfaced now, consumption deferred (no consumer yet):
tracker, scm(+webhook), opencodeIssueSessionStrategy; sessionPrefix feeds
the display prefix only (session-id generation unchanged).

Validation lives on domain.ProjectConfig.Validate() and runs when config is
set (CLI/API). PermissionMode/AgentConfig stay typed; harness names validated
via domain.AgentHarness.IsKnown().

Surface: PUT /projects/{id}/config (replaces /agent-config) + typed
`ao project set-config` flags (--default-branch/--env/--symlink/--post-create/
--agent-rules/--worker-agent/… or --config-json). OpenAPI + TS regenerated.

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

* fix(lint): tighten symlink dir perms to 0o750 (gosec G301)

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

* feat(config): centralize default project config + tests

Add domain.DefaultProjectConfig / ProjectConfig.WithDefaults with a single
DefaultBranchName ("main") source of truth, replacing the literal "main"
scattered in the read-model and the gitworktree adapter. Unconfigured
projects now resolve the default branch through one path; every other field
defaults to its zero value.

Tests: defaults present for all fields (DefaultProjectConfig/WithDefaults),
and an unconfigured project reports the default branch + derived session
prefix while omitting the empty config object.

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

* feat(config): encode documented defaults (branch=main, tracker=github)

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

* fix(config): fail-safe paths for missing/corrupt per-project config

Address review on default-config / fail-safe spawning:
- projectRules: a missing AgentRulesFile is optional context, skipped
  rather than aborting every spawn (only a real read error surfaces)
- store: a corrupt config JSON column degrades to a zero config instead
  of failing GetProject/ListProjects/FindProjectByPath for that row
- restore: re-apply the project's resolved AgentConfig so a configured
  model/permissions carry across a restore (matches fresh spawn)

Tests: missing rules file skips, corrupt config degrades to zero, restore
applies the project agent config.

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

* refactor(config): trim per-project config to consumer-backed fields

Drop config that has no live consumer yet, so this PR lands only the
fields actually read at spawn/display:

- Remove prompt rules (agentRules, agentRulesFile, orchestratorRules)
  from ProjectConfig. Project/agent instructions belong on the system
  prompt path or repo-local AGENTS.md, not another rules family.
- Remove future-only integration config with no consumer: tracker, scm,
  scm.webhook, and opencodeIssueSessionStrategy (plus their types,
  constants, the github tracker default, CLI flags, and spec schemas).
  These return in focused PRs alongside the code that reads them.

Kept: defaultBranch, sessionPrefix, env, symlinks, postCreate,
agentConfig (model/permissions), and worker/orchestrator role
overrides. Cross-agent model/permissions support stays follow-up (#157).

Regenerated openapi.yaml + frontend schema.ts.

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

* fix(config): reject unknown config JSON keys; confine symlink paths

Two review hardenings on the now-trimmed per-project config surface:

- Project add/set-config endpoints decode with DisallowUnknownFields, so
  a misspelled or removed config field surfaces as a clear 400 instead
  of being silently dropped. Locks the removals from e213b68 (and any
  future trims) at the API gate. Covered by new controllers test.
- applySymlinks now refuses absolute paths and any ".." segment via a
  safeRelPath guard, so a project config cannot escape the project or
  workspace tree via a malicious symlinks entry. Covered by new
  session_manager test.

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

* fix(config): reject symlink path traversal at config write time

greptile flagged ProjectConfig.Symlinks as a write-time path-traversal
gap on PR #154 — the runtime guard in applySymlinks catches a malicious
entry on every spawn, but the config itself accepted it. Move the check
into ProjectConfig.Validate so a bad symlinks entry surfaces as
INVALID_PROJECT_CONFIG when set (CLI/API) instead of silently sitting in
the row until the next spawn. The runtime guard stays as
defense-in-depth.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: harshitsinghbhandari <24b4506@iitb.ac.in>
2026-06-08 21:35:29 +05:30
Harshit Singh Bhandari c343c55c14
fix: 7 bugs from discussion #149 smoke walk (envelope, spawn, CDC, observer) (#153)
* 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.
2026-06-07 07:35:46 +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
Harshit Singh Bhandari bab0d2d167
feat: add light backend CLI commands (#98) 2026-06-03 16:18:00 +05:30
neversettle 3a93e33331
refactor: move project manager to service layer (#68)
* refactor(project): manager talks to the sqlite store; drop the in-memory store

The project Manager now runs only against the durable backend store: remove the
process-local MemoryStore (and NewMemoryManager), and require a real Store. The
daemon already wires the sqlite store; tests now build a real temp-dir sqlite
store instead of the mock.

- Move Row + the Store port to project/store.go. The Store interface stays
  because it is the dependency-inversion port that lets the manager reach the
  backend without an import cycle (storage imports project.Row), not an extra
  mock layer — there is no longer any in-memory implementation.
- NewManager requires a non-nil Store (no in-memory fallback).
- Add project/manager_test.go: List/Add/Get/Remove happy paths +
  PATH_REQUIRED/NOT_A_GIT_REPO/PATH_ALREADY_REGISTERED/ID_ALREADY_REGISTERED,
  PROJECT_NOT_FOUND/INVALID_PROJECT_ID, and UpdateConfig — all against a real
  sqlite store (the service-logic tests #47 lacked).

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

* refactor(project): trim routes, consolidate package, add code-first OpenAPI

- Remove POST /reload, PATCH /{id}, POST /{id}/repair routes and their
  Manager methods (Reload, UpdateConfig, Repair) and DTOs (ReloadResult,
  UpdateConfigInput) — not needed at this stage
- Merge Manager interface into manager.go; delete project.go (single-impl
  split served no purpose)
- Remove dead notImplemented helper from errors.go
- Port PR #59 code-first OpenAPI generation: controllers/dto.go named
  response types, specgen/build.go (4 routes), parity + drift tests,
  cmd/genspec, go generate wiring; regenerate openapi.yaml
- Add swaggest deps; add YAML() method to apispec.Spec

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>

* fix(project): address PR review comments

- t.Skipf → t.Fatalf in gitRepo helper: git failures now hard-fail
  instead of silently skipping manager tests on a misconfigured runner
- FindProjectByPath: add AND archived_at IS NULL so archived paths don't
  permanently block re-registration (update queries/projects.sql and
  generated gen/projects.sql.go)
- Add TestManager_ReaddAfterRemove to lock the fix

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>

* fixed lint and fmt

* addressed greptile comments

* Apply suggestions from code review

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* project tests fix

* project_tests fix

* fix: Linting and formatting fix

* refactor: move project manager into service layer (#68)

* refactor: split service package by resource (#68)

* fix: ignore archived project id conflicts (#68)

* refactor: move pr manager into service layer (#68)

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: itrytoohard <ayetrytoohard@gmail.com>
2026-06-02 01:26:48 +05:30
prateek c8f6050539
refactor: remove activity source tracking (#62) (#66)
Co-authored-by: itrytoohard <ayetrytoohard@gmail.com>
2026-06-01 09:26:18 +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 cb2a00a0c2
Revert "feat: add notifier delivery runtime" 2026-06-01 04:46:38 +05:30
Adil Shaikh d06c0ce8c2
Merge pull request #58 from aoagents/feat/55
feat: add notifier delivery runtime
2026-06-01 04:26:41 +05:30
whoisasx f0c57ac2e2 docs: clarify notification routing migration 2026-06-01 03:47:53 +05:30
whoisasx d39e8e0da3 fix: address notifier review cleanup 2026-06-01 03:47:44 +05:30
whoisasx 041c8c8f7f fix: harden notification delivery leases 2026-06-01 03:47:39 +05:30
whoisasx d4622fe223 fix: address notifier delivery review feedback 2026-06-01 03:47:39 +05:30
whoisasx 217f6b1652 feat: add notifier delivery runtime 2026-06-01 03:47:19 +05:30
prateek 42eab57d49 refactor(storage): add compile-time port guards on *Store
Re-add the blank-identifier interface assertions lost when wiring.Adapter was
collapsed: *Store now directly satisfies ports.SessionStore and ports.PRWriter,
so prove it at the point of definition. Drift between either port and the
implementation now fails here instead of at the call sites in lifecycle_wiring
or tests.

Addresses greptile review comment on #60.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 03:40:33 +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
whoisasx 5303c51d29 feat: add durable notification foundation 2026-06-01 00:07:55 +05:30
Harshit Singh Bhandari f8611decc0
Merge pull request #52 from aoagents/feat/wire-session-manager
feat(backend): wire Session Manager into the daemon (real tmux + gitworktree, stub Agent)
2026-05-31 23:48:15 +05:30
harshitsinghbhandari 27fb82dbeb feat(backend): wire Session Manager into the daemon (real tmux + gitworktree, stub Agent)
Constructs a live *session.Manager in main alongside the LCM, sharing the
exact same SessionStore + LCM dependencies the lifecycle stack already
holds.

Refactor: storeAdapter moves from package main to a new internal
package, wiring.Adapter, so the daemon's composition root and any
in-process integration tests can share a single bridge.

Stubbed for now: ports.Agent has no production adapter on main; a loud
*noopAgent returns sentinel AO_AGENT_HARNESS_NOT_WIRED and logs a
warning once on first call, so a future Spawn through this lane fails
at the runtime layer with a clear breadcrumb rather than starting a
broken session quietly. ports.Notifier and ports.AgentMessenger remain
stubbed alongside the LCM.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 20:12:38 +05:30
harshitsinghbhandari ad1c4dacec test(integration): LCM+SM live-fire against real SQLite store
Adds backend/internal/integration with five end-to-end tests that hydrate the
real lifecycle.Manager + session.Manager against a tmp SQLite store and
exercise the full pipeline through the DB triggers and the CDC poller:

- TestHappyPath_Spawn_PR_Kill — spawn -> SCM PR observation (open + CI
  passing) -> kill; asserts canonical row, pr row, and change_log event
  types (session_created/_updated, pr_created, pr_check_recorded).
- TestRestoreRoundTrip_PreservesMetadata — spawn, kill, close store, reopen
  same DB path, hydrate fresh LCM/SM, Restore(); asserts AgentSessionID and
  the rest of SessionMetadata survive across the daemon restart.
- TestCIFailureAndRecovery_NudgeThenClears — failing CI observation drives
  the CI-failed reaction nudge with the log tail injected; passing CI
  observation switches to approved-and-green human notify; pr_checks history
  reads back the failure (the brake's source of truth).
- TestDetectingPersistsAcrossRestart — failed probe parks the session in
  detecting with detecting_* columns populated, round-trips across a
  close/reopen, alive probe clears the quarantine memory.
- TestCDCPollerReceivesAllStages — drives the real cdc.Poller; asserts the
  trigger pipeline emits each expected event_type and seq is monotonic.

Wiring gap fixed (minimal): goose v3 keeps baseFS/logger/dialect as
package-level globals, so two concurrent sqlite.Open() calls — uncommon in
production but normal under -race with t.Parallel() — race on
goose.SetBaseFS/SetLogger/SetDialect inside migrate(). Added a process-level
sync.Mutex around the migrate() call. ~11 lines, no signature changes.

Scope notes (the task brief assumed a fancier architecture than what
actually shipped in PR #37):
- No outbox / consumer_offsets / janitor exist on main — the change_log
  table IS the durable, ordered source of truth (see cdc/event.go), so the
  brief's janitor-watermark step is skipped.
- No reaction_trackers table / ReactionStore port — trackers are in-memory
  per lifecycle/reactions.go; persistence-round-trip there is N/A.
- No revision column / Upsert(rec, eventType) — write-mutex serialises and
  change_log.seq orders, so the assertions land on event_type + seq, not on
  a per-row revision counter.

All 219 tests pass under -race across 18 packages. lifecycle/fakes_test.go
is untouched; existing unit tests still drive the in-memory fake.
2026-05-31 18:34:51 +05:30
prateek 70aab5eb26 feat(backend): atomic PR-observation write + CDC on check status updates
Addresses review on PR-observation persistence:

- pr_checks now has an AFTER UPDATE CDC trigger (guarded on status change), so a
  check flipping in_progress->failed on the same commit emits change_log instead
  of updating silently. Restores symmetry with the sessions/pr triggers.
- writePR persists scalar facts + checks + comments in ONE transaction via
  Store.WritePRObservation, so a mid-write failure can't leave the pr row (and
  its CDC event) committed while checks/comments are partial. Collapses the
  PRWriter port's three write methods into one WritePR.
- db.go: record why modernc.org/sqlite (pure-Go, CGO-free static binary) at the
  import site.

Regression tests for both the update-trigger (emit on change, suppress no-op
re-poll) and the transactional write. go test -race ./... green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 17:02:47 +05:30
prateek e5c4fd6ffd feat(storage,cdc): minimal 6-table schema + trigger-driven CDC (storage layer)
Reworks the storage + CDC layer to the simplified design agreed in review:

Schema (one clean migration, 0001): projects, sessions, pr, pr_checks,
pr_comment, change_log. sessions.id is a single string key "{project}-{num}"
(mer-1); operational metadata folded into sessions; is_alive replaces the
runtime axis; no revision (the per-session write mutex serializes, change_log.seq
orders). pr keyed by URL (1 session : many PRs). pr_checks is CI run history
(one row per check per commit) — the CI-fix-loop brake is a LIMIT 3 query, no
counter stored. change_log carries a required project_id FK + nullable session_id.

CDC is DB-native: AFTER INSERT/UPDATE triggers on sessions/pr/pr_checks append
to change_log atomically with the change (json_object payloads). The old durable
outbox/JSONL/janitor pipeline is gone; the cdc package is now a Poller that reads
change_log and fans events out through the in-memory Broadcaster (hardened with
recover()). Clients catch up via the log from their own offset (SSE Last-Event-ID).

Storage uses a single writer connection + a reader pool (read-your-writes for the
triggers' subqueries; concurrent reads). sqlc-generated typed queries.

Tests (-race): CRUD, per-project id assignment, the loop-brake query, concurrent
creates, triggers populating change_log; CDC end-to-end through the real store,
concurrent goroutine delivery, broadcaster panic-isolation.

NOTE: scoped to storage + CDC. The lifecycle-engine consumers (decide, lifecycle,
session, reaper, main wiring) still reference the old domain axes and need a
follow-up integration pass to compile against the new model.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 05:42:59 +05:30
prateek ba47212802 perf(storage): allow concurrent reads; serialize writes via a mutex
SetMaxOpenConns(1) forced every read (List/Get/GetPR/...) to queue behind
the single connection, so the dashboard's reads contended with the LCM's
writes. WAL already supports many concurrent readers, so raise the pool to 8
and instead serialize *writes* with a Store.writeMu. That keeps WAL's
single-writer rule and the revision-CAS read-then-write atomic regardless of
pool size, while reads now run in parallel across the pool.

Every write method takes writeMu (Upsert, PatchMetadata, UpsertPR/DeletePR,
the pr_check/pr_comment Replace* via inTx, the CDC outbox/offset writes,
project writes, reaction-tracker writes); reads take nothing. Added
TestConcurrentReadsAndWrites (16 writers + 16 readers) which passes under
-race.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 00:51:18 +05:30
prateek 4ce90448e2 refactor(storage): make session metadata + PR facts typed and structured
The first storage cut modelled two side tables as free-form blobs. This
replaces both with opinionated, statically-typed schema so what a session
can carry is fixed by the schema, not by convention.

session_metadata: was a (session_id, key, value) KV bag with six
convention-only keys. Now a 1:1 table of named, typed columns. The domain
currency is a typed domain.SessionMetadata struct (was map[string]string),
threaded through ports.LifecycleStore, the LCM, the Session Manager and the
reaper, so an unknown key is a compile error rather than a silently-dropped
write. PatchMetadata keeps its non-destructive merge ("empty = leave
unchanged"). The off-canonical invariant is now enforced at the type level
via json:"-" on SessionRecord.Metadata, removing the manual `Metadata = nil`
scrub the change_log/snapshot paths had to remember; the Meta* string-key
constants are deleted.

pr_enrichment -> pr (+ pr_check, pr_comment): the scalar facts are now
typed columns with CHECK-constrained enums (review_decision, mergeability,
ci_state) and integer CI counts instead of opaque TEXT. The two list facts
the old `pending_comments`/ci_summary strings smuggled are normalized into
child tables (pr_check, pr_comment) that cascade from pr. The store exposes
UpsertPR/GetPR plus atomic ReplacePRChecks/ReplacePRComments + List.

Both tables remain off the canonical CDC path. sqlc regenerated; migrations
0001/0002 revised in place (nothing released). gofmt/vet clean; go test
-race green; daemon smoke-boots and creates the new schema.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 00:33:13 +05:30
Pritom14 23b8fe43cf feat(backend): add projects and pr_enrichment tables to SQLite store
Migration 0002 adds two tables off the canonical CDC path:

- projects: durable registry of managed repos (the twin of the old YAML
  config). Soft-deletable via archived_at so a session's project_id always
  resolves; ListProjects returns active rows only, GetProject resolves any.
- pr_enrichment: per-session cache of rich SCM facts (CI summary, review
  decision, mergeability, pending comments, CI log tail) that do not live
  in the canonical lifecycle. 1:1 with a session, cascades on session delete.

Both are written outside the LCM write path: no revision bump, no
change_log/outbox event. Store methods mirror the reaction_trackers adapter
pattern with storage-local row structs.
2026-05-30 21:53:14 +05:30