* 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>
* 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>
* 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>
* 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.
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>
* feat(plugin): add agents plugin (first iteration)
Faithful copy of the agents plugin implementation from yyovil/better-ao
(internal/plugin/ -> backend/internal/plugin/) plus its PRD
(prds/plugins/agents/PRD.md), as a first-iteration proposal for review.
Imports are left at their original github.com/yyovil/better-ao/... paths and
are NOT yet reconciled to this repo's module; see PR description for the
integration deltas (module path, missing internal/utils dependency).
Co-authored-by: Claude <noreply@anthropic.com>
* Move agent adapters under backend adapters
* Keep daemon ports and session out of adapter move
* Remove Better-AO naming from flake
* Keep flake as dev shell only
* Use goimports for local formatting
* Wire session manager to per-session agent adapters
Move the Agent port into internal/ports and have the claude-code and
codex adapters implement it directly, alongside their workspace-local
activity hooks and a manifest-keyed adapter registry. Rename
RuntimeConfig.LaunchCommand to Argv and update the tmux and zellij
runtimes to match.
The session Manager now resolves a real agent adapter per session via a
new ports.AgentResolver: from cfg.Harness on Spawn and the stored harness
on Restore, so one daemon runs claude-code and codex sessions side by
side. The daemon backs the resolver with the registry; AO_AGENT selects
the default harness (default claude-code), validated at startup. Removes
the temporary noopAgent stub.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(agent): point the agent contract at internal/ports/agent.go
The Agent interface moved from internal/adapters/agent to internal/ports;
update the PRD's Goal and Agent Contract sections (and the SessionInfo
references) to match the code.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Wire the session service into the daemon
daemon.Run now builds the controller-facing session service — a session
manager over the zellij runtime, a gitworktree workspace, the shared
store + LCM, and the per-session agent resolver (AO_AGENT default,
validated at startup) — and mounts it at httpd APIDeps.Sessions, so the
session REST routes are backed by a real service. startLifecycle moves
ahead of the HTTP server so both share one LCM.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Address Greptile review: complete the live spawn path
- Spawn and Restore now install workspace-local activity hooks
(GetAgentHooks) and run the adapter's optional PreLaunch step before
launch, via a shared prepareWorkspace helper. PreLaunch is how Claude
Code records workspace trust, so its interactive "trust this folder?"
dialog can't hang the headless pane; the spawned env now also carries
AO_DATA_DIR so the installed hook commands find the store.
- claudecode and codex hook/config writes are now atomic (temp + rename)
instead of os.WriteFile, so a crash mid-write can't leave a partial
file the agent fails to parse.
- ensureWorkspaceTrusted serializes its read-modify-write under a package
mutex, so concurrent spawns to different workspaces don't drop each
other's ~/.claude.json trust entries.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(ports): pin MetadataKeyAgentSessionID to domain.SessionMetadata json tag
The equality between ports.MetadataKeyAgentSessionID and the json tag on
domain.SessionMetadata.AgentSessionID is a hand-maintained invariant; this
test fails loudly if either side drifts.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(adapters): use ports.MetadataKeyAgentSessionID in claudecode + codex
The native session id metadata key is defined in ports for cross-package
consumption; drop the duplicated literals in each adapter so the constant
has one home.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test(codex): cover ensureCodexHooksFeatureEnabled TOML edge cases
The helper is a string editor over config.toml; pin its content
transformation for missing/empty files, existing [features] blocks,
the no-op case, and the legacy codex_hooks=true migration paths.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(adapters): document Registry concurrency contract
Registry registration runs at daemon boot before any goroutine calls Get,
so the underlying map needs no lock; pin that contract in the doc comment
so a future change doesn't quietly introduce a race.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* style(codex): gofmt codex_test.go after constant rename
The previous commit (7c5b2a9) replaced codexAgentSessionIDMetadataKey with
ports.MetadataKeyAgentSessionID inside a map literal; the longer key threw
off gofmt's column alignment on the adjacent codexTitleMetadataKey /
codexSummaryMetadataKey lines. Caught by agent-ci's Check formatting step.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: harshitsinghbhandari <dev@theharshitsingh.com>