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(daemon): self-heal a stale run-file instead of refusing to start
On Windows the desktop supervisor can only TerminateProcess the daemon
(no POSIX signal reaches a detached child), so the daemon's graceful
shutdown never runs and ~/.ao/running.json is never removed. The leaked
file survives into the next launch, and because Windows reuses PIDs
aggressively the recorded PID usually belongs to an unrelated process.
The startup pre-flight trusted PID liveness alone (runfile.CheckStale ->
processalive.Alive), so it concluded a daemon was "already running" and
exited with "refusing to start" on every restart. A dead daemon then
makes the renderer's loopback REST calls (e.g. Spawn Orchestrator) fail
silently.
Verify the recorded port is actually served by an AO daemon with the
recorded PID (a /healthz probe matching service + pid, the same ground
truth inspectDaemon already uses) before refusing. A run-file left by a
crashed, hard-killed, or reused-PID predecessor is treated as stale and
overwritten, so startup is robust to a leaked run-file from any cause.
Fixes#256
* fix(release): build the desktop daemon natively on each target OS
build-daemon.mjs compiles the bundled `ao` daemon with the build host's
GOOS and names it off the host platform (ao.exe only when the builder is
Windows). The release workflow ran only on macos-latest, so a Windows
package would ship a macOS binary named `ao` with no `ao.exe`, and the
app could not launch a valid Windows daemon ("This program cannot be run
in DOS mode" / binary not found).
Run the release as a per-OS matrix (macOS + Windows) so host == target
and each installer bundles a daemon compiled for its own platform, and
pin the Go toolchain with setup-go since build-daemon needs it on every
runner.
Fixes#235
* feat(terminal): Windows ConPTY support for /mux attach
Replaces the Windows stub in internal/terminal/pty_windows.go with a real ConPTY implementation backed by github.com/aymanbagabas/go-pty, so the daemon's /mux attach can stream a live terminal to the renderer on Windows.
PTYSource.AttachCommand now returns (argv, env, err). On Windows the zellij attach is spawned directly (no powershell.exe wrapper) — wrapping ConPTY startup around a shell surfaces as modal application-error dialogs — and the per-session ZELLIJ_SOCKET_DIR is delivered via the spawn's CreateProcess env block instead of an 'env -u NO_COLOR' shim. Unix continues to use the env-shim wrapper and returns nil env.
Adds go-pty v0.2.3 (+ bumps golang.org/x/sys to v0.44.0 transitively). Updates the in-process test fakes (terminal/fakes_test.go, httpd/terminal_mux_test.go) for the new signature.
* feat(zellij): discover zellij binary on Windows and raise command timeout
Defaults the zellij binary to whatever exec.LookPath finds first (preferring zellij.exe on Windows), falling back to LOCALAPPDATA\Programs\zellij\zellij.exe and ProgramFiles{,(x86)}\{zellij,Zellij}\zellij.exe so a fresh-installed Windows user gets a working runtime without setting Options.Binary.
Raises the per-command timeout from 5s to 30s on Windows: the first zellij invocation after install routinely takes longer than 5s on Windows due to filesystem/AV warmup, which was causing benign DeadlineExceeded failures during session create.
* feat(zellij,cli): Windows agent launcher trampoline for codex argv
On Windows, zellij's KDL `args` quoting cannot round-trip codex's --config key=value flags (or any argv with embedded quotes), and shell-wrapping the agent in powershell/cmd quoting is equally unsound. This adds a small launch trampoline so zellij runs a known-fixed argv and the real argv is delivered out-of-band.
How it works on Windows:
1. zellij.Runtime.writeLayout persists cfg.Argv to a temp JSON spec via the new agentlaunch package (AO_LAUNCH_SPEC env var points at the file).
2. The KDL layout runs the trampoline as `<ao.exe> launch` (windowsLaunchArgv); PATH is augmented so the trampoline resolves.
3. The new hidden `ao launch` subcommand reads the spec, deletes the temp file, and execs the real agent with cfg.Argv inside cfg.WorkspacePath.
Also adds:
- runner.Start fire-and-forget path (process_windows.go uses powershell.exe -EncodedCommand + Start-Process -WindowStyle Hidden with CREATE_NEW_CONSOLE so the daemon is not blocked on zellij's --create-background settling).
- powerShellEncodedCommand helper and switch from -Command to -EncodedCommand for the existing powershell shellLaunchSpec (avoids brittle KDL→PowerShell quoting round-trips).
Unix is unchanged: writeLayout passes cfg.Env straight through, createSession stays synchronous via runner.Run, and process_other.go is a stub that returns an error if anyone calls into the background path.
* feat(codex): Windows binary resolution, terminal compat flags, TOML literal strings
Three Windows-targeted refinements to the codex agent plugin so a default Windows install lands in a working state:
1. ResolveCodexBinary now follows .cmd/.ps1 shims to the underlying codex.exe (resolveNativeWindowsCodex + windowsNativeCodexCandidatesForShim). The npm-distributed codex shim cannot be exec'd directly under ConPTY without a shell wrapper; jumping straight to the .exe avoids that wrapper.
2. appendTerminalCompatibilityFlags adds Windows-specific args (e.g. --no-alt-screen) so codex's TUI renders correctly inside zellij's pane without the alternate-screen buffer churn that breaks ConPTY redraws.
3. hooks.go gains codexTOMLLiteralString / codexTOMLConfigString / containsTOMLControl so paths and other values with backslashes and quotes round-trip through codex's --config TOML parser using literal strings ('...') when basic strings would require unsafe escaping.
* fix(lint): paramTypeCombine in pty_unix.go, revive doc comments in agentlaunch, codex test quotes
* fix: stabilize windows zellij sessions
---------
Co-authored-by: harshitsinghbhandari <24b4506@iitb.ac.in>
Co-authored-by: Madhav <madhavkumar@microsoft.com>
Co-authored-by: Vaibhaav <user@example.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>
* Add `ao spawn` and `ao project add`; resolve project repos for worktrees
Make a registered project spawnable end-to-end from the CLI:
- DB-backed RepoResolver: the daemon resolves a project's on-disk repo
path from the projects table (replacing the empty StaticRepoResolver
that failed every lookup), so a session's worktree is cut from the
right repo.
- session_manager defaults an empty spawn branch to ao/<session-id> — a
fresh, unique branch per session, since gitworktree can't reuse a
branch already checked out elsewhere (e.g. main).
- `ao project add --path <repo>`: register a local git repo (POST /api/v1/projects).
- `ao spawn --project <id> [--harness] [--branch] [--prompt] [--issue]`:
spawn a worker session (POST /api/v1/sessions); harness defaults to the
daemon's AO_AGENT.
- Shared postJSON daemon client (reads the run-file for the port, surfaces
the API error envelope).
Stacked on #65, which lands the agent-adapter + session-manager wiring
this depends on.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Address Copilot review on #77
- `ao spawn` no longer prints a branch the sessions API doesn't return
(session metadata is json:"-"), so the output is no longer misleading.
- Unregistered/archived/no-path projects now surface a 400
PROJECT_NOT_RESOLVABLE with an actionable message instead of a generic
500: a new sessionmanager.ErrProjectNotResolvable sentinel the resolver
wraps and writeSessionError maps.
- postJSON reuses the injected Deps.HTTPClient (cloned, with a longer
timeout) instead of a fresh client, keeping HTTP behaviour stubbable.
- postJSON treats a stale run-file (dead PID) as "not running" via
ProcessAlive, matching its docstring.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Assert the project-not-resolvable sentinel in the resolver test
Greptile review: harden TestProjectRepoResolver to verify the unregistered
-project error wraps ErrProjectNotResolvable, so a future regression in the
sentinel wrapping (which the HTTP 400 mapping relies on) is caught.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Fix ao spawn 500 on long session ids (zellij socket-path overflow)
Root cause: the daemon built the zellij runtime with an empty SocketDir,
so zellij fell back to its $TMPDIR-based default (long on macOS). That
left almost none of the ~103-byte unix-socket-path budget for the session
name, so a long session id (e.g. "aoagents-agent-orchestrator-1", derived
from a long project id) was rejected by zellij with "session name must be
less than 0 characters". runtime.Create failed, the spawn 500'd, and the
worktree was rolled back (leaving an orphan ao/ branch).
- New zellij.DefaultSocketDir(): a short, stable per-user socket dir
(/tmp/ao-zellij-<uid>); the daemon uses it (and MkdirAll's it).
- ao spawn's attach hint now prefixes ZELLIJ_SOCKET_DIR so it stays
copy-pasteable against the daemon's socket dir.
- Regression test guards that the socket dir leaves >= 48 bytes for the
session name within the 103-byte limit.
Verified: ao spawn against a long-id project now succeeds (session live,
worktree created) where it previously 500'd.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(cli): guard CLI/daemon DTO drift with an e2e round-trip
The CLI keeps its own request structs (spawnRequest, addProjectRequest)
separate from the daemon's canonical DTOs (controllers.SpawnSessionRequest,
project.AddInput). Nothing verified the JSON field names agreed, so a renamed
tag on either side would compile but break at runtime.
Drive `ao spawn` and `ao project add` through the real httpd router and
controllers (fakes only at the service layer) over a real loopback round trip
via postJSON, asserting each field decodes into the right SpawnConfig/AddInput
field. Runs in the normal test lane (no extra ports/processes).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(cli,daemon): address review findings on ao spawn
- spawn: print the sanitised zellij session name (zellij.SessionName) in the
attach hint; a long/non-conforming session id is registered under a different
name, so the raw id sent users to a missing session.
- client: surface the daemon error envelope's requestId so a failed command can
be correlated with daemon logs.
- daemon: don't swallow the zellij socket-dir MkdirAll error — log it, since a
failure otherwise surfaces later as an opaque socket-bind error on every spawn.
- project: reject an embedded ".." in a project id up front; it passed the id
pattern but yielded an invalid branch (ao/a..b-1) and an opaque 500 at spawn.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: harshitsinghbhandari <24b4506@iitb.ac.in>
* feat(plugin): add agents plugin (first iteration)
Faithful copy of the agents plugin implementation from yyovil/better-ao
(internal/plugin/ -> backend/internal/plugin/) plus its PRD
(prds/plugins/agents/PRD.md), as a first-iteration proposal for review.
Imports are left at their original github.com/yyovil/better-ao/... paths and
are NOT yet reconciled to this repo's module; see PR description for the
integration deltas (module path, missing internal/utils dependency).
Co-authored-by: Claude <noreply@anthropic.com>
* Move agent adapters under backend adapters
* Keep daemon ports and session out of adapter move
* Remove Better-AO naming from flake
* Keep flake as dev shell only
* Use goimports for local formatting
* Wire session manager to per-session agent adapters
Move the Agent port into internal/ports and have the claude-code and
codex adapters implement it directly, alongside their workspace-local
activity hooks and a manifest-keyed adapter registry. Rename
RuntimeConfig.LaunchCommand to Argv and update the tmux and zellij
runtimes to match.
The session Manager now resolves a real agent adapter per session via a
new ports.AgentResolver: from cfg.Harness on Spawn and the stored harness
on Restore, so one daemon runs claude-code and codex sessions side by
side. The daemon backs the resolver with the registry; AO_AGENT selects
the default harness (default claude-code), validated at startup. Removes
the temporary noopAgent stub.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(agent): point the agent contract at internal/ports/agent.go
The Agent interface moved from internal/adapters/agent to internal/ports;
update the PRD's Goal and Agent Contract sections (and the SessionInfo
references) to match the code.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Wire the session service into the daemon
daemon.Run now builds the controller-facing session service — a session
manager over the zellij runtime, a gitworktree workspace, the shared
store + LCM, and the per-session agent resolver (AO_AGENT default,
validated at startup) — and mounts it at httpd APIDeps.Sessions, so the
session REST routes are backed by a real service. startLifecycle moves
ahead of the HTTP server so both share one LCM.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Address Greptile review: complete the live spawn path
- Spawn and Restore now install workspace-local activity hooks
(GetAgentHooks) and run the adapter's optional PreLaunch step before
launch, via a shared prepareWorkspace helper. PreLaunch is how Claude
Code records workspace trust, so its interactive "trust this folder?"
dialog can't hang the headless pane; the spawned env now also carries
AO_DATA_DIR so the installed hook commands find the store.
- claudecode and codex hook/config writes are now atomic (temp + rename)
instead of os.WriteFile, so a crash mid-write can't leave a partial
file the agent fails to parse.
- ensureWorkspaceTrusted serializes its read-modify-write under a package
mutex, so concurrent spawns to different workspaces don't drop each
other's ~/.claude.json trust entries.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test(ports): pin MetadataKeyAgentSessionID to domain.SessionMetadata json tag
The equality between ports.MetadataKeyAgentSessionID and the json tag on
domain.SessionMetadata.AgentSessionID is a hand-maintained invariant; this
test fails loudly if either side drifts.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* refactor(adapters): use ports.MetadataKeyAgentSessionID in claudecode + codex
The native session id metadata key is defined in ports for cross-package
consumption; drop the duplicated literals in each adapter so the constant
has one home.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test(codex): cover ensureCodexHooksFeatureEnabled TOML edge cases
The helper is a string editor over config.toml; pin its content
transformation for missing/empty files, existing [features] blocks,
the no-op case, and the legacy codex_hooks=true migration paths.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs(adapters): document Registry concurrency contract
Registry registration runs at daemon boot before any goroutine calls Get,
so the underlying map needs no lock; pin that contract in the doc comment
so a future change doesn't quietly introduce a race.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* style(codex): gofmt codex_test.go after constant rename
The previous commit (7c5b2a9) replaced codexAgentSessionIDMetadataKey with
ports.MetadataKeyAgentSessionID inside a map literal; the longer key threw
off gofmt's column alignment on the adjacent codexTitleMetadataKey /
codexSummaryMetadataKey lines. Caught by agent-ci's Check formatting step.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: harshitsinghbhandari <dev@theharshitsingh.com>
Introduces backend/.golangci.yml (27 linters across correctness, dead-code/
boilerplate, style, and security), wires it into CI as a blocking job, and
fixes every finding so the tree starts at zero.
Config:
- 27 linters: errcheck, govet, staticcheck, errorlint, bodyclose,
sqlclosecheck, rowserrcheck, nilerr, makezero, unused, unparam, unconvert,
wastedassign, copyloopvar, prealloc, dupl, revive (incl. exported-symbol doc
comments), gocritic, misspell, usestdlibvars, predeclared, nakedret, gosec, …
- Tuned for signal over noise: govet/shadow and gocritic hugeParam/rangeValCopy/
unnamedResult disabled (idiomatic-Go false positives); sqlc-generated code and
tests get scoped exclusions; gosec G304 excluded (paths are config/run-file/
worktree-derived, not user input); nilerr excluded in cli/status.go (probe
failures are the reported status, not a command error).
CI:
- New blocking lint job (golangci-lint-action, latest binary for Go-version
compatibility).
- go-version now read from go.mod (was pinned 1.22 while go.mod declares 1.25).
Cleanup to reach zero (no behavior change):
- errcheck: wrap deferred/inline Close()/Remove()/Rollback() with `_ =`.
- gosec: tighten dir/file perms (0755->0750, 0644->0600).
- unparam: drop always-nil error return from startLifecycle; drop unused
shellPath param (zellij PowerShell) and always-500 fallbackStatus param
(writeProjectError).
- gocritic: regexp \d, s != "", switch->if, combined appends.
- revive: doc comments on all exported symbols; rename project.ProjectRow ->
project.Row (stutter); rename `max` locals shadowing the builtin.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>