The Codex terminal rendered only in the top half of the pane: FitAddon
divided the pane height by a too-tall cell box (measured before the
post-open WebGL renderer and the monospace font's real metrics resolved),
under-counted rows, and sent that short grid to zellij. It never recovered
because every remaining fit trigger after the settle window was the host
ResizeObserver, and the host's height:100% box never changes when only the
row count is wrong.
Add an onRender convergence loop: each renderer repaint re-proposes
dimensions from the current measured cell box and re-fits when they differ,
converging the grid to the true row count once metrics settle, then detaches
once stable (bounded by a re-fit cap). proposeDimensions returns undefined
until the cell box is non-zero, so a fit is never accepted from an unmeasured
cell. Also listen on window resize for OS-window / DPR changes that move the
true cell box without touching the host box.
Fixes#280
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* docs: refocus README on the product, move progress to docs/STATUS.md
Rework the README around what ReverbCode is and does, drawing the
agent/runtime/tracker framing and the "how it works" flow from the legacy
agent-orchestrator README but stating only what the rewrite's code actually
implements (zellij runtime, GitHub SCM/tracker, 23 verified agent adapters,
port/adapter extensibility surface).
Move progress tracking out of the README: rename docs/status.md to
docs/STATUS.md, reconcile the README's "Status and roadmap" content into it
(SCM observer issue refs, milestone link), correct the adapter count to 23,
and drop the stray trailing markup. Update the README, AGENTS.md, docs/README.md,
and docs/stack.md references accordingly.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore: format with prettier [skip ci]
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
* feat(frontend): show dashboard and orchestrator buttons on project hover
Hovering a project row in the sidebar now reveals a dashboard button
(opens the project board), an orchestrator button (opens the running
orchestrator or spawns one), and a vertical three-dot kebab menu,
replacing the lone horizontal kebab. Closes#292.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore: format with prettier [skip ci]
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
* feat(frontend): move kill session control to topbar beside Open orchestrator
Relocate the worker "Kill session" control out of the inspector's Summary
"Danger zone" section and into the app topbar actions row, as a small
danger-tinted icon button next to "Open orchestrator". It only renders for
active worker sessions and keeps the same arm-then-confirm flow and
POST /sessions/{id}/kill behavior.
Move the kill-button tests to ShellTopbar.test.tsx accordingly.
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(skills): adapt bug-triage skill to ReverbCode stack
The bug-triage skill was copied verbatim from the upstream TypeScript
agent-orchestrator and never adapted to ReverbCode (Go + Electron).
- Target repo: ComposioHQ/agent-orchestrator -> aoagents/ReverbCode
- Stack translation: pm2/tmux/Node -> Go daemon on 127.0.0.1:3001 + Zellij
runtime adapter; packages/*.ts paths -> verified backend/ Go paths;
ao --version -> ao version; lsof :3000 -> :3001; SQLite at ~/.ao/data,
handshake at ~/.ao/running.json
- Add prominent CLI-footgun warning: bare 'ao' may resolve to a different AO
install (old npm build on :3000); build /tmp/ao or use the bundled daemon
and confirm 'ao status' shows port 3001
- Remove broken push_fix_to_github.py reference; replace 5f with a Go flow
(branch + go build/test + gh pr create for trivial fixes; ao spawn worker
for non-trivial)
- Label guidance: check 'gh label list' first, only apply existing labels,
state priority/confidence in the body otherwise
- Add pitfall: verify the bug reproduces against ReverbCode (:3001) first
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore: format with prettier [skip ci]
---------
Co-authored-by: itrytoohard <ayetrytoohard@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
The worker session inspector had no way to stop a running session from
the UI. Add a "Kill session" action in the Summary view's Danger zone
that arms a one-step confirmation, then POSTs /sessions/{id}/kill and
invalidates the workspace query so the session moves to the terminated
group. The action is hidden for already-terminated/merged sessions.
Closes#287
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(frontend): populate session.pullRequest from /pr endpoint (#251)
WorkspaceSession.pullRequest was declared but never populated, so every
Boolean(session.pullRequest) check was dead: the Summary tab gated its PR
fetch on it (never ran), and the Board card and /prs page read it directly
(always empty).
Hydrate the field centrally in fetchWorkspaces — the single place that
builds session objects for the workspace, board, PR page, and sidebar — by
fetching GET /sessions/{sessionId}/pr per non-terminated session in parallel
and attaching {number, state}. A per-session fetch error degrades to "no PR"
rather than failing the whole workspace query; terminated sessions are
skipped. GET /sessions/{sessionId}/pr stays the single source of truth, so
no new backend endpoint and no changes to the consuming components.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore: format with prettier [skip ci]
* test(frontend): verify PR hydration end-to-end for a normal project (#251)
Drives the real useWorkspaceQuery + real SessionsBoard / PullRequestsPage
for an ordinary project (from /api/v1/projects) whose session has an open PR,
mocking only the HTTP client and router. Confirms PR facts fetched from
/sessions/{id}/pr flow through the shared workspace cache into both the Board
card ("PR #278 · open") and the PR page row.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(frontend): move PR hydration integration test into __tests__/integration
Cross-component integration test belongs in a dedicated folder, separate
from the co-located unit tests. No behavior change.
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(session): support multiple PRs per session
A session can now own several pull requests (a root plus stacked
children) instead of being capped at one. The SQLite schema was already
1-session->many-PR (pr.url PK, session_id a plain FK), so this is a
behavioural change across the observe -> persist -> derive -> react
pipeline, not a migration.
- observe: the SCM observer discovers every open PR whose source branch
matches a session branch or descends from it ("branch/..." stacking),
attributing each to the owning session; the longest matching branch
wins so a child session claims its own stacked PRs.
- derive: session status is a worst-wins aggregate over all owned PRs,
with a stack model (B is a child of A iff B.target == A.source and A is
open) exposed via prs[] on every session read DTO.
- react: per-PR reactions; a stacked child blocked by an open parent is
exempt from the rebase/merge-conflict nudge (only the bottom of the
stack is eligible), and the session completes only when no PR is open
and at least one merged.
- tests: unit coverage across stack/status/observer/lifecycle, a
real-SQLite ListPRFactsForSession test for the stacked-PR read path,
and a functional end-to-end integration test driving the real store +
lifecycle + observer through attribution, completion, and stacked-child
nudge suppression.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(scm): ignore fork heads in PR attribution and persist discovered siblings before completion
Branch-prefix attribution now requires a discovered PR's head branch to live
in the project repo. A fork PR can reuse a session's branch name while its
commits live in the fork, so the previous code could auto-claim foreign work.
Carry head repo full_name from the REST list response and skip any PR whose
head repo is not the base repo.
discoverNewPRs also writes each newly discovered PR as an open baseline row
before the refresh/lifecycle pass runs. A session can own several PRs, and a
terminal observation triggers a completion check that reads all of the
session's PRs from the store. Without the early write, an open sibling found
in the same poll was not yet durable and the session could terminate while
that PR was still open.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(session): surface actionable signals from blocked stacked children; clarify worker prompt
Status aggregation previously dropped any open PR blocked by an open parent,
hiding actionable child signals (failing CI, draft, requested changes,
unresolved comments) behind the parent's status. A blocked child still cannot
merge, so its readiness signals (mergeable/approved/review-pending/open) stay
suppressed, but its problem signals now contribute to the worst-wins aggregate.
The all-blocked fallback is preserved so a session never goes dark.
The worker multi-PR prompt said independent PRs could branch off the base
branch as usual, which conflicts with branch-prefix attribution. Clarify that
a PR may target the base branch, but its source branch must stay under the
session branch namespace for AO to track it.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* fix(review): make the reviewer post to GitHub and record its verdict autonomously
The claude-code reviewer never completed a review on its own. Three defects
in the reviewer launch + flow:
- It launched with no permission mode, so a headless pane stalled on the
first tool-permission prompt and never ran gh/ao. Launch with
bypassPermissions (read-only is enforced by the prompt, not a sandbox).
- The reviewer pane got no pinned PATH, so `ao review submit` resolved to a
foreign `ao` on the inherited PATH and failed. Pin PATH to the daemon's
own dir the same way worker sessions do — export HookPATH and reuse it in
the launcher.
- The prompt did not enforce ordering. Make it post the review on the PR
via gh first, then run `ao review submit`.
Fixes#258
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(review): fall back to a comment review when self-approval is rejected
GitHub does not let an author approve their own PR, so a reviewer running
under the same account can't post an `approve`. Tell the reviewer to post
the approval as a regular comment review (COMMENT event stating it is an
approval) when the provider rejects the self-approval, instead of failing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(review): serialize concurrent triggers per worker to stop double-spawn
Engine.Trigger was a read-then-write (idempotency check -> reviewer spawn ->
InsertReviewRun) with no serialization and no backing constraint. Two near-
simultaneous triggers for the same worker at the same head SHA both passed the
GetReviewRunBySessionAndSHA check, both spawned a reviewer against the same
deterministic review-<id> handle, and both inserted a running run for one commit.
Add a per-worker keyed mutex (lockWorker) held across the whole Trigger body, so
the loser re-reads the freshly-recorded run and short-circuits to Created:false
instead of spawning. Back it with a partial unique index on
review_run(session_id, target_sha) (migration 0013) as a cross-restart safety
net; rows with an empty target_sha (head not yet observed) are excluded so they
are not blocked.
Adds a concurrency test asserting N simultaneous triggers spawn once and record
one run.
Closes#242
* fix(review): make migration 0013 dedup-safe and handle the unique conflict in Trigger
Pre-#242 daemons can already hold duplicate (session_id, target_sha)
review_run rows, on which CREATE UNIQUE INDEX fails and wedges startup.
Migration 0013 now collapses each duplicate group to a single survivor
(a completed pass over a still-running one, then newest by created_at)
before building the index.
Trigger now treats a unique-constraint hit as a fallback rather than an
error: InsertReviewRun maps it to the new domain.ErrDuplicateReviewRun
sentinel, and Trigger re-reads GetReviewRunBySessionAndSHA and returns
that run with Created:false instead of surfacing a raw error after the
reviewer may already have launched.
A worker agent session's primary topbar action read "Kanban" and
navigated to the board. Workers now get an "Open orchestrator" button
(Waypoints icon) that navigates to their project's orchestrator, while
orchestrator sessions keep the "Open Kanban" board action.
Closes#234
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Two clocks defaulted to local time.Now while the rest of the codebase writes
UTC, so ao session get showed created and updated in different timezones:
- session manager clock → spawn-stamped CreatedAt/UpdatedAt
- lifecycle manager clock → activity-driven LastActivityAt/UpdatedAt
A real spawn made this visible: createdAt came back UTC but updatedAt/
lastActivityAt were local once the agent reported activity. Default both clocks
to UTC.
Closes#214
Register the repo's actual checked-out branch as the project default so
session worktrees base off a ref that exists. Previously Config.DefaultBranch
was left empty and defaulted to "main", so a repo on master/develop/trunk
failed every spawn with BRANCH_NOT_FETCHED and had no CLI workaround.
Detection is best-effort (symbolic-ref --short HEAD); a detached HEAD or git
error falls back to the existing main default. Only persist when the branch
diverges from main, so the common main repo keeps a NULL config.
Closes#208
A bare "node_modules/**" replaces vitest's default "**/node_modules/**"
and only matches the repo root, so the tracked src/landing preview app's
nested node_modules had its vendored third-party test suites (zod, next, ...)
collected and run once those deps were installed — 20+ failures from code
that isn't ours.
Anchor it at any depth with "**/node_modules/**".
Closes#216
validateBranch returned an untyped error for a name rejected by
git check-ref-format, so toAPIError fell through to INTERNAL_ERROR 500.
Add a ports.ErrWorkspaceBranchInvalid sentinel (mirroring the not-fetched /
checked-out-elsewhere ones), wrap it in validateBranch, and map it to
INVALID_BRANCH (400). Completes the residual of #152 Bug 3, which typed the
not-fetched and checked-out-elsewhere cases but left the invalid-format case
collapsing to 500.
Closes#212
An unknown --harness was only caught at the agent-registry lookup deep in
Spawn, after the seed row and worktree were already created: the untyped
error collapsed to INTERNAL_ERROR 500 and left a terminated orphan row.
Validate the harness against the registry before any durable state is
created and return a typed ErrUnknownHarness mapped to UNKNOWN_HARNESS (400).
Sibling to #152 Bug 6 (unknown binary on PATH), which did not cover a harness
with no registered adapter.
Closes#210
* feat(review): configurable AO code review backend (V1)
Add per-project configurable code review of a worker's PR. A reviewer
agent runs one-shot over the worker's own worktree and posts its result
to the PR; the worker picks the feedback up through the existing SCM
observer review-nudge path.
- domain: ProjectConfig.reviewers (+ default reviewer harness), Review /
ReviewRun types and verdict/status vocab.
- storage: review + review_run tables (0011), sqlc queries, store methods.
- service/review: rewrite the in-memory stub as a persisted ReviewService
(Trigger/Submit/List) with a reviewer Runner over agent resolver +
runtime; ports.PRReviewPoster implemented on the GitHub adapter.
- http: session-scoped routes POST /sessions/{id}/reviews/trigger,
POST .../submit, GET .../reviews; regenerated OpenAPI + TS types.
- cli: ao review trigger|submit|list.
- frontend: adapt ReviewDashboard to the per-worker reviews API.
Closes#192
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* refactor(review): address review — drop submit/poster/CLI, default reviewer to worker harness
Per PR #197 review feedback:
- Reviewer agent posts its review to the PR itself, so remove the
ports.PRReviewPoster port, the GitHub review poster, the submit HTTP
route + DTO, and the service Submit method (#1, #4, #7).
- Trigger spawns the reviewer agent over the worker's worktree with its
own review prompt, mirroring the session launch flow (resolve agent by
harness -> argv -> runtime.Create) (#8, #9).
- Default reviewer harness reuses the worker's harness when supported,
falling back to claude-code; reviewer config stays independent of the
worker override (#5, #6).
- Drop the `ao review` CLI for this PR's scope (#2, #3).
Regenerated OpenAPI + TS types.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(review): restore ao review submit (records verdict+body in AO)
Per maintainer request, bring back `ao review submit`. AO records the
reviewer's verdict and body on the review_run and marks the pass complete;
it does not post to GitHub — the reviewer agent posts its review to the PR
itself.
- storage: add review_run.body (0011), persist via Insert/UpdateReviewRunResult.
- service: restore Submit (no SCM poster) storing verdict + body.
- http: restore POST /sessions/{id}/reviews/submit + SubmitReviewInput.
- cli: ao review submit [worker] --verdict --body (worker from arg/--session/$AO_REVIEW_WORKER).
- runner: reviewer prompt instructs posting to GitHub and recording via ao review submit.
Regenerated OpenAPI + TS types.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* refactor(review): move reviewer runner to its own package; sharpen prompt
Per PR #197 review:
- Move the concrete reviewer runner out of the service layer into a new
internal/review_runner package (package reviewrunner), beside other
orchestration packages like session_manager. The service keeps only the
Runner interface + RunSpec it depends on; the agent-resolver + runtime
launch flow lives in review_runner.
- Sharpen the reviewer prompt: tell the agent to diff against the PR base,
focus on high-confidence findings, post via `gh pr review`, and record
the result with `ao review submit`; review-only (no commits/edits).
- Add unit tests for the runner.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* refactor(review): simplify review_run schema; provider-agnostic reviewer prompt
Per PR #197 review:
- review_run: status default 'running' (drop 'pending'), drop CHECK
constraints on status/verdict, drop the updated_at column and the
session/iteration index. Propagated through queries, domain, store,
service, and tests.
- Reviewer prompt no longer hardcodes GitHub/gh commands — it instructs the
agent to use whatever review tooling the provider offers, keeping the
flow extensible across SCM providers.
Regenerated sqlc + OpenAPI/TS.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* refactor(review): launch reviewer before persisting the run
Trigger now spawns the reviewer agent first and then writes the review_run
with a status derived from the launch outcome (running on success, failed
if it never started), instead of inserting a running row and correcting it
to failed afterwards.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* refactor(review): pluggable reviewer registry distinct from worker harnesses
Reviewers are now their own pluggable adapter set, separate from the worker
agent registry — adding a reviewer (claude-code today, greptile tomorrow) is
a one-line registration that does not widen the worker harness vocabulary,
and a worker harness does not automatically become a valid reviewer.
- domain.ReviewerHarness: a distinct vocabulary (AllReviewerHarnesses) with
its own IsKnown; ReviewerConfig/Review/ReviewRun use it. ResolveReviewerHarness
reuses the worker harness only when it is itself a supported reviewer, else
falls back to claude-code.
- ports.Reviewer: a reviewer-specific contract (ReviewCommand → argv + env)
that models one-shot / non-prompt CLIs natively instead of forcing every
reviewer through the worker's interactive GetLaunchCommand(Prompt:...).
- internal/adapters/reviewer: a separate registry + resolver (mirrors the
worker agent registry) with the claude-code reviewer adapter, which owns the
review prompt and reuses the worker claude-code launch construction.
- review_runner resolves via the reviewer registry (not the worker
AgentResolver) and merges AO_REVIEW_WORKER into the adapter's env.
- daemon wires the reviewer resolver. Registry/domain parity is test-enforced.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(review): cover run-scoped reviewer submit
* fix(api): update generated review submit schema
* refactor(review): split core engine (internal/review) from API service
Move the review orchestration (Trigger/Submit/List, run-id generation,
deps, RunSpec/Runner, sentinels) into a transport-independent core package
internal/review (Engine). internal/service/review is now a thin API-flow
boundary: the controller-facing Manager interface + a Service that delegates
to the engine + error re-exports.
This keeps the service layer to API concerns and lets the same engine back a
future in-process CLI trigger without going through HTTP. review_runner now
depends on the core package; daemon builds the engine and wraps it in the
service. No API/schema changes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(review): commit-aware trigger, reviewer handle for UI, no env vars
Reworks the review trigger lifecycle and drops env-based coupling:
- review_run gains target_sha (the reviewed commit) and drops iteration.
A repeat trigger for the same PR head short-circuits to the existing run.
- review gains reviewer_handle_id: the live reviewer pane's runtime handle,
reused across passes and exposed in the reviews API so the UI can attach
its terminal over /mux.
- Trigger flow: if a live reviewer pane exists and a new commit arrived,
message it to re-review; otherwise spawn a fresh reviewer. The run is
recorded only after the reviewer is launched.
- No environment variables: the reviewer adapter embeds the explicit
`ao review submit --session <w> --run <id>` command in the spawn prompt
and the re-review message. CLI submit requires --run/--session (no env
fallbacks).
- Merge review_runner into internal/review as a Launcher (spawn/notify/alive).
- Trigger returns 201 for a new pass, 200 when reusing an existing run.
Regenerated sqlc + OpenAPI/TS.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* refactor(review): author the reviewer prompt centrally, not in the adapter
Mirror the worker model (session_manager builds the prompt; adapters just
place it via LaunchConfig.Prompt). The reviewer prompt now lives in
internal/review/prompt.go and is passed through ports.ReviewInvocation.Prompt;
the claude-code reviewer adapter just feeds inv.Prompt to its launch command
and returns it as the re-review message. One-shot CLI reviewers may ignore it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* refactor(review): split reviewer prompt into system+task, mirroring buildSpawnTexts
Mirror session_manager.buildSpawnTexts for the reviewer: a standing role goes
in the system prompt, the per-pass task (PR/commit + exact `ao review submit`
command) goes in the user prompt. internal/review/prompt.go now returns
(prompt, systemPrompt); both flow through ports.ReviewInvocation and the
claude-code adapter places them via LaunchConfig{Prompt, SystemPrompt}. The
re-review message reuses the per-pass prompt (role already established in the
running pane).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Vaibhaav <user@example.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
A spawn with no explicit harness ran the daemon default (claude-code) but
stored an empty harness: effectiveHarness returned "", seedRecord persisted it,
and the empty->default resolution lived only inside agentRegistry.Agent. The API
then omitted harness and the UI defaulted to "codex" — mislabelling a Claude
Code session.
Inject the daemon's default agent (AO_AGENT / config.DefaultAgent) into the
session manager and resolve an unspecified harness to it before the seed row is
written, so the stored/returned harness matches the agent that actually runs.
Closes#220
* Use ~/.ao as canonical state home
* chore: format with prettier [skip ci]
---------
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
* docs: sync documentation with current state of main
The rewrite is further along than several docs claimed. Bring the docs
in line with the actual code on main:
- status.md: rewrite the stale "session HTTP routes not wired yet" /
"next integration work" framing into a shipped vs in-flight breakdown.
- cli/README.md: document the full product command surface (project,
session, spawn, send, orchestrator) instead of "not present yet".
- architecture.md: correct the package layout (service/{pr,review},
observe/scm, observe/reaper, daemon, config) and add the no_signal
status to the derivation precedence.
- backend-code-structure.md: add service/review and observe/scm.
- README.md: expand the agent-adapter list (20+), add project set-config,
and describe the frontend as the real wired supervisor it is.
- AGENTS.md / docs/README.md: drop "placeholder frontend" wording and
add the agent-adapter doc to the index.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore: format with prettier [skip ci]
* fix(session): remove orchestrator kickoff auto-prompt on spawn
Spawning a session without an explicit prompt injected a "Get oriented..."
kickoff turn for orchestrators, which surfaced as an unsolicited message to
the orchestrator. Drop the auto-prompt so a promptless spawn delivers nothing
to the agent, leaving it idle at an empty input box.
Closes#226
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(session): re-apply derived system prompt on agent resume
Restore re-derives the standing system prompt but only handed it to the
fresh-launch fallback, not the native GetRestoreCommand path, so a resumed
orchestrator/worker lost its role instructions. Pass SystemPrompt through to
the restore command too, matching adapters that re-append it on resume.
Also fix the recordingAgent test double to return ok=false when there is no
agent-session id, mirroring every real adapter, so the fallback-launch path is
actually exercised. These three TestRestore_* cases were red on main since #222.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat(sidebar): restore remove-project functionality
#224 deleted the per-project "remove project" feature (and its tests)
instead of completing the wiring #222 left half-applied. Restore it:
- Sidebar: add onRemoveProject to SidebarProps, thread it to
ProjectItem, re-add the removeError/isRemoving state and the confirm
handler, render the "Project actions" kebab (MoreHorizontal) with a
destructive "Remove project" item (Trash2), and re-add the row
hover/focus/open padding + count-fade classes.
- _shell: re-add the removeProject handler (DELETE /api/v1/projects/{id}
+ drop the project from the workspace cache) and pass it to Sidebar.
- Sidebar.test: restore the confirm-before-remove and cancel-aborts
coverage alongside the existing class assertions.
Rebuilt on top of #224. Full renderer suite green (127 tests).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore: format with prettier [skip ci]
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
* feat(frontend): rebuild Electron desktop UI as a React + Vite renderer
Replaces the skeleton Electron frontend with a full React 19 + TypeScript
renderer (Vite, electron-forge, contextBridge preload), plus the backend
additions it needs.
Renderer:
- TanStack Query + EventTransport (CDC SSE on /api/v1/events)
- TanStack Router file-system routing (hash history for the file:// origin)
- Tailwind + shadcn/ui, react-resizable-panels, Zustand UI state
- @xterm/xterm per-session PTY over /mux WebSocket + WebGL addon
- openapi-typescript + openapi-fetch types off openapi.yaml
- electron-forge packaging + update-electron-app auto-updater
- Vitest + RTL · Playwright
Backend:
- cors.go — allowlist-only CORS, handles Private Network Access preflight
for app:// renderer -> loopback daemon
- session.TerminalHandleID exposed in domain + OpenAPI spec
- project.Path added to OpenAPI spec, service, store, and tests
DESIGN.md documents the emdash-matched dark UI (tokens, blue accent, status
glyph spec, orchestrator-led layout).
Co-authored-by: Ashish Huddar <ashish.hudar@gmail.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: b1e334c1e54a
* feat(terminal): port yyork's terminal rendering architecture
XtermTerminal becomes a self-contained, dependency-free renderer component
(yyork's xterm-terminal.tsx pattern):
- Nothing writes into the buffer at mount — status/empty-state is DOM chrome.
Fixes the startup crash (xterm Viewport.syncScrollArea reading renderer
dimensions on a zero-sized panel).
- Multi-trigger fit (rAF + 50/250ms settle + fonts.ready + ResizeObserver):
FitAddon must re-measure after monospace font metrics settle or it
over-counts columns. xterm only fires onResize on real grid changes, so
repeated fits don't spam the PTY.
- Unicode11 width (agent CLIs print emoji/wide glyphs), WCAG-AA minimum
contrast, WebGL→canvas renderer fallback, full ANSI-16 palette per DESIGN.md.
TerminalPane keeps ONE terminal instance across session switches — the
attachment effect re-points the mux and RIS-resets the screen instead of
remounting (a keyed remount drops the warm GPU surface mid-switch).
useTerminalSession: resize debounced 100ms trailing (one SIGWINCH per pane
drag, not dozens); the "Attaching…" writeln is gone (banner chrome covers it).
Test infra: vitest never loaded vite.renderer.config.ts after the forge split
(it only auto-discovers vite.config.*) so the whole suite ran without jsdom.
Point the test script at the config and type it via vitest/config. Fix
pre-existing type errors (notarytool field, maker-zip config, named
updateElectronApp import). 97/97 passing, typecheck clean.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Entire-Checkpoint: eb0f94fcf914
* fix(spawn): don't send base branch; surface real API errors
Two bugs found spawning a worker from the modal:
1. "Based on: main" sent branch:"main" in the POST, but git can't add a
second worktree on a branch already checked out (main lives in the repo
root) — the daemon returns 409 BRANCH_CHECKED_OUT_ELSEWHERE. The base
branch must be OMITTED so the daemon mints a fresh ao/<sessionId> off the
project default. Only a non-default branch (resume an existing session
branch) is sent through.
2. The daemon's error body is {error,code,message,requestId}; App.createTask
did String(error) on it → the modal showed "[object Object]". Add
apiErrorMessage() to unpack message/error from the structured body, with
Error/string fallbacks.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Entire-Checkpoint: d3e718d393cd
* feat(terminal): Nerd Font glyph support via --font-mono (yyork pattern)
The terminal now resolves its fontFamily from the --font-mono CSS token
(styles.css @theme), which leads with the Nerd Font family stack
(JetBrainsMono Nerd Font Mono first). Agent TUIs get powerline separators
and file-type icons; box-drawing stays renderer-rasterized.
Mirrors yyork exactly: no font is bundled — the stack names system-installed
Nerd Fonts and the browser picks the first present, falling back to plain
monospace (no icon glyphs) when none are installed.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Entire-Checkpoint: 28120fa527a7
* chore(frontend): set up shadcn/ui foundation
Prep for the route-parity port: build new screens from shadcn primitives.
- components.json: Tailwind v4, css=styles.css, cssVariables, lucide, "@/" aliases
- "@/" -> src/renderer alias in tsconfig (paths) + vite (resolve.alias)
- fill the shadcn token gaps in @theme (card-foreground, input, destructive,
destructive-foreground) mapped to existing emdash tokens so `shadcn add`
components render on-brand without touching the design system
- add Card primitive (first use: Phase 1 board)
Did NOT run `shadcn init` (it would overwrite styles.css and wipe the emdash
tokens); the @theme already maps shadcn semantic names onto emdash raw tokens.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Entire-Checkpoint: 9b9c8391e52c
* refactor(renderer): persistent _shell layout + per-route pages (projects vocab)
Phase 0 of the route-parity port. Replaces the single state-driven <App> with
real TanStack Router pages behind a persistent _shell layout, the foundation
every ported screen builds on.
- _shell.tsx: pathless layout owning the Sidebar + shared state (workspace
query, daemon status, spawn modal, create project/task, theme, shortcuts);
child routes render into <Outlet>. The daemon-status effect runs once here.
- Router owns selection: ui-store sheds view/selectedSession/selectedWorkspace
(now route params); keeps only theme/sidebar/workbenchTab. Sidebar/SideRail
navigate via router and read active state from useParams.
- Routes (projects vocabulary): / -> SessionsBoard (new board home, replacing
the orchestrator-terminal home), /projects/$projectId -> scoped board,
/projects/$projectId/sessions/$sessionId and /sessions/$sessionId ->
SessionView (Topbar + terminal + git rail).
- Terminal persistence: it lives on the session route, so session->session is
a param change (TanStack keeps the route mounted -> mux re-points, no
remount); leaving for the board unmounts it and the server ring replays on
return.
- shell-context.ts hands daemonStatus/openSpawn/create* to route content.
Removed the monolithic App.tsx (+ App.test.tsx, whose create/spawn coverage
moves to route/hook-level tests in Phase 5) and the old workspaces.* routes.
shadcn Card used for the board cards. typecheck clean, 91 tests pass.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Entire-Checkpoint: d10b22f0d841
* feat(board): attention-zone kanban home
Phase 1: SessionsBoard becomes the real kanban, porting agent-orchestrator's
getAttentionLevel state machine (packages/web/src/lib/types.ts) as a pure
function rebound to reverbcode's SessionStatus.
- attentionZone() buckets a session into urgency-ordered zones — merge (one
click to clear, leftmost) → action (needs-you: needs_input/ci_failed/
changes_requested, the collapsed respond+review) → pending (waiting on
reviewer/CI) → working → done (archive).
- Board renders a horizontal column per non-empty zone; cards navigate into
the session route. shadcn Card for cards. Styled to DESIGN.md (emdash
hairlines, status dots, accent), not agent-orchestrator's tokens.
- 13 zone-mapping unit tests. typecheck clean, 104 tests pass.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Entire-Checkpoint: d315acacf784
* feat(settings): project settings form (Phase 2)
/projects/$projectId/settings — a settings page on reverbcode's own
ProjectConfig shape (not agent-orchestrator's agent/runtime/tracker/scm,
which the Go daemon doesn't have). Reuses agent-orchestrator's form structure:
read-only identity card + editable config.
- Reads GET /api/v1/projects/{id} (config + identity), saves via
PUT /api/v1/projects/{id}/config. The PUT replaces the whole config, so the
form merges edited fields over what loaded (keeps env/symlinks/postCreate
it doesn't expose).
- Editable: defaultBranch, sessionPrefix, default worker/orchestrator agent,
model override. React Query for load + mutation with inline save state.
- shadcn select + label added; settings gear in the project board header.
typecheck clean, 104 tests pass.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Entire-Checkpoint: 0633ebde603b
* feat(session): PR inspector in the session rail (Phase 2)
Ports agent-orchestrator's SessionInspector onto reverbcode's SessionPRFacts
(GET /api/v1/sessions/{id}/pr -> {prs: SessionPRFacts[]}). Mounts above the
git rail on the session route; renders nothing when the session has no PR.
- Shows PR number + state badge, and CI / mergeability / review facts with
tone derived from the fact string (pass/fail/pending), plus an unresolved
review-comments flag.
- React Query, fetched only when the session has a PR.
Completes Phase 2 (project board reuse + settings + inspector).
typecheck clean, 104 tests pass.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Entire-Checkpoint: c75dadfd45b8
* feat(prs): pull-requests board (Phase 3)
/prs — a PR board ported from agent-orchestrator's PullRequestsPage. The Go
daemon has no PR-list endpoint, so rows are derived from session PR fields
(every session carries pullRequest), sorted open/draft above merged/closed.
- Per-row Merge (POST /prs/{number}/merge) and Resolve comments
(POST /prs/{number}/resolve-comments) mutations with inline result; clicking
a row opens the session (whose inspector has the full CI/review facts).
- shadcn Table; "Pull requests" + "Review" nav added to the sidebar footer.
- /review + /reviews routes added as placeholders (the reviews board needs a
daemon backend — Phase 4); /reviews redirects to /review.
typecheck clean, 104 tests pass.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Entire-Checkpoint: 3526d847c568
* feat(reviews): code-review API + dashboard (Phase 4)
The long pole: the Go daemon had no reviews surface, so /review needed a
backend. Adds one, mirroring agent-orchestrator's reviews feature.
Backend:
- internal/service/review: in-memory reviews Manager (Run + Finding types,
List/Execute/Send). Execution is not yet wired to a real review agent —
Execute records a pending run so the surface is live; agent-backed findings
+ persistence are a follow-up (documented in the package).
- ReviewsController (GET /reviews, POST /reviews/execute, POST /reviews/{id}/send),
wired through api.go + daemon.go (constructed, not nil — actually serves).
- genspec: reviewOperations() + tag + schemaNames; openapi.yaml + schema.ts
regenerated. apispec parity/drift tests pass, go build + go test green.
Frontend:
- ReviewDashboard reads GET /reviews, lists runs with status + findings, lets
you pick a worker and Run review (execute) and Send a run. Replaces the
placeholder /review route. shadcn Card/Badge/Select.
Verified live: GET /reviews -> 200, POST /reviews/execute -> created run.
typecheck clean, 104 frontend tests pass.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Entire-Checkpoint: ce16c62dfdb0
* chore(renderer): Phase 5 polish — route prefetch + restored spawn coverage
- Route loader: _shell prefetches the workspace list via
queryClient.ensureQueryData (parent loader runs before children), pairing
with defaultPreload: "intent" so a hovered nav target is warm on click.
workspaceQueryOptions exported so the loader and hook share one cache.
- Restored the spawn coverage dropped with App.test.tsx as a focused
SpawnWorkerModal test: the base-branch-omission regression guard (the 409
fix) + the empty-prompt gate.
Full parity surface green: 106 frontend tests, typecheck clean, backend
build + vet clean, all daemon endpoints 200.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Entire-Checkpoint: 284ae668ffea
* feat(board): match agent-orchestrator's board verbatim
Per explicit request to mirror agent-orchestrator's app exactly (overriding
DESIGN.md/emdash for this screen). Rebuilds SessionsBoard from its actual
source (Dashboard + AttentionZone + SessionCard + mc-board.css), using its
exact tokens and values:
- 4 equal-width columns (grid 1fr), left->right flow: Working -> Needs you ->
In review -> Ready to merge (SIMPLE_KANBAN_LEVELS), always rendered; "done"
archived to a separate strip, not a column.
- Per-column vertical glow gradient (status-tinted top fading at 130px) +
glow dots + uppercase tinted column titles; #0a0b0d base, #15171b cards.
- Topbar: project crumb + Coding/Reviews tabs + breathing "N working" pill +
bell + blue "New worker" primary. "Board" subhead + subtitle.
- Card: status badge (dot + label) · mono id, 2-line title, mono branch line,
hairline-topped PR footer ("no PR yet").
typecheck clean, 106 tests pass.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Entire-Checkpoint: 3569e49ba7d6
* feat(theme): clone agent-orchestrator's dark palette globally
Per the verbatim-clone directive (supersedes DESIGN.md/emdash). Remaps the
:root tokens to agent-orchestrator's exact values — #0a0b0d base, #15171b
card, #f4f5f7/#9ba1aa/#646a73 text, hairline white-alpha borders, #4d8dff
accent, orange/amber/green/red status — so every screen's base shifts at once.
Adds --color-working (orange) for the working status.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Entire-Checkpoint: d70096d7f30d
* feat(renderer): clone agent-orchestrator ProjectSidebar verbatim
Rebuild the sidebar to match agent-orchestrator's ProjectSidebar: #08090b
rail, "Reverb / Code" brand with dimmed separator + collapse button,
uppercase PROJECTS label, project disclosure rows (rotating chevron +
hover-revealed New worker action + session count), nested session rows
with a 6px breathing working-dot and mono session id, and a single
Settings menu footer (Pull requests / Reviews / Search / Project
settings) plus a daemon-health dot.
Adds a shadcn dropdown-menu primitive (radix-ui unified package, matching
the existing select/label convention) for the footer menu.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: c4e1827b6142
* feat(renderer): clone agent-orchestrator session topbar
Restyle the session header to match agent-orchestrator's SessionDetailHeader:
a "Kanban" back-to-board button + hairline divider, a stacked identity
(project / title over a mono branch line with a git-branch icon), and a
StatusBadge --pill (tinted bordered pill with a 6px dot that breathes while
the agent is working). Wire onOpenBoard from SessionView to navigate back to
the project board (or home).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: dcd3e4880af5
* feat(renderer): unify board/review/PR/settings chrome verbatim
Extract the mc-board dashboard header (project crumb · Coding/Reviews tabs ·
"N working" breathing pill · bell · settings · New worker) and the 21px
subhead into a shared DashboardTopbar/DashboardSubhead, then apply it to the
review, PR, and settings screens so every dashboard surface shares one stable
agent-orchestrator top strip. SessionsBoard now consumes the shared chrome
instead of its inline copy; review/PR/settings drop their minimal h-11 headers
for the crumb+tabs+subhead treatment on the #0a0b0d base.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: 2801a9c5c0ba
* docs: record agent-orchestrator-verbatim design direction
Per explicit user decision (2026-06-10), the renderer clones the
agent-orchestrator web app verbatim, superseding the older "match emdash"
direction. Add a prominent banner at the top of DESIGN.md (reference files,
live palette, the cloned surfaces, shadcn-primitive guidance), mark the
Aesthetic Direction section as superseded, and retarget CLAUDE.md's QA rule so
future review flags divergence from agent-orchestrator instead of emdash.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Entire-Checkpoint: fe028f97d5d5
* feat(renderer): clone agent-orchestrator shell and inspector
Finish the agent-orchestrator-style renderer pass with shadcn sidebar chrome, titlebar navigation, resizable session inspector, orchestrator spawn affordances, and matching design tokens.
Co-authored-by: Cursor <cursoragent@cursor.com>
* chore: format with prettier [skip ci]
* fix: repair UI PR CI drift
Co-authored-by: Cursor <cursoragent@cursor.com>
* refactor(terminal): per-client zellij attach replaces shared PTY + replay ring
Each WebSocket client that opens a pane now gets its own `zellij attach`
PTY (attachment.go) instead of sharing one PTY whose output was replayed
from a bounded byte ring. Zellij answers every fresh attach with its full
init handshake (alt screen, SGR mouse tracking, bracketed paste) and a
faithful repaint — the ring replay lost exactly that handshake, leaving
late subscribers without mouse reporting (dead wheel scroll). The cost is
one zellij client process per open pane per connection, which the zellij
server is built for (yyork ships the same model).
ring.go and session.go (fan-out, replay buffer) are deleted; manager.go
now tracks per-client attachments with liveness gating, and pty_unix.go
answers every resize frame with an explicit SIGWINCH.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(renderer): re-assert settled terminal resize; align docs with per-client attach
After each debounced resize settles, send one follow-up resize frame with
the same grid (RESIZE_REASSERT_MS). xterm only fires onResize on actual
grid changes, so a resize update the zellij client loses (raced mid-attach
or coalesced during a drag) would otherwise desync the session layout from
the pane until the next real change. The backend answers every resize
frame with an explicit SIGWINCH, so the re-assert is a no-op when already
in sync.
Comments in the terminal hook/components now describe the per-client
attach model (fresh server-side `zellij attach` per open, no replay ring).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(renderer): full-width shell topbar; retire per-view topbars and review dashboard
The shell now owns a single full-width ShellTopbar (status pill, history
arrows, notifications, kanban/inspector toggles) with the sidebar pinned
below it, replacing the per-view Topbar/DashboardTopbar pair; board pages
get a lightweight DashboardSubhead. The standalone review dashboard and
its /review(s) routes are removed — review state lives on the PR board.
Approved divergence from the AO reference (full-height sidebar) recorded
in DESIGN.md; macOS traffic lights re-centered on the 56px header row.
Also hardens the session view around rrp v4:
- inspector defaultSize re-derived per panel mount (orchestrator → worker
navigation kept SessionView mounted while the panel remounted), and the
imperative expand/collapse effect no longer races panel registration
- onResize writes gated on data-separator="active" so flex-grow
transition frames can't bounce the store (dead-looking toggle button)
- findProjectOrchestrator skips terminated orchestrators so the topbar
offers Spawn instead of attaching to a dead zellij session
- inspector resize handle gets a visible 1px divider at rest
- playwright specs for history arrows + inspector toggle; test-results/
gitignored
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* docs: document Electron app dev quick start
Add an "Electron app (dev)" section: npm install + npm run dev under
frontend/, with the explicit heads-up that the app does not start the
daemon — it attaches over loopback to a daemon started via `ao start`
(plus npm run dev:web for renderer-only work in a browser).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* chore(fork): ignore local agent session dirs
Fork-only ignore entries (.entire/.claude/.gstack) — must not be included
in upstream PRs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* chore: format with prettier [skip ci]
* feat: align backend session lifecycle with workspace runtime updates
* refactor: replace spawn modal with shell-native worker controls
* chore: add shared daemon launch helper and docs updates
* chore: format with prettier [skip ci]
---------
Co-authored-by: Ashish Huddar <ashish.hudar@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
* chore: add skills from agent-orchestrator
Copy the skills directory from AgentWrapper/agent-orchestrator into
<repo_root>/skills/, preserving structure.
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>
Wrap handleInspectorResize in useCallback so rrp v4's panel registration
useLayoutEffect (which includes onResize in its dep array) does not
de-register/re-register the inspector panel on every render.
Without this, any re-render of SessionView (workspace data arriving,
daemon status change, etc.) created a new handleInspectorResize reference,
triggering rrp's cleanup → re-registration cycle. With React 19's automatic
batching, this window reliably overlapped with the expand()/collapse()
useEffect on initial sidebar click, causing the "Panel constraints not found
for Panel inspector" throw that tore down the session view.
Closes#185.
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>
* 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>
* 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>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Orchestrator role definitions and worker coordination hints were being
prepended/appended to the user-facing prompt string. They now go into
SystemPrompt in LaunchConfig so agents receive them as standing instructions
rather than part of the human's task request.
Closes#182
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(workspace): name orchestrator worktree orchestrator/{prefix}-orchestrator
Orchestrator sessions now get a dedicated worktree path under
orchestrator/{prefix}-orchestrator within the project directory,
matching the pattern described in issue #184.
Workers retain the existing {sessionID} naming. The session prefix
falls back to the first 12 chars of the project ID when no explicit
SessionPrefix is configured.
Closes#184
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(lint): remove unnecessary string conversion in sessionPrefix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(lint): drop redundant string() casts — project.ID is already string
- Replace free-text AgentConfig.permissions field with a Select showing
the four valid modes (default, accept-edits, auto, bypass-permissions)
so users can't enter invalid strings (#186).
- Delete the non-functional Notifications bell button from Topbar and
DashboardTopbar; remove the now-unused Bell import (#188).
Closes#186Closes#188
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces os.UserConfigDir() + "agent-orchestrator/..." fallback with
os.UserHomeDir() + ".ao/..." in resolveRunFilePath() and resolveDataDir(),
so the default layout is:
~/.ao/running.json
~/.ao/data/
AO_RUN_FILE and AO_DATA_DIR env overrides are unchanged.
Closes#183
* ci: run the frontend vitest suite on pull requests
The renderer suite was dead for months with nothing noticing — no workflow
executed it (and until #171 it could not even run: vitest auto-loads only
vite.config.ts / vitest.config.ts, which the repo lacked). This job keeps
the revived suite alive.
Typecheck is intentionally left out until the pre-existing forge.config /
update-electron-app type errors are fixed.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(frontend): regenerate package-lock.json in sync with package.json
The committed lock was missing dozens of entries (ansi-regex, error-ex,
minimatch, ...), so `npm ci` under CI's npm 10 hard-fails with "Missing:
<pkg> from lock file" — the new Frontend workflow caught it on its first
run. Newer local npm versions tolerated the drift, which is why it went
unnoticed. Regenerated with npm install (npm 10.9.8, lockfileVersion 3) and
validated with a clean `npm ci` + full vitest run (9/9 files, 99/99 tests).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(spawn): stop sending branch on spawn, render API errors, wire worker name
Three spawn-modal bugs, re-landed from the closed redesign branch (#156):
- createTask no longer sends `branch`: the API field names the session's
NEW worktree branch, so submitting the modal's default ("main") made the
daemon 409 with BRANCH_CHECKED_OUT_ELSEWHERE on every spawn. The "Based on"
pane is informational — workers branch off the project's default branch in
a fresh worktree.
- API errors render their envelope message instead of "[object Object]":
openapi-fetch resolves non-2xx responses to a plain {code,error,message}
object, not an Error; new apiErrorMessage unwraps it (message + code).
- The "Worker name" field is actually used: after spawn, a best-effort
PATCH rename sets the displayName (a failed rename must not look like a
failed spawn — the worker is already running).
Also revives the renderer test suite, which made these tests (and 7 of 9
suite files) impossible to run on main:
- vitest.config.ts re-exports vite.renderer.config so `vitest run` actually
loads the jsdom environment + setup file. Forge's per-target
vite.*.config.ts names are invisible to vitest, so the existing `test`
block was dead config and every DOM-touching test died on
"window is not defined".
- vite.renderer.config.ts imports defineConfig from vitest/config so its
`test` key typechecks.
- routeTree.gen.ts + the session route are regenerated by the pinned
@tanstack/router-plugin (it runs as part of loading the renderer config;
the checked-in tree predated the installed plugin version and its route-ID
drift caused 3 of main's 7 typecheck errors).
- App.test.tsx wraps App in TooltipProvider, mirroring routes/__root.tsx.
Frontend: 9/9 test files, 99/99 tests pass (was 2/9 files). Typecheck is
down from 7 errors to 3 — the survivors (forge.config notarize/maker types,
update-electron-app call signature) predate this branch and are untouched.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* chore: format with prettier [skip ci]
* fix(gitworktree): base new session branches on the local default branch when no remote exists
Re-lands the remoteless fallback from the closed redesign branch (archived
in 641b712). Creating a session worktree resolved the base for a NEW branch
only via the remote-tracking ref (origin/<defaultBranch>), so a registered
repo with no remote failed every spawn with BRANCH_NOT_FETCHED — an error
that misleadingly names the new session branch and suggests `git fetch`,
which is impossible without a remote.
refs/heads/<defaultBranch> now follows origin/<defaultBranch> in the
candidate list: remote-tracking still wins whenever it exists, and a
remoteless repo bases session branches on its local default branch.
Verified live: a plain `git init` repo (no remote) that previously failed
now spawns, and the integration suite covers it
(TestWorkspaceIntegrationCreateInRemotelessRepo).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
* fix(sessions): stop AO hook files from making every worktree permanently dirty
Agent adapters write hook files (.codex/hooks.json, .opencode/plugins/
ao-activity.ts, .claude/settings.local.json, ...) into fresh session
worktrees as untracked files. `git worktree remove` (deliberately run
without --force) refuses on any untracked file, so Workspace.Destroy
failed for every session of the 12 workspace-writing harnesses:
POST /sessions/{id}/kill returned an unlogged 500 INTERNAL_ERROR and
`ao session cleanup` reported 'Would clean N' then '0 sessions cleaned'
with no reason, leaking workspaces forever.
Three coordinated fixes, none of which force-deletes user/agent work:
- Root cause: every adapter now writes a sentinel-guarded, self-ignoring
.gitignore next to its hook files (hookutil.EnsureWorkspaceGitignore),
so AO's own files no longer count as dirt while anything an agent
drops — even in the same directory — still blocks teardown. A
registry-wide conformance test enforces the contract for all current
and future adapters. (Per-worktree .git/worktrees/<name>/info/exclude
was evaluated first but git does not honor it.)
- Typed refusal: gitworktree.Destroy classifies a still-dirty refusal as
ports.ErrWorkspaceDirty (git status probe). Kill maps it to success
with freed=false (session terminated, worktree preserved); Cleanup
reports it per-session as skipped-with-reason through the API
(CleanupSessionsResponse.skipped), and the CLI prints
'Skipped: <id> (workspace has uncommitted changes)' plus a summary.
- Observability: envelope.WriteError records the raw service error into
a request-scoped slot and the access log attaches it to 5xx lines, so
any remaining internal error is diagnosable server-side.
Worktrees created before this fix gain the .gitignore on restore (hook
install re-runs); their cleanup is otherwise reported as skipped instead
of erroring.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(cleanup): address Greptile P2s — surface dirty-probe failures, stop leaking raw errors
Two review findings on this PR:
- gitworktree.Destroy: when the isDirty probe itself failed, the error was
silently discarded and the refusal looked identical to "registered but not
dirty". The probe failure now rides the returned error (dirty probe: ...),
so it reaches the access log via the 5xx error capture.
- Cleanup skip reasons: a non-dirty teardown failure put the raw error —
including internal filesystem paths — into the public skipped[].reason
field. The public reason is now the fixed string "workspace teardown
failed"; the full cause goes to the daemon log (warn, with sessionID and
path). The dirty-refusal reason is unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(gitworktree): wrap the dirty-probe error with %w per errorlint
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix(codex): deliver activity hooks via -c session flags, trust worktree at launch
Codex (0.136+) never loads hook config from AO's per-session worktrees:
project-local .codex/ layers only load from trusted directories, and for
linked git worktrees codex sources hook declarations from the matching
folder in the root checkout — so the workspace-local .codex/hooks.json AO
wrote was dead config and codex sessions never reported activity.
Deliver the hooks on the launch/resume command instead:
- -c 'hooks.<Event>=[...]' session-flag config for SessionStart,
UserPromptSubmit, PermissionRequest, and Stop; the session-flags layer
is not trust-gated and aggregates with the user's own hooks. The
existing --dangerously-bypass-hook-trust flag lets them run without a
persisted trust hash.
- -c 'projects={"<worktree>"={trust_level="trusted"}}' (inline-table
form; the dotted projects."<path>".trust_level key is corrupted by
codex's naive -c dot-split) so spawns into never-trusted repos don't
hang invisibly on the interactive directory-trust prompt. Both the
literal and symlink-resolved worktree paths are trusted.
- -c notice.hide_rate_limit_model_nudge=true so the "switch to a cheaper
model?" dialog can't hang a headless pane and swallow the spawn prompt.
GetAgentHooks no longer writes workspace files (worktrees stay clean); it
only strips entries older AO versions left in .codex/hooks.json,
preserving user hooks. UninstallHooks/AreHooksInstalled now operate on
those legacy files only.
Verified with a real spawn into a fresh untrusted repo: activity
transitions idle -> active -> idle hands-free, no .codex dir in the
worktree, no hook delivery failures.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* feat(sessions): activity-signal watchdog + hook delivery hardening
A codex upgrade broke activity tracking silently: sessions showed a
confident "idle" forever while the agent worked. This bundle makes hook
delivery verifiable end to end and makes any future breakage loud
instead of invisible.
Watchdog (no_signal status):
- sessions.first_signal_at (migration 0010) records the FIRST hook
callback per spawn/restore — raw signal receipt, independent of the
derived activity state. lifecycle.ApplyActivitySignal stamps it (and
writes through same-state repeats until stamped, e.g. Codex
SessionStart reporting idle on an idle-seeded row); MarkSpawned clears
it so every relaunch re-proves its hook pipeline.
- deriveStatus downgrades a live session with no receipt to the new
no_signal display status after a 90s grace, instead of idle.
Terminated/PR-derived statuses still win. The sessions CDC update
trigger now also fires on first-signal receipt so the dashboard
transition is pushed live.
- frontend maps no_signal -> needs_you (a human should look at the pane).
Hook callback hardening (re-landed from the closed redesign PR #156):
- the session manager pins each spawned session's PATH with the daemon
executable's directory first, so the bare `ao` in hook commands
resolves to the daemon that installed them, with a spawn-time warning
when the pin cannot apply.
- `ao hooks` failures append to $AO_DATA_DIR/hooks.log (size-capped);
`ao doctor` gains a hooks-log check that warns on failures from the
last 24h, and an ao-binary identity check.
Codex launch-surface canary:
- `ao doctor` gains codex-launch-flags: it runs probes exported by the
codex adapter (built from the same flag builders as the real spawn
argv) against the installed binary, warning when codex rejects the
hook-trust bypass flag or AO's -c session-flag overrides.
- codex hook callback timeout drops 30s -> 5s so a hung daemon cannot
stall the agent's turn.
Docs: the agent PRD callback section now describes the implemented flow
(derive state, POST /sessions/{id}/activity, hooks.log) instead of the
unbuilt SQLite/metadata merge, and notes that hook-derived metadata
persistence (codex resume) is still not implemented.
Frontend note: main's renderer test suite has 7 pre-existing failing
files and a vite-config typecheck error unrelated to this change;
workspace.test.ts (the only frontend file touched) passes 26/26.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(store): restore TestSessionWorktreesRoundTrip lost in the re-landing port
The branch ported store_test.go wholesale from the closed redesign
branch, whose copy predates #165 — silently dropping the
session-worktrees round-trip test #165 added. Restore main's file and
re-apply only this branch's addition (TestSessionFirstSignalRoundTrip).
No other ported file lost main-side content (audited per-file against
main; the remaining deletions are this branch's intended refactors).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* fix(status): only derive no_signal for harnesses that have a hook pipeline
Review finding: the no_signal downgrade had no harness-capability gate, but
first_signal_at can only ever be stamped by an `ao hooks` callback. Ten
spawnable harnesses (amp, aider, crush, grok, kimi, devin, auggie, continue,
vibe, pi) install no hooks at all, so every live session of theirs would have
flipped from idle to a permanent no_signal -> needs_you after the 90s grace.
The session service now takes a SignalCapable predicate; daemon wiring injects
activitydispatch.SupportsHarness (the deriver registry is the source of truth
for "this harness can signal"). Left nil, the service never claims no_signal.
A new dispatch test pins that every deriver token is a known harness name.
Also from the same review:
- lifecycle/manager.go and the 0010 migration claimed Codex's SessionStart
reports idle as the first signal; both codex and claude-code derivers
deliberately return no signal for session-start, so the comments now cite a
real case (a lost "active" POST followed by a Stop hook landing idle).
- docs/agent/README.md documents the gate and the restore caveat: a restored
session the user never prompts has nothing to signal, so it shows no_signal
after the grace until a receipt-only session-start signal exists.
- 0010 migration uses DROP TRIGGER IF EXISTS per house style.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* feat: add workspace project registration schema
* fix: satisfy workspace registration lint
* fix: harden workspace registration edge paths
- Reject linked-worktree and bare parents via validateWorkspaceParent before any mutation
- Roll back git init/.gitignore on failure in initWorkspaceParent so retries are clean
- Reject child repos named __root__ (reserved PK in session_worktrees)
- Serialise Service.Add with addMu to eliminate TOCTOU on concurrent same-path calls
- Fix ensureWorkspaceGitignore permission 0o600 -> 0o644
- Improve guardNoGitlinks suggestedFix with actionable git rm --cached guidance
- Remove dead CASE/__root__ ordering from ListWorkspaceRepos SQL (regenerated via sqlc)
- Resolve RepoOriginURL once per code path in Add (workspace vs single-repo)
- Add 7 tests covering the new edge paths