Commit Graph

18 Commits

Author SHA1 Message Date
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
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 6f8112e7b9
feat: surface SCM summaries in desktop (#263)
* feat: surface scm summaries in desktop

* chore: format with prettier [skip ci]

* fix: hydrate PR views from session facts

* chore: format with prettier [skip ci]

* fix: document PR summary DTOs

* fix: clean pr summary attention details

* chore: format with prettier [skip ci]

* fix: default codex sessions to bypass approvals

* fix: refine pr inspector summary

* chore: format with prettier [skip ci]

* fix: color pr diff metadata

* chore: format with prettier [skip ci]

* fix: move diff icon to file count

* fix: keep shell topbar on board routes

* fix: render all session prs on board

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-06-22 00:35:00 +05:30
Laxman 0e1c5fe54b
Feat/posthog error tracking redaction (#329)
* feat(telemetry): send renderer exceptions to PostHog error tracking

* fix(telemetry): redact local paths and urls before capture

* feat(telemetry): enrich backend error metadata

* fix(telemetry): document telemetry helper exports

* fix(telemetry): satisfy gocritic on helper signature

* chore: format with prettier [skip ci]

* fix(telemetry): redact embedded local urls

* chore: format with prettier [skip ci]

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-06-21 18:50:47 +05:30
Adil Shaikh 72b757b203
feat: require explicit project agents (#355)
* feat: require explicit project agents

* chore: format with prettier [skip ci]

* test: configure integration project agents

* fix: center project agent selection dialog

* fix: keep agent selects neutral before validation

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-06-21 06:07:53 +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
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 9e84a3b5ae
fix(spawn): map invalid branch name to 400 instead of opaque 500 (#213)
validateBranch returned an untyped error for a name rejected by
git check-ref-format, so toAPIError fell through to INTERNAL_ERROR 500.

Add a ports.ErrWorkspaceBranchInvalid sentinel (mirroring the not-fetched /
checked-out-elsewhere ones), wrap it in validateBranch, and map it to
INVALID_BRANCH (400). Completes the residual of #152 Bug 3, which typed the
not-fetched and checked-out-elsewhere cases but left the invalid-format case
collapsing to 500.

Closes #212
2026-06-15 14:50:58 +05:30
Khushi Diwan cbf3f0a2db
fix(spawn): reject unknown harness with 400 instead of opaque 500 (#211)
An unknown --harness was only caught at the agent-registry lookup deep in
Spawn, after the seed row and worktree were already created: the untyped
error collapsed to INTERNAL_ERROR 500 and left a terminated orphan row.

Validate the harness against the registry before any durable state is
created and return a typed ErrUnknownHarness mapped to UNKNOWN_HARNESS (400).
Sibling to #152 Bug 6 (unknown binary on PATH), which did not cover a harness
with no registered adapter.

Closes #210
2026-06-15 14:50:53 +05:30
yyovil f7df36bb8b
Split: backend runtime/session updates + frontend shell refactor (#222)
* feat(frontend): rebuild Electron desktop UI as a React + Vite renderer

Replaces the skeleton Electron frontend with a full React 19 + TypeScript
renderer (Vite, electron-forge, contextBridge preload), plus the backend
additions it needs.

Renderer:
- TanStack Query + EventTransport (CDC SSE on /api/v1/events)
- TanStack Router file-system routing (hash history for the file:// origin)
- Tailwind + shadcn/ui, react-resizable-panels, Zustand UI state
- @xterm/xterm per-session PTY over /mux WebSocket + WebGL addon
- openapi-typescript + openapi-fetch types off openapi.yaml
- electron-forge packaging + update-electron-app auto-updater
- Vitest + RTL · Playwright

Backend:
- cors.go — allowlist-only CORS, handles Private Network Access preflight
  for app:// renderer -> loopback daemon
- session.TerminalHandleID exposed in domain + OpenAPI spec
- project.Path added to OpenAPI spec, service, store, and tests

DESIGN.md documents the emdash-matched dark UI (tokens, blue accent, status
glyph spec, orchestrator-led layout).

Co-authored-by: Ashish Huddar <ashish.hudar@gmail.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: b1e334c1e54a

* feat(terminal): port yyork's terminal rendering architecture

XtermTerminal becomes a self-contained, dependency-free renderer component
(yyork's xterm-terminal.tsx pattern):
- Nothing writes into the buffer at mount — status/empty-state is DOM chrome.
  Fixes the startup crash (xterm Viewport.syncScrollArea reading renderer
  dimensions on a zero-sized panel).
- Multi-trigger fit (rAF + 50/250ms settle + fonts.ready + ResizeObserver):
  FitAddon must re-measure after monospace font metrics settle or it
  over-counts columns. xterm only fires onResize on real grid changes, so
  repeated fits don't spam the PTY.
- Unicode11 width (agent CLIs print emoji/wide glyphs), WCAG-AA minimum
  contrast, WebGL→canvas renderer fallback, full ANSI-16 palette per DESIGN.md.

TerminalPane keeps ONE terminal instance across session switches — the
attachment effect re-points the mux and RIS-resets the screen instead of
remounting (a keyed remount drops the warm GPU surface mid-switch).

useTerminalSession: resize debounced 100ms trailing (one SIGWINCH per pane
drag, not dozens); the "Attaching…" writeln is gone (banner chrome covers it).

Test infra: vitest never loaded vite.renderer.config.ts after the forge split
(it only auto-discovers vite.config.*) so the whole suite ran without jsdom.
Point the test script at the config and type it via vitest/config. Fix
pre-existing type errors (notarytool field, maker-zip config, named
updateElectronApp import). 97/97 passing, typecheck clean.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Entire-Checkpoint: eb0f94fcf914

* fix(spawn): don't send base branch; surface real API errors

Two bugs found spawning a worker from the modal:

1. "Based on: main" sent branch:"main" in the POST, but git can't add a
   second worktree on a branch already checked out (main lives in the repo
   root) — the daemon returns 409 BRANCH_CHECKED_OUT_ELSEWHERE. The base
   branch must be OMITTED so the daemon mints a fresh ao/<sessionId> off the
   project default. Only a non-default branch (resume an existing session
   branch) is sent through.

2. The daemon's error body is {error,code,message,requestId}; App.createTask
   did String(error) on it → the modal showed "[object Object]". Add
   apiErrorMessage() to unpack message/error from the structured body, with
   Error/string fallbacks.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Entire-Checkpoint: d3e718d393cd

* feat(terminal): Nerd Font glyph support via --font-mono (yyork pattern)

The terminal now resolves its fontFamily from the --font-mono CSS token
(styles.css @theme), which leads with the Nerd Font family stack
(JetBrainsMono Nerd Font Mono first). Agent TUIs get powerline separators
and file-type icons; box-drawing stays renderer-rasterized.

Mirrors yyork exactly: no font is bundled — the stack names system-installed
Nerd Fonts and the browser picks the first present, falling back to plain
monospace (no icon glyphs) when none are installed.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Entire-Checkpoint: 28120fa527a7

* chore(frontend): set up shadcn/ui foundation

Prep for the route-parity port: build new screens from shadcn primitives.
- components.json: Tailwind v4, css=styles.css, cssVariables, lucide, "@/" aliases
- "@/" -> src/renderer alias in tsconfig (paths) + vite (resolve.alias)
- fill the shadcn token gaps in @theme (card-foreground, input, destructive,
  destructive-foreground) mapped to existing emdash tokens so `shadcn add`
  components render on-brand without touching the design system
- add Card primitive (first use: Phase 1 board)

Did NOT run `shadcn init` (it would overwrite styles.css and wipe the emdash
tokens); the @theme already maps shadcn semantic names onto emdash raw tokens.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Entire-Checkpoint: 9b9c8391e52c

* refactor(renderer): persistent _shell layout + per-route pages (projects vocab)

Phase 0 of the route-parity port. Replaces the single state-driven <App> with
real TanStack Router pages behind a persistent _shell layout, the foundation
every ported screen builds on.

- _shell.tsx: pathless layout owning the Sidebar + shared state (workspace
  query, daemon status, spawn modal, create project/task, theme, shortcuts);
  child routes render into <Outlet>. The daemon-status effect runs once here.
- Router owns selection: ui-store sheds view/selectedSession/selectedWorkspace
  (now route params); keeps only theme/sidebar/workbenchTab. Sidebar/SideRail
  navigate via router and read active state from useParams.
- Routes (projects vocabulary): / -> SessionsBoard (new board home, replacing
  the orchestrator-terminal home), /projects/$projectId -> scoped board,
  /projects/$projectId/sessions/$sessionId and /sessions/$sessionId ->
  SessionView (Topbar + terminal + git rail).
- Terminal persistence: it lives on the session route, so session->session is
  a param change (TanStack keeps the route mounted -> mux re-points, no
  remount); leaving for the board unmounts it and the server ring replays on
  return.
- shell-context.ts hands daemonStatus/openSpawn/create* to route content.

Removed the monolithic App.tsx (+ App.test.tsx, whose create/spawn coverage
moves to route/hook-level tests in Phase 5) and the old workspaces.* routes.
shadcn Card used for the board cards. typecheck clean, 91 tests pass.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Entire-Checkpoint: d10b22f0d841

* feat(board): attention-zone kanban home

Phase 1: SessionsBoard becomes the real kanban, porting agent-orchestrator's
getAttentionLevel state machine (packages/web/src/lib/types.ts) as a pure
function rebound to reverbcode's SessionStatus.

- attentionZone() buckets a session into urgency-ordered zones — merge (one
  click to clear, leftmost) → action (needs-you: needs_input/ci_failed/
  changes_requested, the collapsed respond+review) → pending (waiting on
  reviewer/CI) → working → done (archive).
- Board renders a horizontal column per non-empty zone; cards navigate into
  the session route. shadcn Card for cards. Styled to DESIGN.md (emdash
  hairlines, status dots, accent), not agent-orchestrator's tokens.
- 13 zone-mapping unit tests. typecheck clean, 104 tests pass.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Entire-Checkpoint: d315acacf784

* feat(settings): project settings form (Phase 2)

/projects/$projectId/settings — a settings page on reverbcode's own
ProjectConfig shape (not agent-orchestrator's agent/runtime/tracker/scm,
which the Go daemon doesn't have). Reuses agent-orchestrator's form structure:
read-only identity card + editable config.

- Reads GET /api/v1/projects/{id} (config + identity), saves via
  PUT /api/v1/projects/{id}/config. The PUT replaces the whole config, so the
  form merges edited fields over what loaded (keeps env/symlinks/postCreate
  it doesn't expose).
- Editable: defaultBranch, sessionPrefix, default worker/orchestrator agent,
  model override. React Query for load + mutation with inline save state.
- shadcn select + label added; settings gear in the project board header.

typecheck clean, 104 tests pass.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Entire-Checkpoint: 0633ebde603b

* feat(session): PR inspector in the session rail (Phase 2)

Ports agent-orchestrator's SessionInspector onto reverbcode's SessionPRFacts
(GET /api/v1/sessions/{id}/pr -> {prs: SessionPRFacts[]}). Mounts above the
git rail on the session route; renders nothing when the session has no PR.

- Shows PR number + state badge, and CI / mergeability / review facts with
  tone derived from the fact string (pass/fail/pending), plus an unresolved
  review-comments flag.
- React Query, fetched only when the session has a PR.

Completes Phase 2 (project board reuse + settings + inspector).
typecheck clean, 104 tests pass.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Entire-Checkpoint: c75dadfd45b8

* feat(prs): pull-requests board (Phase 3)

/prs — a PR board ported from agent-orchestrator's PullRequestsPage. The Go
daemon has no PR-list endpoint, so rows are derived from session PR fields
(every session carries pullRequest), sorted open/draft above merged/closed.

- Per-row Merge (POST /prs/{number}/merge) and Resolve comments
  (POST /prs/{number}/resolve-comments) mutations with inline result; clicking
  a row opens the session (whose inspector has the full CI/review facts).
- shadcn Table; "Pull requests" + "Review" nav added to the sidebar footer.
- /review + /reviews routes added as placeholders (the reviews board needs a
  daemon backend — Phase 4); /reviews redirects to /review.

typecheck clean, 104 tests pass.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Entire-Checkpoint: 3526d847c568

* feat(reviews): code-review API + dashboard (Phase 4)

The long pole: the Go daemon had no reviews surface, so /review needed a
backend. Adds one, mirroring agent-orchestrator's reviews feature.

Backend:
- internal/service/review: in-memory reviews Manager (Run + Finding types,
  List/Execute/Send). Execution is not yet wired to a real review agent —
  Execute records a pending run so the surface is live; agent-backed findings
  + persistence are a follow-up (documented in the package).
- ReviewsController (GET /reviews, POST /reviews/execute, POST /reviews/{id}/send),
  wired through api.go + daemon.go (constructed, not nil — actually serves).
- genspec: reviewOperations() + tag + schemaNames; openapi.yaml + schema.ts
  regenerated. apispec parity/drift tests pass, go build + go test green.

Frontend:
- ReviewDashboard reads GET /reviews, lists runs with status + findings, lets
  you pick a worker and Run review (execute) and Send a run. Replaces the
  placeholder /review route. shadcn Card/Badge/Select.

Verified live: GET /reviews -> 200, POST /reviews/execute -> created run.
typecheck clean, 104 frontend tests pass.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Entire-Checkpoint: ce16c62dfdb0

* chore(renderer): Phase 5 polish — route prefetch + restored spawn coverage

- Route loader: _shell prefetches the workspace list via
  queryClient.ensureQueryData (parent loader runs before children), pairing
  with defaultPreload: "intent" so a hovered nav target is warm on click.
  workspaceQueryOptions exported so the loader and hook share one cache.
- Restored the spawn coverage dropped with App.test.tsx as a focused
  SpawnWorkerModal test: the base-branch-omission regression guard (the 409
  fix) + the empty-prompt gate.

Full parity surface green: 106 frontend tests, typecheck clean, backend
build + vet clean, all daemon endpoints 200.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Entire-Checkpoint: 284ae668ffea

* feat(board): match agent-orchestrator's board verbatim

Per explicit request to mirror agent-orchestrator's app exactly (overriding
DESIGN.md/emdash for this screen). Rebuilds SessionsBoard from its actual
source (Dashboard + AttentionZone + SessionCard + mc-board.css), using its
exact tokens and values:

- 4 equal-width columns (grid 1fr), left->right flow: Working -> Needs you ->
  In review -> Ready to merge (SIMPLE_KANBAN_LEVELS), always rendered; "done"
  archived to a separate strip, not a column.
- Per-column vertical glow gradient (status-tinted top fading at 130px) +
  glow dots + uppercase tinted column titles; #0a0b0d base, #15171b cards.
- Topbar: project crumb + Coding/Reviews tabs + breathing "N working" pill +
  bell + blue "New worker" primary. "Board" subhead + subtitle.
- Card: status badge (dot + label) · mono id, 2-line title, mono branch line,
  hairline-topped PR footer ("no PR yet").

typecheck clean, 106 tests pass.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Entire-Checkpoint: 3569e49ba7d6

* feat(theme): clone agent-orchestrator's dark palette globally

Per the verbatim-clone directive (supersedes DESIGN.md/emdash). Remaps the
:root tokens to agent-orchestrator's exact values — #0a0b0d base, #15171b
card, #f4f5f7/#9ba1aa/#646a73 text, hairline white-alpha borders, #4d8dff
accent, orange/amber/green/red status — so every screen's base shifts at once.
Adds --color-working (orange) for the working status.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Entire-Checkpoint: d70096d7f30d

* feat(renderer): clone agent-orchestrator ProjectSidebar verbatim

Rebuild the sidebar to match agent-orchestrator's ProjectSidebar: #08090b
rail, "Reverb / Code" brand with dimmed separator + collapse button,
uppercase PROJECTS label, project disclosure rows (rotating chevron +
hover-revealed New worker action + session count), nested session rows
with a 6px breathing working-dot and mono session id, and a single
Settings menu footer (Pull requests / Reviews / Search / Project
settings) plus a daemon-health dot.

Adds a shadcn dropdown-menu primitive (radix-ui unified package, matching
the existing select/label convention) for the footer menu.

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

* feat(renderer): clone agent-orchestrator session topbar

Restyle the session header to match agent-orchestrator's SessionDetailHeader:
a "Kanban" back-to-board button + hairline divider, a stacked identity
(project / title over a mono branch line with a git-branch icon), and a
StatusBadge --pill (tinted bordered pill with a 6px dot that breathes while
the agent is working). Wire onOpenBoard from SessionView to navigate back to
the project board (or home).

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

* feat(renderer): unify board/review/PR/settings chrome verbatim

Extract the mc-board dashboard header (project crumb · Coding/Reviews tabs ·
"N working" breathing pill · bell · settings · New worker) and the 21px
subhead into a shared DashboardTopbar/DashboardSubhead, then apply it to the
review, PR, and settings screens so every dashboard surface shares one stable
agent-orchestrator top strip. SessionsBoard now consumes the shared chrome
instead of its inline copy; review/PR/settings drop their minimal h-11 headers
for the crumb+tabs+subhead treatment on the #0a0b0d base.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: 2801a9c5c0ba

* docs: record agent-orchestrator-verbatim design direction

Per explicit user decision (2026-06-10), the renderer clones the
agent-orchestrator web app verbatim, superseding the older "match emdash"
direction. Add a prominent banner at the top of DESIGN.md (reference files,
live palette, the cloned surfaces, shadcn-primitive guidance), mark the
Aesthetic Direction section as superseded, and retarget CLAUDE.md's QA rule so
future review flags divergence from agent-orchestrator instead of emdash.

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

* feat(renderer): clone agent-orchestrator shell and inspector

Finish the agent-orchestrator-style renderer pass with shadcn sidebar chrome, titlebar navigation, resizable session inspector, orchestrator spawn affordances, and matching design tokens.

Co-authored-by: Cursor <cursoragent@cursor.com>

* chore: format with prettier [skip ci]

* fix: repair UI PR CI drift

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor(terminal): per-client zellij attach replaces shared PTY + replay ring

Each WebSocket client that opens a pane now gets its own `zellij attach`
PTY (attachment.go) instead of sharing one PTY whose output was replayed
from a bounded byte ring. Zellij answers every fresh attach with its full
init handshake (alt screen, SGR mouse tracking, bracketed paste) and a
faithful repaint — the ring replay lost exactly that handshake, leaving
late subscribers without mouse reporting (dead wheel scroll). The cost is
one zellij client process per open pane per connection, which the zellij
server is built for (yyork ships the same model).

ring.go and session.go (fan-out, replay buffer) are deleted; manager.go
now tracks per-client attachments with liveness gating, and pty_unix.go
answers every resize frame with an explicit SIGWINCH.

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

* fix(renderer): re-assert settled terminal resize; align docs with per-client attach

After each debounced resize settles, send one follow-up resize frame with
the same grid (RESIZE_REASSERT_MS). xterm only fires onResize on actual
grid changes, so a resize update the zellij client loses (raced mid-attach
or coalesced during a drag) would otherwise desync the session layout from
the pane until the next real change. The backend answers every resize
frame with an explicit SIGWINCH, so the re-assert is a no-op when already
in sync.

Comments in the terminal hook/components now describe the per-client
attach model (fresh server-side `zellij attach` per open, no replay ring).

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

* feat(renderer): full-width shell topbar; retire per-view topbars and review dashboard

The shell now owns a single full-width ShellTopbar (status pill, history
arrows, notifications, kanban/inspector toggles) with the sidebar pinned
below it, replacing the per-view Topbar/DashboardTopbar pair; board pages
get a lightweight DashboardSubhead. The standalone review dashboard and
its /review(s) routes are removed — review state lives on the PR board.
Approved divergence from the AO reference (full-height sidebar) recorded
in DESIGN.md; macOS traffic lights re-centered on the 56px header row.

Also hardens the session view around rrp v4:
- inspector defaultSize re-derived per panel mount (orchestrator → worker
  navigation kept SessionView mounted while the panel remounted), and the
  imperative expand/collapse effect no longer races panel registration
- onResize writes gated on data-separator="active" so flex-grow
  transition frames can't bounce the store (dead-looking toggle button)
- findProjectOrchestrator skips terminated orchestrators so the topbar
  offers Spawn instead of attaching to a dead zellij session
- inspector resize handle gets a visible 1px divider at rest
- playwright specs for history arrows + inspector toggle; test-results/
  gitignored

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

* docs: document Electron app dev quick start

Add an "Electron app (dev)" section: npm install + npm run dev under
frontend/, with the explicit heads-up that the app does not start the
daemon — it attaches over loopback to a daemon started via `ao start`
(plus npm run dev:web for renderer-only work in a browser).

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

* chore(fork): ignore local agent session dirs

Fork-only ignore entries (.entire/.claude/.gstack) — must not be included
in upstream PRs.

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

* chore: format with prettier [skip ci]

* feat: align backend session lifecycle with workspace runtime updates

* refactor: replace spawn modal with shell-native worker controls

* chore: add shared daemon launch helper and docs updates

* chore: format with prettier [skip ci]

---------

Co-authored-by: Ashish Huddar <ashish.hudar@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-06-13 12:18:55 +05:30
Ashish Huddar 40bd2dfb2b
fix(sessions): stop AO hook files from making every worktree permanently dirty (#169)
* fix(sessions): stop AO hook files from making every worktree permanently dirty

Agent adapters write hook files (.codex/hooks.json, .opencode/plugins/
ao-activity.ts, .claude/settings.local.json, ...) into fresh session
worktrees as untracked files. `git worktree remove` (deliberately run
without --force) refuses on any untracked file, so Workspace.Destroy
failed for every session of the 12 workspace-writing harnesses:
POST /sessions/{id}/kill returned an unlogged 500 INTERNAL_ERROR and
`ao session cleanup` reported 'Would clean N' then '0 sessions cleaned'
with no reason, leaking workspaces forever.

Three coordinated fixes, none of which force-deletes user/agent work:

- Root cause: every adapter now writes a sentinel-guarded, self-ignoring
  .gitignore next to its hook files (hookutil.EnsureWorkspaceGitignore),
  so AO's own files no longer count as dirt while anything an agent
  drops — even in the same directory — still blocks teardown. A
  registry-wide conformance test enforces the contract for all current
  and future adapters. (Per-worktree .git/worktrees/<name>/info/exclude
  was evaluated first but git does not honor it.)
- Typed refusal: gitworktree.Destroy classifies a still-dirty refusal as
  ports.ErrWorkspaceDirty (git status probe). Kill maps it to success
  with freed=false (session terminated, worktree preserved); Cleanup
  reports it per-session as skipped-with-reason through the API
  (CleanupSessionsResponse.skipped), and the CLI prints
  'Skipped: <id> (workspace has uncommitted changes)' plus a summary.
- Observability: envelope.WriteError records the raw service error into
  a request-scoped slot and the access log attaches it to 5xx lines, so
  any remaining internal error is diagnosable server-side.

Worktrees created before this fix gain the .gitignore on restore (hook
install re-runs); their cleanup is otherwise reported as skipped instead
of erroring.

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

* fix(cleanup): address Greptile P2s — surface dirty-probe failures, stop leaking raw errors

Two review findings on this PR:

- gitworktree.Destroy: when the isDirty probe itself failed, the error was
  silently discarded and the refusal looked identical to "registered but not
  dirty". The probe failure now rides the returned error (dirty probe: ...),
  so it reaches the access log via the 5xx error capture.

- Cleanup skip reasons: a non-dirty teardown failure put the raw error —
  including internal filesystem paths — into the public skipped[].reason
  field. The public reason is now the fixed string "workspace teardown
  failed"; the full cause goes to the daemon log (warn, with sessionID and
  path). The dirty-refusal reason is unchanged.

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

* fix(gitworktree): wrap the dirty-probe error with %w per errorlint

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 11:06:45 +05:30
Ashish Huddar 8d0c53ec1d
fix(codex): reliable activity signals — session-flag hooks, trust bypass, no_signal watchdog (#170)
* fix(codex): deliver activity hooks via -c session flags, trust worktree at launch

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 10:36:45 +05:30
yyovil c2c4404c7d
feat(frontend): scaffold for frontend with complimentary backend changes (#168)
* feat(frontend): rebuild Electron desktop UI as a React + Vite renderer

Replaces the skeleton Electron frontend with a full React 19 + TypeScript
renderer (Vite, electron-forge, contextBridge preload), plus the backend
additions it needs.

Renderer:
- TanStack Query + EventTransport (CDC SSE on /api/v1/events)
- TanStack Router file-system routing (hash history for the file:// origin)
- Tailwind + shadcn/ui, react-resizable-panels, Zustand UI state
- @xterm/xterm per-session PTY over /mux WebSocket + WebGL addon
- openapi-typescript + openapi-fetch types off openapi.yaml
- electron-forge packaging + update-electron-app auto-updater
- Vitest + RTL · Playwright

Backend:
- cors.go — allowlist-only CORS, handles Private Network Access preflight
  for app:// renderer -> loopback daemon
- session.TerminalHandleID exposed in domain + OpenAPI spec
- project.Path added to OpenAPI spec, service, store, and tests

DESIGN.md documents the emdash-matched dark UI (tokens, blue accent, status
glyph spec, orchestrator-led layout).

Co-authored-by: Ashish Huddar <ashish.hudar@gmail.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: b1e334c1e54a

* chore: format with prettier [skip ci]

---------

Co-authored-by: Ashish Huddar <ashish.hudar@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-06-10 11:40:17 +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
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
neversettle 7880f59cf5
refactor(backend): LLD maintainability fixes in controllers/service layers (#95) (#96)
* refactor(backend): LLD maintainability fixes in controllers/service layers

Addresses the high + medium severity findings from the LLD review of
backend/internal/httpd and backend/internal/service (#95):

1. Controllers no longer import internal/session_manager. Session sentinel
   errors are now *domain.ServiceError values carrying their own HTTP mapping,
   so the controller translates them with one generic errors.As — no
   cross-package sentinel imports.
2. One error pattern across services: project.Error is now an alias of the
   shared domain.ServiceError, and session_manager sentinels use it too. A
   single writeServiceError replaces the per-resource error switches.
3. Clean-orchestrator business logic moved out of the controller into
   session.Service.SpawnOrchestrator(ctx, projectID, clean).
4. isGitRepo no longer treats case-different paths as equal on case-sensitive
   filesystems; case-insensitive compare is gated to darwin/windows via samePath.
5. Project repo check sits behind an injectable GitChecker, so the service is
   testable without a real git binary.
6. httpd exports only the production constructors (NewWithDeps,
   NewRouterWithControl); the 3 test-only wrappers are removed and the
   "router with empty deps" convenience moved to an unexported test helper.

Closes #95

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

* refactor(backend): standardize service errors on internal/httpd/errors

Replace the domain.ServiceError approach with a REST-API-scoped error package
and a single envelope renderer, per review feedback:

- Add internal/httpd/errors (package errors, aliased apierr): one structured
  Error type with semantic Kinds (Internal/Invalid/NotFound/Conflict) and
  constructors. Imports nothing, so any layer can depend on it.
- envelope.WriteError is now the single path from a service error to the wire
  APIError, and the only place a Kind becomes an HTTP status/word. The
  per-resource writeProjectError/writeSessionError translators are gone.
- Delete domain/errors.go (keeps domain pure of HTTP-flavored kinds) and
  service/project/errors.go (no per-service error files); services build
  errors inline via apierr constructors.
- session_manager sentinels are apierr.Error values (pointer identity still
  works with errors.Is).

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

* revert(backend): drop GitChecker seam and isGitRepo case-sensitivity change

Defer findings #4 (isGitRepo case-sensitivity) and #5 (GitChecker seam) out
of this PR. Restores the original exec-based isGitRepo and the New(store)
constructor; removes git.go, git_test.go, and the test-only export shims. The
error-standardization and other findings are unaffected.

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

* refactor(session): translate engine errors to API errors at the facade

The session_manager is the internal command engine and must not depend on the
REST API error vocabulary. Revert its sentinels to plain errors.New values and
move the engine→API translation into the service/session facade (toAPIError),
which is the correct boundary. Controllers still see apierr.Error and never
import the engine; the engine no longer imports internal/httpd/errors.

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

* docs(session): tighten error comments to state what the code does

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

* style(envelope): make KindInternal an explicit case in httpStatus

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

* refactor(apierr): rename package, test SpawnOrchestrator, parity fixes

Address review feedback on PR #96:
- Rename internal/httpd/errors → internal/httpd/apierr (package apierr) so
  importers no longer alias around the stdlib errors package.
- Add a commander seam to session.Service and unit-test the relocated
  clean-orchestrator rule: clean=true kills all active orchestrators before
  spawning; clean=false spawns without kills.
- project.Add: wrap the UpsertProject store error in apierr.Internal for parity
  with its sibling paths (was a raw 500).
- Document that KindInternal is iota's zero value, so a zero-value Error
  defaults to 500.

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

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 18:09:02 +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