* 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>
Joins the ground-up ReverbCode rewrite (this tree) with the prior
agent-orchestrator history as a second parent, preserving both lineages.
Tree content is ReverbCode's; old history remains reachable in main.
* fix(desktop): package Windows via NSIS instead of Squirrel (#401)
Squirrel.Windows is a poor fit: per-user install only, no custom install
directory, no proper add/remove-programs uninstaller, and fragile updates.
Replace it with a real NSIS installer (per-user or per-machine, custom
install dir, uninstaller), matching recordly's working Windows setup.
Electron Forge ships no first-party NSIS maker, so add a thin MakerBase
subclass (makers/maker-nsis.ts) that bridges to electron-builder's
buildForge, the same engine electron-builder uses, scoped to win32. The
maker exposes the NSIS knobs the issue calls for (oneClick:false,
allowToChangeInstallationDirectory, per-machine) and defaults to an
assisted installer.
- forge.config.ts: drop maker-squirrel, add the NSIS maker instance.
- testing-build.yml: target "nsis"; smoke-install via /S under out/make;
drop the Squirrel-specific log capture.
- Rename the package "agent-orchestrator-frontend" -> "agent-orchestrator":
this repo is the full app, not just a frontend. User-facing naming was
already "Agent Orchestrator" (productName) / agent-orchestrator.exe.
Deferred (per issue, separate follow-ups): bundling zellij.exe so a fresh
Windows install needs no manual zellij, an actionable "zellij not found"
error at session-create, and Windows code-signing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore: format with prettier [skip ci]
* fix(desktop): disable electron-builder publish + drop electron-squirrel-startup
CI fix: the NSIS maker's electron-builder run inferred a GitHub publish
target from package.json `repository` and tried to upload (to emit
auto-update info), failing with "GitHub Personal Access Token is not set".
Forge owns publishing (the workflow uploads via `gh release`), so set
`config.publish = null` to disable electron-builder's upload entirely.
Also remove `electron-squirrel-startup`: it only handled Squirrel.Windows
install/update hooks (--squirrel-* flags) and is dead weight under NSIS.
Drop the dependency, its import, the startup quit-block, the whenReady
guard, and the type shim. The EPIPE std-stream guard stays (it covers any
windowless Windows GUI launch, e.g. from a shortcut).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore: gitignore electron-builder's builder-debug.yml dump
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
* fix(desktop): handle Squirrel startup events + CI smoke-install the Windows build
Two Windows-install fixes.
1. Add electron-squirrel-startup handling. main.ts had no handler for the
--squirrel-{install,updated,uninstall,obsolete} flags Squirrel runs the exe
with during install/update/uninstall. Without it the install hook booted the
full app (window + daemon spawn) and never exited, so the installer hung
waiting on it. Now we create/remove shortcuts and quit immediately; it is a
no-op on macOS/Linux.
2. Add a Windows CI smoke-install step. After the build, run Setup.exe --silent
on the clean native x64 windows-latest runner, assert the install dir is
created, and upload SquirrelSetup.log as an artifact. This captures the
install log we otherwise cannot get and gives a build-vs-host verdict: a clean
install proves the artifact is good, so a failing user machine is host-side
(AV/disk/signing). The runner has no real-time AV, so it does NOT prove
SmartScreen/Defender will accept the unsigned binaries on end-user machines;
code-signing stays the durable fix.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* ci(windows): capture SquirrelSetup.log from the app dir, fix misleading warning
Update.exe writes SquirrelSetup.log into %LocalAppData%\AgentOrchestrator, not
SquirrelTemp; the smoke step looked only in SquirrelTemp and so printed
"failed before Update.exe ran" even on a successful install (exit 0, dir
created). Look in the app root dir first, fall back to SquirrelTemp, and drop
the false-failure wording.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
On Windows the daemon shells out to console-subsystem processes without
suppressing the console window. The reaper polls session liveness on a
timer (and the attach loop retries), each call runs a zellij command, and
every invocation pops a console window that instantly closes, so the user
sees rapid blinking.
- Add a platform-split hideWindow(cmd) helper to the zellij package
(CREATE_NO_WINDOW + HideWindow on Windows, no-op elsewhere) and call it
in execRunner.Run so list-sessions and friends stop flashing.
- startBackgroundProcess now uses CREATE_NO_WINDOW instead of
CREATE_NEW_CONSOLE (the latter creates a console then hides it, flashing).
- Pass windowsHide: true to the daemon spawn in main.ts so the daemon's own
console stays hidden on a Windows GUI launch.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(desktop): recover login-shell env so the daemon finds zellij/credentials
A Finder/Dock launch starts the app via launchd, not a login shell, so
~/.zprofile and ~/.zshrc are never sourced. The daemon then inherits
launchd's minimal env (no /opt/homebrew/bin on PATH, no exported
ANTHROPIC_API_KEY, etc.), cannot exec zellij/git, and its agents cannot
authenticate. Launching from a terminal masked this because the shell had
already populated the env, so it only reproduced on a real Finder/Dock launch.
Resolve the login-shell environment once at startup
($SHELL -ilc "printf sentinel; env -0"), adopt it as the base for
daemonEnv(), and force PATH from the shell with a static floor when the probe
fails (timeout/non-zero exit). The probe never blocks startup (3s SIGKILL
timeout, stdin closed) and degrades to the floor rather than erroring.
Windows keeps its existing behavior: its env comes from the registry/session
and is inherited by GUI-launched apps, so this bug does not exist there.
Pure parse/merge logic lives in shared/shell-env.ts (no node:* imports, per
the daemon-attach.ts convention) with unit tests; main.ts owns the real spawn.
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(cli): add examples to ao preview --help (#387)
Update the preview command help to include a concise Examples section
covering the four common workflows:
ao preview
ao preview file:///home/aoagent/ReverbCode/index.html
ao preview http://localhost:5173
ao preview clear
Replace the workspace-relative path (./dist/index.html) in the Long
description with the absolute file:// URL pattern agents actually use.
Command syntax and behavior are unchanged.
Closes#387
* fix(cli): clarify no-arg preview fallback behavior in help text
Address review feedback on #388: the no-argument description now
reflects the daemon's actual behavior — autodetect the workspace
entry point, fall back to the session's existing preview target
when none exists.
---------
Co-authored-by: AO Bot <ao-bot@composio.dev>
On Windows Squirrel/GUI launches there is no attached console, so
process.stdout/stderr are dead pipes. The daemon-output console.log/error
calls failed with EPIPE and, lacking an error listener, crashed the main
process with 'A JavaScript error occurred in the main process'. Add an
error listener that ignores broken-pipe writes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replaces linux/windows/macos-testing-build.yml with a single testing-build.yml
(matrix over ubuntu/windows/macos) that publishes all artifacts to one
0.0.0-testing-<sha> prerelease. Manual dispatch + 0.0.0-testing-* tag push.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fixes the packaged desktop app getting permanently stuck on "AO daemon
is not ready" (#385) via two daemon-lifecycle fixes.
1. Port conflict no longer exits the daemon. When the configured port
(default 127.0.0.1:3001) is held by a non-AO process, NewWithDeps now
falls back to an OS-assigned ephemeral port instead of returning a bind
error. A genuine peer AO daemon is already ruled out upstream (the
running.json + /healthz check in daemon.Run), so a conflict here means a
foreign holder. The bound port is logged ("daemon listening") and written
to running.json, both of which the supervisor reads, so the fallback
propagates to the renderer with no UI changes.
2. Detached daemon is torn down on more exit paths. before-quit already
group-kills the daemon, but app.exit() and some shutdown routes skip it,
orphaning the daemon so it keeps holding the port for the next launch. A
synchronous process 'exit' handler now also signals the daemon's process
group. A hard SIGKILL/crash still can't run JS, but fix#1 covers the
orphan that leaves behind.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
mainWindow.on('closed') -> browserViewHost.dispose() ran destroy(), which
touched contentView/child WebContentsViews already torn down by Electron,
crashing the main process with 'Object has been destroyed'. Skip the window
ops when the window reports destroyed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(preview): add clear, reuse defaults, force refresh, local files (#379)
`ao preview` had four issues that made the desktop browser panel awkward
during sessions. This addresses all four:
1. No way to clear the panel. Adds `ao preview clear` (DELETE
/sessions/{id}/preview) which empties the stored target; the panel
loads about:blank and returns to its empty state.
2. Bare `ao preview` always autodetected index.html. It now reuses the
session's existing preview target (so each agent/context keeps its own
default), falling back to index.html only when nothing was previewed.
3. Re-running `ao preview <same-url>` never refreshed. The preview_url
alone could not distinguish a real re-run from a CDC replay of an
unrelated session update. A new monotonic preview_revision (bumped on
every set, migration 0018, added to the sessions_cdc_update trigger)
gives the renderer a per-command identity to key navigation on, so a
re-run always re-navigates while unrelated updates are ignored.
4. Local files could not be previewed. `ao preview ./dist/index.html`
(and other workspace-relative paths) now resolve server-side to the
preview/files proxy URL when the file exists; non-file targets stay
verbatim.
Backend, CLI, and renderer all covered by tests; OpenAPI spec and the
frontend schema are regenerated for the new DELETE route and field.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(cdc): include previewRevision in sessions update event payload
The CDC trigger watched preview_revision changes but didn't include it
in the JSON payload, so the frontend couldn't detect same-URL preview
refreshes via SSE events. This broke the core purpose of the feature.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(migration): renumber to 0019 to resolve conflict with main
Main branch now has migration 0018 (review_run_delivered_at), causing
a duplicate version conflict when CI merges the PR branch with main.
Renamed 0018_add_session_preview_revision.sql to 0019.
Also fixed the Down migration to properly restore the CDC trigger state
after migration 0017 (with previewUrl but without previewRevision).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Vaibhaav <user@example.com>
* fix(desktop): attach to a serving daemon instead of spawning a doomed child
Launching the Electron app while a standalone `ao daemon` already owns the
port made the Electron-spawned child daemon log "daemon already running …
refusing to start" and exit 1, instead of attaching to the running daemon.
`inspectExistingDaemon` only attaches when ~/.ao/running.json agrees with a
live daemon, so any run-file divergence (missing/stale/unparseable file, dead
PID, or a /healthz pid mismatch) made it return null — and there was no
independent port probe before spawn(), so Electron spawned into an occupied
port and the Go bind guard correctly refused.
Add a defensive direct probe of http://127.0.0.1:<expectedPort>/healthz in
startDaemonInner, after the run-file check and before spawn(): if a genuine
daemon answers, attach to it (the same "ready" DaemonStatus shape the run-file
path returns) instead of spawning. The expected port is resolved the same way
startup does (AO_PORT or the default).
The attach-or-spawn decision is extracted into a pure, dependency-injected
module (shared/daemon-attach.ts) so it can be exercised directly; main.ts
keeps ownership of fs reads, process signals, fetch, and the path identity
check. Covered by 27 tests, including real loopback-server cases that
reproduce the issue scenario end to end.
Fixes#367
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore: format with prettier [skip ci]
* fix(desktop): enforce readiness + identity checks on the port-probe attach path
Address review feedback on #367: the new direct port probe attached to any
service-matching daemon as soon as /healthz returned ok, without the /readyz
and foreign-binary identity checks the run-file path enforces. That reopened
the mismatch daemonIdentityError was built to prevent — Electron could
silently drive a different/older AO build serving the port — and could mark a
still-starting daemon "ready".
Extract the shared post-handshake tail (readinessStatus) and run it from both
paths, anchoring on the PID /healthz reports for the port probe. A serving
daemon that is not ready, or whose binary the identity check refuses, now
yields the same "error" DaemonStatus instead of attaching — strictly safer
than spawning, which would only collide on the occupied port and die.
resolveDaemonFromPort now takes the same identityError dependency
resolveDaemonFromRunFile does; main.ts passes daemonIdentityError(launch, …).
Adds port-path tests for not-ready, foreign-binary identity (unit), and a
real-server foreign-binary scenario (e2e). 30 tests in daemon-attach.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore: format with prettier [skip ci]
* docs(desktop): note why the port probe uses the expected (not hardcoded) port
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
#366 (brand text overlapped by the fixed macOS TitlebarNav cluster on
board routes) is already fixed on main: #263 made the shell render the
topbar on every route, so the sidebar always hangs below the 56px
titlebar band and the brand never lands in the cluster's lane.
Add an e2e regression guard that locks the invariant in for the routes
the issue named — the brand must not overlap the fixed cluster and the
"Agent Orchestrator" wordmark must stay readable (not truncated) — on the
home board route and the project board route, plus across a board→session
transition (the persistent brand must not jump). Drives the live renderer
under a forced macOS UA. If a topbar-less route is ever reintroduced,
these fail.
Verified: passes against current main with no source changes;
typecheck clean.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* 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>
* fix(frontend): add terminal controls and reliable copy
* chore: format with prettier [skip ci]
* fix(frontend): preserve terminal mouse scrolling
* fix(backend): enable zellij wheel scrolling
* fix(frontend): forward xterm scroll input
* fix(frontend): restore terminal drag selection copy
* fix(frontend): make terminal wheel scroll zellij scrollback
zellij 0.44.x with mouse-mode true acts on SGR wheel reports written to
its stdin and scrolls the focused pane, but it does not enable host mouse
reporting. xterm therefore never reports the wheel itself (protocol stays
NONE) and, with scrollback:0, converts the wheel into cursor-arrow keys,
which move the agent's cursor/history instead of scrolling.
Synthesize SGR wheel reports from a custom wheel handler and send them
through the existing input pipe; accumulate pixel deltas into line counts
to match xterm's native scroll feel. Ctrl/Cmd wheel is left for the
font-size zoom handler. Drag-copy is unaffected (separate selection path).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore: format with prettier [skip ci]
* fix(frontend): handle line/page wheel modes for cross-platform scroll
The wheel-to-SGR translation only divided pixel deltas by row height,
which is the deltaMode browsers emit for trackpads and normalized wheels
(macOS). Many Linux/Windows mouse wheels report whole lines (deltaMode 1)
or pages (deltaMode 2) with small deltaY, which truncated to zero lines
and never scrolled. Mirror xterm's getLinesScrolled across all three
modes so scroll works on every platform.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Harshit Singh Bhandari <dev@theharshitsingh.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(review): message worker on changes_requested instead of relying on SCM poll (#337)
review.Engine.Submit previously only persisted the verdict/body and left the
worker to learn about requested changes via the SCM poll loop, which is gated on
GitHub's reviewDecision and never reaches CHANGES_REQUESTED for self-reviews or
COMMENT-state reviews. Submit now nudges the worker's live pane directly via
ports.AgentMessenger (the same mechanism lifecycle uses) whenever the verdict is
changes_requested.
Extended flow: the reviewer reads back the GitHub review id it posted and passes
it through `ao review submit --review-id`; the id is stored on the review_run row
(new column + migration 0016) and included in the worker message so the worker
knows exactly which review to address and reply to.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(review): mark worker nudge as AO internal review, ask to reply + resolve
Distinguish the AO internal review nudge from the external GitHub-reviewer
feedback the lifecycle SCM loop relays. For an AO review the worker is now asked,
once it has pushed its fix, to reply on the review referencing its id with what
it changed and resolve the inline review comment threads it addressed (the
reviewer posts inline comments, so the per-finding threads are resolvable via
resolveReviewThread; the top-level review object is not, hence the reply).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(review): generalise the changes-requested worker nudge wording
Drop the "not an external GitHub PR reviewer" aside and the assumption that the
worker pushes a fix — it may resolve the feedback without code changes. The nudge
now reads "Review the feedback below and address it" and asks the worker to reply
with how it addressed the review and resolve the threads it addressed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(review): harden the review-id read-back against array order and empty results
The reviewer read the just-posted review id with `--jq '.[-1].id'`, which trusts
the REST API to return reviews in ascending submission order and errors when no
review exists. Review ids are monotonic, so select the highest id instead and
emit nothing when the list is empty: `--jq 'map(.id) | max // empty'`. Update the
matching `--review-id` flag help.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(review): post the review via gh api and capture its id from that response
The reviewer must use `gh api --method POST .../reviews` to attach inline
comments anyway (`gh pr review` cannot), and that response already contains the
created review's id. Capture `.id` from that single call instead of a second
read-back, dropping the array-ordering/pagination heuristics entirely — the id is
the exact review just created.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(review): send the review as a JSON body so inline comments are a real array
gh api -f/-F cannot build an array of objects: comments[][path] is sent as a
literal key, so the inline comments are dropped — defeating the reason for using
gh api over gh pr review. Post the review via --input JSON instead, keeping the
.id capture and the approve/COMMENT fallback.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore: format with prettier [skip ci]
* chore(review): drop accidentally committed reviewer scratch, write review out of tree
review.md was the reviewer agent's own writeup, swept onto the worker branch by a
stray `git add -A` in 5df20c9. Remove it, gitignore `/review.md` as a backstop,
and change the reviewer prompt to write its review to a temp file outside the
checkout instead of into the worktree (where it could be committed onto the
worker's branch).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(review): message the worker before marking the run complete
If messenger.Send failed after UpdateReviewRunResult had already flipped the run
to complete, a retried `ao review submit` tripped the status='running' guard and
could never record the result. Send first; only mark the run complete once the
worker has been notified, so a failed send leaves the run retryable. A landed
message followed by a failed DB write degrades to one extra nudge on retry — the
same trade lifecycle's sendOnce makes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(review): accept the review body on stdin so the reviewer writes no file
`ao review submit --body -` now reads the review from stdin, and the reviewer
prompt pipes its writeup via a heredoc instead of writing a file. Previously the
reviewer wrote review.md into its checkout to pass as --body, which could be
committed onto the worker's branch (as it just was). A file path is still
accepted for backward compatibility.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(review): always post approvals as COMMENT, drop the APPROVE attempt
The reviewer posts from the PR author's own GitHub account, so event=APPROVE
always 422s. Drop it: request changes with REQUEST_CHANGES, approve with a
COMMENT-event review whose body states it is an approval.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(review): post every review as event=COMMENT (author can't APPROVE or REQUEST_CHANGES own PR)
The reviewer posts from the PR author's own account, where GitHub rejects both
APPROVE and REQUEST_CHANGES. Always post a COMMENT-event review and state the
verdict in the body; the machine-readable verdict still reaches AO via
`ao review submit --verdict`.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(cli): accept underscore flag names on `ao review submit`
Reviewer agents routinely invoke the submit command with --review_id
instead of --review-id, which cobra rejected as an unknown flag and
dropped the GitHub review id from the worker notification. Normalize
underscores to hyphens on the command's flags so both spellings resolve
to the same flag.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: sanitize review id in worker notifications
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Vaibhaav <user@example.com>
Electron's main process set app.setName(...) but never overrode userData,
so Chromium runtime state (cache, cookies, local/session storage, crash
dumps) defaulted to ~/Library/Application Support/<name>. Older dev builds
also wrote the daemon DB there. Pin userData to ~/.ao/electron so the
entire app footprint lives under the canonical AO home alongside the
daemon data dir and running.json; sessionData and crashDumps derive from
userData, so the single override reparents them all.
Document the ~/.ao-only rule as a hard boundary in AGENTS.md and CLAUDE.md.
Closes#368
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(session-manager): terminate sessions even when handle is missing
Kill hard-failed with ErrIncompleteHandle before recording terminal
intent, so a session that lost its runtime/workspace handle (crash,
partial spawn) was un-killable and stuck forever on the dashboard.
MarkTerminated now runs unconditionally; each destroy step is gated
individually on whether that handle exists. If nothing is present to
tear down, the session still terminates — freed=false just signals
nothing was freed. ErrIncompleteHandle is still returned by Restore
and runtimeMessenger.Send, where a missing handle is genuinely fatal.
Updates TestKill_RefusesIncompleteHandle → TestKill_TerminatesIncompleteHandle.
* style: gofmt blank line between test functions
---------
Co-authored-by: AO Bot <ao-bot@composio.dev>
* fix: recover terminal reattach after daemon idle
* chore: format with prettier [skip ci]
* fix: harden daemon start recovery
* fix: cancel stale daemon start attempts
* fix: quarantine untrusted daemon base url
* chore: format with prettier [skip ci]
* fix: close daemon status race windows
* chore: format with prettier [skip ci]
* fix: bootstrap daemon trust before shell load
* test: trust mocked API base in PR hydration
---------
Co-authored-by: Vaibhaav <user@example.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
The `Desktop testing build` workflow (tag 0.0.0-testing-*) failed on two of
its three runners, and the unsigned macOS artifact has no real app icon.
Linux (rpm): maker-rpm reported "cannot run on linux" because
electron-installer-redhat is only a deeply-nested optional dependency and npm
non-deterministically skipped it on the runner (debian installed, redhat did
not). Promote both electron-installer-debian and electron-installer-redhat to
top-level optionalDependencies so npm reliably installs them on linux/darwin
and still skips them cleanly on win32. Also give the rpm maker an explicit
License (rpmbuild rejects an empty License field) and a maintainer/homepage.
Windows (squirrel): NuGet pack exits 1 when <authors> is empty. package.json
had no author, so add author/license/homepage and set authors + setupIcon on
the squirrel maker.
App icon: generate icon.icns/.ico/.png from src/landing/public/og-image.png
(1024x1024) and wire packagerConfig.icon, the deb/rpm/squirrel makers, and the
runtime BrowserWindow icon (Linux/Windows; macOS uses the bundle .icns).
Verified on macOS: npm run make builds the zip, the packaged app's icns
matches the generated icon, typecheck + 160 vitest tests pass.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Add a tag-triggered workflow that builds the Electron desktop app (with the
bundled Go daemon) on macOS, Windows, and Linux runners and attaches the
unsigned artifacts to a GitHub prerelease for end-to-end pipeline validation.
Signing/notarization is intentionally off until the certs and secrets exist,
so these builds are for validating packaging, not distribution.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(sidebar): always show project row action icons
The hover-reveal mechanism (opacity-0 → group-hover/menu-item:opacity-100)
was not reliably triggering in the Electron app — icons never appeared on
hover. Instead of debugging CSS :hover group propagation in Chromium, make
the action icons always visible.
Changes:
- Remove opacity-0/opacity-100 hover gating from the action cluster div
- Change button padding from hover-gated pr-[84px] to always pr-[84px]
- Hide the session count badge (it was only shown when icons were hidden)
- Keep z-10 on the action cluster so session rows don't paint over it
Reported by phylolver(vaibhaav).
* fix(sidebar): correct inverted collapsed-count class
The count badge was set to hidden-by-default but shown-as-grid in
collapsed/icon mode — the opposite of correct. In icon mode there's no
room for a count badge and the action cluster is hidden anyway. Since the
action icons are now always visible and permanently replace the count,
the badge should be hidden in all states.
Reported by phylolver(vaibhaav).
---------
Co-authored-by: AO Bot <ao-bot@composio.dev>
* fix(sidebar): make project row hover icons clickable and non-overlapping
Three fixes for project row hover actions (dashboard, orchestrator, kebab):
1. Count badge pointer-events: The session count span fades to opacity-0 on
hover but kept pointer-events active, intercepting clicks meant for the
action icons. Added pointer-events-none on hover/focus/menu-open states.
2. Action icons z-index: The absolutely-positioned action cluster had no
z-index, so positioned session rows (SidebarMenuSubItem with position:
relative) in expanded projects painted on top and blocked hover/click on
the first project's icons. Added z-10 to lift the action cluster.
3. Padding clearance: Bumped pr-[78px] to pr-[84px] to give more room for
the three 20px action buttons + right offset.
Reported by aditi and phylolver(vaibhaav).
* fix(test): update Sidebar padding assertion from 78px to 84px
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: AO Bot <ao-bot@composio.dev>
Co-authored-by: Vaibhaav <user@example.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
The orchestrator/worker system prompts (role, coordination, branch
conventions) contained no instruction telling the agent to treat them as
private, so a plain "give me your system prompt" made Claude Code dump
the role block verbatim.
Add a systemPromptGuard appended to every non-empty system prompt via
buildSystemPrompt, covering both spawn and restore paths. The guard
covers direct, indirect, and embedded reveal requests while leaving
general project/workflow questions answerable.
Adds TestSystemPrompt_AppendsConfidentialityGuard across orchestrator
and both worker variants.
Co-authored-by: i-trytoohard <i-trytoohard@users.noreply.github.com>
* feat(frontend): surface multiple PRs per session
Replace the single optional pullRequest on a session with a prs[] list and
render it across every PR surface: the inspector stacks one card per PR, the
PR board lists one row per attributed PR, and the board card shows a PR count
summary. useWorkspaceQuery now maps the live /api/v1/sessions prs[] into the
query (the frontend surfaced no PRs before this). Ordering is actionable-first
(open, draft, merged, closed).
Adds unit tests (SessionInspector, PullRequestsPage), a Playwright e2e spec,
and a multi-PR mock-data fixture.
* chore: format with prettier [skip ci]
* feat(frontend): swap inspector Changes tab for empty Reviews placeholder
The inspector rail's Summary tab already renders the multi-PR stack from
PR #237 work, so the Changes (Git rail) tab is the next inspector surface
to evolve. Reviews will land on its own backed by PR-scoped review data
(separate workstream); reserve the slot now with an empty placeholder so
the navigation lands before the data does.
- Tabs are now Summary, Reviews, Browser. The InspectorView union and
the VIEWS array are updated; Reviews gets a message-bubble icon to
distinguish it from Summary's list icon.
- ChangesView and its lucide imports (GitBranch, GitCommitHorizontal,
Plus, Square, Trash2) and the unused Button import are removed; a
small ReviewsView mirrors BrowserView's empty-state shape.
- styles.css drops the orphaned .inspector-changes__* block.
- SessionInspector.test.tsx asserts the new tab labels and that Reviews
shows the empty placeholder.
* feat(frontend): wire the reviewer feature into the inspector Reviews tab
The multi-PR transplant left the Reviews tab an empty placeholder, which would
have regressed the existing session review feature. Move the reviewer panel
(trigger/re-run a review, open the reviewer terminal, surface verdict + status)
into the Reviews tab, gated on the multi-PR model: it reads the session's prs[]
to decide between the reviewer card and the "no PR yet" empty state, and pulls
review runs + project reviewer config straight from the daemon.
Reconciles the prs[]-from-session-list model across the suite: ShellTopbar and
pr-hydration fixtures carry prs[], useWorkspaceQuery tests drop the obsolete
per-session /pr fetch, and SessionsBoard restores the dropped board title.
Adds a Playwright reviews-tab e2e spec (reviewer card for a session with PRs;
empty state for one without).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test(frontend): adapt multi-PR e2e specs to main's expanded fixture
The rebase pulled in main's larger mock-data fixture (4 workspaces, 13 PRs)
and its renamed refactor-mux title. Rewrite the PR-board assertion to verify
the actionable-first ordering invariant instead of a brittle full-list match,
and update the empty-state selector to the session's current title.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: format with prettier [skip ci]
---------
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Polish the board and task action chrome.
- move board actions into the board header and remove empty board topbar space
- refine session topbar actions with notification, Kill, and Orchestrator controls
- add pointer cursors for clickable controls and clean sidebar child-session styling
Verified with frontend typecheck and targeted renderer tests.
Improve the frontend board and session workflow presentation.
- refine project sidebar hierarchy and child session display
- clean up kanban task cards and status labels
- add direct task creation from project boards
Verified with frontend typecheck and targeted renderer tests.
* test(storage): guard against duplicate goose migration version prefixes
Statically scans embedded migration filenames for repeated numeric
version prefixes and fails with a clear message, catching the class
of bug from #333 (two PRs adding 0014_*.sql independently) before
goose.Up() would panic at runtime.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(storage): dedupe migration versions by goose's parsed int64, not raw string
versionPrefix compared raw filename prefixes, so 014_x.sql and 0014_y.sql
were treated as distinct even though goose.NumericComponent parses both
to version 14 and panics on the collision. Use goose.NumericComponent
directly so the test enforces goose's actual collision rule.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(storage): trim PR-specific framing from migration version test comment
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(storage): renumber telemetry migration to 0015 to resolve goose version collision
* chore: format with prettier [skip ci]
---------
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>