* docs: grounded spec for ao start bootstrapper + npm deprecation
Real-codebase-verified implementation spec (Track A: launcher + app-state
marker + release asset wiring). Replaces the somthing.md draft's aspirational
assumptions with file:line ground truth: correct bundle name (Agent
Orchestrator.app), publish repo (aoagents/agent-orchestrator), draft-release
+ asset-rename gaps, unsigned-build reality, and the already-wired
update-electron-app updater. Includes a dependency-ordered task breakdown
for AO execution.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs: reframe spec around Go ao start subcommand (not a JS launcher)
The npm package ships the existing Go cobra CLI (backend/cmd/ao); this effort
rewrites the `ao start` subcommand to fetch+open the desktop app. Corrections:
- releases land on AgentWrapper/agent-orchestrator (aoagents was the temporary
rewrite home; forge publisher must be repointed)
- ao start stops starting the daemon; the frontend owns the daemon
- adds the real Go CLI command surface (1.7), the npm-delivery gap for the Go
binary (1.6), and the legacy first-boot import decision (6.4)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs: separate prod vs test release targets in ao start spec
Dev/test loop must never cut a production release. Download repo + forge
publisher + npm scope are now build-time overridable:
- prod: AgentWrapper/agent-orchestrator + real package name
- test: harshitsinghbhandari/agent-orchestrator (fork) + @theharshitsingh/ao
T3/T5 now release+test against the fork and the test scope, with prod cut as a
separate gated step.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(cli): rewrite ao start to fetch + open the desktop app (T1)
ao start no longer starts the daemon (the desktop app owns it). It now
resolves the installed app (~/.ao/app-state.json marker -> stat -> known-
location scan), fetches the latest release zip and ditto-unpacks it on macOS
when absent, opens it with --installed-via=npm-bootstrap, and prints an honest
deprecation notice. releaseRepo is build-time overridable (-ldflags) so test
builds fetch from the fork. Windows/Linux fetch/open are stubbed for T6/T7.
Review fixes folded in: download() copies deps.HTTPClient and drops its 2s
loopback-probe timeout (a real release asset is hundreds of MB), and fetchApp
clears ~/.ao/staging before unpacking. Regression test covers the timeout.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(desktop): write ~/.ao/app-state.json marker on launch (T4)
The app is the sole writer of the marker ao start reads to locate the bundle.
New frontend/src/main/app-state.ts does an atomic temp+rename write mirroring
the daemon's runfile.Write, preserving installedAt/installSource across launches
and refreshing appPath/version/lastReconciledAt. main.ts hooks it into
app.whenReady ordered relocate (macOS) -> write marker -> createWindow, both
non-fatal. Bundle path is derived from process.execPath (not app.getAppPath,
which is the asar path); JSON keys match start.go's appState reader exactly.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* ci(desktop-release): overridable repo, published release, stable asset aliases (T3)
Make the bootstrapper's constant releases/latest/download URL resolve:
- forge publisher repo reads AO_RELEASE_REPO (default AgentWrapper/agent-
orchestrator); the workflow sets it to github.repository so a fork run
publishes to the fork and never to prod
- draft:false so the release is immediately live (constant URL needs it)
- add ubuntu-latest to the matrix (issue #2191)
- post-publish steps upload stable space-free aliases
(agent-orchestrator-darwin-arm64.zip, agent-orchestrator-win32-x64.exe)
matching exactly what start.go fetches
Review fix: the alias upload targeted GITHUB_REF_NAME (the git tag), but
publisher-github creates the release as v<package.json version>; retargeted to
that and relaxed the guard so workflow_dispatch also produces aliases.
Known gaps (documented inline): macOS x64 needs an Intel runner (macos-latest is
arm64-only); the Linux stable asset name awaits the deb/rpm-vs-AppImage decision.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore: format with prettier [skip ci]
* feat(npm): deliver Go ao binary as @aoagents/ao via per-platform packages (T2)
Ship the ao CLI to npm with ZERO install scripts using the esbuild model:
- packages/ao: pure-JS @aoagents/ao with a bin/ao.js shim that resolves and
execs the matching @aoagents/ao-<platform>-<arch> optionalDependency
- four platform packages (darwin-arm64/x64, win32-x64, linux-x64), each os/cpu
gated so npm installs only the host's; binary cross-compiled CGO-free
(modernc.org/sqlite), gitignored, shipped via files
- build-binaries.sh cross-compiles all four; releaseRepo keeps its prod default
Not added as root workspaces on purpose: os/cpu-restricted members make root
`npm ci` fail EBADPLATFORM (CI's api-drift job runs it). Packages publish
standalone; the shim was verified in a published-like layout.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore(npm): set @aoagents/ao packages to 0.10.0 (above existing 0.9.5)
The existing @aoagents/ao on npm is 0.9.5; the launcher must publish a higher
version so npm latest advances and existing users get the new fetch-and-open
binary on update. All five packages plus the four optionalDeps pins set to
0.10.0.
* chore: format with prettier [skip ci]
* fix(start): capture install provenance before macOS relocation
moveToApplicationsFolder() relaunches the app from /Applications without
forwarding the --installed-via arg, and code past a successful move never
runs in the staging instance. The post-move instance therefore wrote
installSource="unknown", and writeAppStateMarker's sticky logic then locked
it there, losing the npm-bootstrap provenance in the exact path it exists for.
Write the marker before relocation when --installed-via is present so the
source is persisted while the arg is still available; the post-move launch
preserves it (sticky installSource) while refreshing appPath to /Applications.
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>
* fix(daemon): do not tear down live sessions on shutdown; adopt them on boot
Remove SaveAndTeardownAll from the graceful shutdown path. Live tmux/ConPTY
sessions survive daemon exit; Reconcile on the next boot adopts them via
reconcileLive, preserving session IDs and preventing the id-increment bug.
Add runShutdownSessionLifecycle as a testable seam and narrow the
sessionLifecycle interface to Reconcile+RestoreAll only.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(daemon): make shutdown-teardown regression test falsifiable
Re-add SaveAndTeardownAll to the sessionLifecycle interface and to
fakeSessionLifecycle so TestShutdown_DoesNotCallSaveAndTeardownAll is
genuinely falsifiable: the flag flips if runShutdownSessionLifecycle
ever calls sl.SaveAndTeardownAll, making the assertion meaningful.
Name the sl parameter (was discarded with _) so the seam is visible.
RED: with a temporary sl.SaveAndTeardownAll call, test fails.
GREEN: without it, test passes. go test ./internal/daemon/... -race: 23/23.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(daemon): guard shutdown-teardown at compile time via narrowed interface
Remove SaveAndTeardownAll from sessionLifecycle so daemon.Run physically
cannot call teardown on shutdown. Delete the no-op runShutdownSessionLifecycle
seam and its test. The narrowed interface is the guard: re-introducing teardown
requires a visible, reviewable interface change.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(session): make ensure-orchestrator idempotent so POST cannot mint a duplicate
When clean=false, SpawnOrchestrator now checks for an existing active orchestrator
and returns it directly instead of always calling Spawn. Adds RED/GREEN tests.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(session): drop redundant NoCleanSkipsKills, covered by SpawnsWhenNoneExists
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(daemon): supervisor watchdog
Adds backend/internal/daemon/supervisor: a transport-agnostic watchdog
that fires onLastClientGone() exactly once when the live connection count
drops to zero and stays zero for a configurable grace period. Arms only
after the first accepted connection (headless safety: CLI ao start never
self-stops). Reconnect before grace elapses cancels the pending timer.
Mutex guards liveCount/armed/pendingTimer; sync.Once guards the callback.
Tests use net.Pipe + a fake listener (no OS sockets); all three behavior
contracts verified with -race: never fires pre-connect, fires once on
last disconnect, reconnect within grace cancels and re-arms correctly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(daemon): supervisor watchdog review fixes (data race, ErrClosed, leak, cleanup)
- Fix data race: capture liveCount into local inside lock before logging
- Replace io.EOF with errors.Is(err, net.ErrClosed) for correct production behavior; update fakeListener to match
- Fix goroutine leak: derive cancellable child context in Serve so watcher always unblocks on return
- Simplify makePipe: drop dead error return, update 3 call sites
- Remove dead defensive pendingTimer nil-check in armGrace
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(daemon): OS-native supervisor listener triggers clean shutdown
Creates platform-split Listen() in supervisor package (Unix UDS sibling
of run-file; Windows named pipe via go-winio). Wires it into daemon.Run
before srv.Run: listener failure is non-fatal so headless ao-start works.
Adds RequestShutdown() on Server and three unit tests for listen_unix.go.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(daemon): log supervisor Serve error instead of discarding it
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(desktop): supervisor link; daemon self-stops (clean) on frontend exit
Add connectSupervisor() in frontend/src/main/supervisor-link.ts: holds one
client connection to the daemon's OS-native supervisor socket (Unix domain
socket on macOS/Linux, named pipe on Windows). Retries with bounded backoff
until the daemon is up; reconnects automatically on drop.
In main.ts, connect the link from reportBoundPort (once per daemon ready
transition). Remove the quit-time daemon teardown: the before-quit handler
now only disposes the browser view. The process.on("exit") killDaemon call
is also removed. When Electron exits for any reason the OS closes the fd,
the daemon detects EOF, and self-stops after its 5s grace period. tmux and
ConPTY sessions survive and are adopted on the next boot.
killDaemon and stopDaemon are kept for the explicit user-stop path
(ipcMain.handle("daemon:stop", ...)).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(desktop): guard against daemon orphan when supervisor link is down; tidy test setup
Expose a live `connected` getter on SupervisorLinkHandle and add a
last-resort process.on("exit") kill that fires only when the link is
not actually connected (UDS never bound or addr was null), preventing
orphan daemons while keeping the OS-fd teardown path for the normal
case. Log a warning when addr is null so the skip is diagnosable.
Invert the setup.ts guard to the natural form, dropping the empty
if/else skeleton.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(core): restore promptless sessions in place (reboot recovery, no increment)
Drop the empty-prompt early return in restoreArgv that returned ErrNotResumable
when the adapter could not resume and no prompt was saved. Now control falls
through to GetLaunchCommand unconditionally: a saved prompt is replayed, an
empty prompt (orchestrator) launches fresh with the system prompt only, same id,
same workspace. Removes the only producer of ErrNotResumable and deletes the
now-dead sentinel, its service mapping, and the corresponding tests.
Frontend reference to SESSION_NOT_RESUMABLE in TerminalPane.tsx is left intact
(the handler becomes harmlessly dead; the API simply stops returning that code).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(integration): dead-live session is restored, not abandoned, after reconcile
Task 5 made promptless sessions relaunch fresh instead of hitting ErrNotResumable.
The reconcile crash-recovery path (documented in reconcileLive) terminates a
dead-live session then RestoreAll relaunches it on the same boot. This test
asserted the old promptless-stays-terminated artifact; update it to the intended
restored end state (live again, same id, one fresh runtime Create).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* style(session): replace em dashes in service.go messages and comment
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(desktop): scope supervisor link to spawn path; dispose link on explicit stop
Document that the liveness link is established only when the app spawned the
daemon: the attach path intentionally does not link, to keep an `ao start`
daemon persistent (headless safety). Also dispose the link on an explicit
daemon:stop so its reconnect loop does not retry a deliberately-stopped daemon.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore: format with prettier [skip ci]
* docs: add daemon-lifecycle adopt-on-shutdown implementation plan
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore: format with prettier [skip ci]
* fix(daemon): keep supervisor watchdog alive across transient Accept errors
Review of #2185: Serve previously returned on the first unexpected Accept error,
silently disabling the watchdog (the 'restart is caller's job' comment described
a contract the caller never fulfilled). Back off and keep accepting instead, so
a transient error (e.g. EMFILE) cannot leave the daemon unable to self-stop on
frontend death. Also drop the stale Task 3/4 planning comment.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(core): leave promptless workers terminated on restore (orchestrators still relaunch fresh)
A promptless, unresumable KindWorker session had no prompt to replay and
no native session id to resume from. Blank-relaunching it via GetLaunchCommand
would silently drop its task. restoreArgv now returns ErrNotResumable for this
case, gated on (meta.Prompt == "" && kind != domain.KindOrchestrator). Orchestrators
are promptless by design and continue to relaunch fresh with the system prompt only.
Re-introduces ErrNotResumable sentinel and its Conflict API mapping.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(desktop): re-link supervisor on attach for app-owned daemons (close lingering-daemon gap)
- runfile.Info gets Owner field (omitempty): "app" when Electron spawned,
empty for headless `ao start`. server.go reads AO_OWNER env to set it.
- daemonEnv() injects AO_OWNER=app so spawned daemons self-identify.
- Extracted establishSupervisorLink() from inline reportBoundPort code.
Spawn path calls it unconditionally; attach paths call it only when
shouldLinkOnAttach(owner) is true (owner === "app").
- Both attach paths (inspectExistingDaemon, resolveDaemonFromPort) now
read the owner from the run-file and re-link when app-owned, preventing
a lingering app-spawned daemon from self-stopping mid-session.
- Headless ao start daemons stay unlinked: persistent across app quit.
- New daemon-owner.ts + daemon-owner.test.ts (4 vitest cases, all pass).
- Go: 9 tests pass with -race; vet clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: quiet expected ErrNotResumable log in RestoreAll; note attach TOCTOU
Review polish on #2185: a promptless worker left terminated on boot is expected,
not an operational error, so log it at Warn not Error. Document the narrow
run-file re-read TOCTOU on the port-attach re-link path (worst case is linking a
headless daemon; the dispose-idempotency guard prevents any leak).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* style: gofmt the review-fix files (CI format/lint check)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
* 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>
* 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>
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>
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>
* 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>
* 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>
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: 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>
* 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>
* 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(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, 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>
* chore: format with prettier [skip ci]
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
* chore: add prettier config and CI auto-formatter
Adds .prettierrc and .prettierignore (config only, no local enforcement).
Formatting runs in CI via the prettier.yml workflow: on every push to a
non-main branch, Prettier rewrites changed files and commits the result back
using GITHUB_TOKEN. Developers never need to run Prettier locally.
Intentionally excludes husky/lint-staged — local pre-commit hooks are the
wrong layer for a formatter that the whole team doesn't need installed.
Also adds .envrc.local to .gitignore for personal local shell overrides.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Entire-Checkpoint: 27336650d2ee
* chore: format with prettier [skip ci]
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Initial buildable skeleton for the agent-orchestrator rewrite, splitting
the repo into a Go backend daemon and an Electron + TypeScript frontend.
- backend/: go.mod (Go 1.22) + main.go that compiles and prints a startup line
- frontend/: package.json, strict tsconfig.json, Electron main-process stub
- .gitignore for Node/Electron/Go/OS/editor/env artifacts
- README note describing the new two-folder structure
No app logic or architecture layering yet (routes/controllers/services/etc.
come in a later task). go build and tsc --noEmit both pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>