Fixes the packaged desktop app getting permanently stuck on "AO daemon
is not ready" (#385) via two daemon-lifecycle fixes.
1. Port conflict no longer exits the daemon. When the configured port
(default 127.0.0.1:3001) is held by a non-AO process, NewWithDeps now
falls back to an OS-assigned ephemeral port instead of returning a bind
error. A genuine peer AO daemon is already ruled out upstream (the
running.json + /healthz check in daemon.Run), so a conflict here means a
foreign holder. The bound port is logged ("daemon listening") and written
to running.json, both of which the supervisor reads, so the fallback
propagates to the renderer with no UI changes.
2. Detached daemon is torn down on more exit paths. before-quit already
group-kills the daemon, but app.exit() and some shutdown routes skip it,
orphaning the daemon so it keeps holding the port for the next launch. A
synchronous process 'exit' handler now also signals the daemon's process
group. A hard SIGKILL/crash still can't run JS, but fix#1 covers the
orphan that leaves behind.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
mainWindow.on('closed') -> browserViewHost.dispose() ran destroy(), which
touched contentView/child WebContentsViews already torn down by Electron,
crashing the main process with 'Object has been destroyed'. Skip the window
ops when the window reports destroyed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(preview): add clear, reuse defaults, force refresh, local files (#379)
`ao preview` had four issues that made the desktop browser panel awkward
during sessions. This addresses all four:
1. No way to clear the panel. Adds `ao preview clear` (DELETE
/sessions/{id}/preview) which empties the stored target; the panel
loads about:blank and returns to its empty state.
2. Bare `ao preview` always autodetected index.html. It now reuses the
session's existing preview target (so each agent/context keeps its own
default), falling back to index.html only when nothing was previewed.
3. Re-running `ao preview <same-url>` never refreshed. The preview_url
alone could not distinguish a real re-run from a CDC replay of an
unrelated session update. A new monotonic preview_revision (bumped on
every set, migration 0018, added to the sessions_cdc_update trigger)
gives the renderer a per-command identity to key navigation on, so a
re-run always re-navigates while unrelated updates are ignored.
4. Local files could not be previewed. `ao preview ./dist/index.html`
(and other workspace-relative paths) now resolve server-side to the
preview/files proxy URL when the file exists; non-file targets stay
verbatim.
Backend, CLI, and renderer all covered by tests; OpenAPI spec and the
frontend schema are regenerated for the new DELETE route and field.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(cdc): include previewRevision in sessions update event payload
The CDC trigger watched preview_revision changes but didn't include it
in the JSON payload, so the frontend couldn't detect same-URL preview
refreshes via SSE events. This broke the core purpose of the feature.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(migration): renumber to 0019 to resolve conflict with main
Main branch now has migration 0018 (review_run_delivered_at), causing
a duplicate version conflict when CI merges the PR branch with main.
Renamed 0018_add_session_preview_revision.sql to 0019.
Also fixed the Down migration to properly restore the CDC trigger state
after migration 0017 (with previewUrl but without previewRevision).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Vaibhaav <user@example.com>
* fix(desktop): attach to a serving daemon instead of spawning a doomed child
Launching the Electron app while a standalone `ao daemon` already owns the
port made the Electron-spawned child daemon log "daemon already running …
refusing to start" and exit 1, instead of attaching to the running daemon.
`inspectExistingDaemon` only attaches when ~/.ao/running.json agrees with a
live daemon, so any run-file divergence (missing/stale/unparseable file, dead
PID, or a /healthz pid mismatch) made it return null — and there was no
independent port probe before spawn(), so Electron spawned into an occupied
port and the Go bind guard correctly refused.
Add a defensive direct probe of http://127.0.0.1:<expectedPort>/healthz in
startDaemonInner, after the run-file check and before spawn(): if a genuine
daemon answers, attach to it (the same "ready" DaemonStatus shape the run-file
path returns) instead of spawning. The expected port is resolved the same way
startup does (AO_PORT or the default).
The attach-or-spawn decision is extracted into a pure, dependency-injected
module (shared/daemon-attach.ts) so it can be exercised directly; main.ts
keeps ownership of fs reads, process signals, fetch, and the path identity
check. Covered by 27 tests, including real loopback-server cases that
reproduce the issue scenario end to end.
Fixes#367
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore: format with prettier [skip ci]
* fix(desktop): enforce readiness + identity checks on the port-probe attach path
Address review feedback on #367: the new direct port probe attached to any
service-matching daemon as soon as /healthz returned ok, without the /readyz
and foreign-binary identity checks the run-file path enforces. That reopened
the mismatch daemonIdentityError was built to prevent — Electron could
silently drive a different/older AO build serving the port — and could mark a
still-starting daemon "ready".
Extract the shared post-handshake tail (readinessStatus) and run it from both
paths, anchoring on the PID /healthz reports for the port probe. A serving
daemon that is not ready, or whose binary the identity check refuses, now
yields the same "error" DaemonStatus instead of attaching — strictly safer
than spawning, which would only collide on the occupied port and die.
resolveDaemonFromPort now takes the same identityError dependency
resolveDaemonFromRunFile does; main.ts passes daemonIdentityError(launch, …).
Adds port-path tests for not-ready, foreign-binary identity (unit), and a
real-server foreign-binary scenario (e2e). 30 tests in daemon-attach.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore: format with prettier [skip ci]
* docs(desktop): note why the port probe uses the expected (not hardcoded) port
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
#366 (brand text overlapped by the fixed macOS TitlebarNav cluster on
board routes) is already fixed on main: #263 made the shell render the
topbar on every route, so the sidebar always hangs below the 56px
titlebar band and the brand never lands in the cluster's lane.
Add an e2e regression guard that locks the invariant in for the routes
the issue named — the brand must not overlap the fixed cluster and the
"Agent Orchestrator" wordmark must stay readable (not truncated) — on the
home board route and the project board route, plus across a board→session
transition (the persistent brand must not jump). Drives the live renderer
under a forced macOS UA. If a topbar-less route is ever reintroduced,
these fail.
Verified: passes against current main with no source changes;
typecheck clean.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat(frontend): add live browser panel
* chore: format with prettier [skip ci]
* feat: preserve and auto-open browser previews
* fix: retry browser preview after session updates
* fix: wait for browser view before preview navigation
* fix: reopen preview after session switches
* fix: preserve browser views across session switches
* chore: format with prettier [skip ci]
* feat: add `ao preview` command to drive the session browser panel
Replaces the browser panel's auto-detect with an explicit, session-scoped
command. `ao preview [url]` runs inside a session (derives the target from
AO_SESSION_ID; rejects when unset or when the session is unknown):
- with a url, opens it verbatim (file://, http, https; no sanitization for now)
- with no url, autodetects index.html in the workspace as before
The resolved target is persisted as a new `previewUrl` session field and fans
out over the existing CDC /events stream (the sessions update trigger now fires
on preview_url and carries previewUrl in its payload). The desktop browser panel
reflects session.previewUrl: it opens, switches the center pane to the browser,
and navigates, re-navigating only when the target changes.
ponytail: file:// preview targets are accepted unsanitized; agent-trusted for now.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(cli): document the `ao preview` command
Add `ao preview` to the CLI command tables in README.md and docs/cli/README.md,
noting it resolves its session from AO_SESSION_ID and its no-arg autodetect vs
explicit-URL behavior.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore: format with prettier [skip ci]
* fix(frontend): reveal `ao preview` in the inspector Browser tab, not the center pane
`ao preview` set session.previewUrl, and SessionView surfaced it by
popping the browser into the center pane, replacing the terminal. Reveal
it in the inspector rail's Browser tab instead (opening the rail if it is
collapsed); the manual pop-out button still expands it to the center.
Lifts the inspector's active tab to an optional controlled prop so
SessionView can drive it, and adds a regression test asserting the center
pane keeps the terminal while the rail switches to Browser.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs: instruct agents to `ao preview` when showing frontend changes
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Vaibhaav <user@example.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Harshit Singh Bhandari <dev@theharshitsingh.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(frontend): add terminal controls and reliable copy
* chore: format with prettier [skip ci]
* fix(frontend): preserve terminal mouse scrolling
* fix(backend): enable zellij wheel scrolling
* fix(frontend): forward xterm scroll input
* fix(frontend): restore terminal drag selection copy
* fix(frontend): make terminal wheel scroll zellij scrollback
zellij 0.44.x with mouse-mode true acts on SGR wheel reports written to
its stdin and scrolls the focused pane, but it does not enable host mouse
reporting. xterm therefore never reports the wheel itself (protocol stays
NONE) and, with scrollback:0, converts the wheel into cursor-arrow keys,
which move the agent's cursor/history instead of scrolling.
Synthesize SGR wheel reports from a custom wheel handler and send them
through the existing input pipe; accumulate pixel deltas into line counts
to match xterm's native scroll feel. Ctrl/Cmd wheel is left for the
font-size zoom handler. Drag-copy is unaffected (separate selection path).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore: format with prettier [skip ci]
* fix(frontend): handle line/page wheel modes for cross-platform scroll
The wheel-to-SGR translation only divided pixel deltas by row height,
which is the deltaMode browsers emit for trackpads and normalized wheels
(macOS). Many Linux/Windows mouse wheels report whole lines (deltaMode 1)
or pages (deltaMode 2) with small deltaY, which truncated to zero lines
and never scrolled. Mirror xterm's getLinesScrolled across all three
modes so scroll works on every platform.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Harshit Singh Bhandari <dev@theharshitsingh.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(review): message worker on changes_requested instead of relying on SCM poll (#337)
review.Engine.Submit previously only persisted the verdict/body and left the
worker to learn about requested changes via the SCM poll loop, which is gated on
GitHub's reviewDecision and never reaches CHANGES_REQUESTED for self-reviews or
COMMENT-state reviews. Submit now nudges the worker's live pane directly via
ports.AgentMessenger (the same mechanism lifecycle uses) whenever the verdict is
changes_requested.
Extended flow: the reviewer reads back the GitHub review id it posted and passes
it through `ao review submit --review-id`; the id is stored on the review_run row
(new column + migration 0016) and included in the worker message so the worker
knows exactly which review to address and reply to.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(review): mark worker nudge as AO internal review, ask to reply + resolve
Distinguish the AO internal review nudge from the external GitHub-reviewer
feedback the lifecycle SCM loop relays. For an AO review the worker is now asked,
once it has pushed its fix, to reply on the review referencing its id with what
it changed and resolve the inline review comment threads it addressed (the
reviewer posts inline comments, so the per-finding threads are resolvable via
resolveReviewThread; the top-level review object is not, hence the reply).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(review): generalise the changes-requested worker nudge wording
Drop the "not an external GitHub PR reviewer" aside and the assumption that the
worker pushes a fix — it may resolve the feedback without code changes. The nudge
now reads "Review the feedback below and address it" and asks the worker to reply
with how it addressed the review and resolve the threads it addressed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(review): harden the review-id read-back against array order and empty results
The reviewer read the just-posted review id with `--jq '.[-1].id'`, which trusts
the REST API to return reviews in ascending submission order and errors when no
review exists. Review ids are monotonic, so select the highest id instead and
emit nothing when the list is empty: `--jq 'map(.id) | max // empty'`. Update the
matching `--review-id` flag help.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(review): post the review via gh api and capture its id from that response
The reviewer must use `gh api --method POST .../reviews` to attach inline
comments anyway (`gh pr review` cannot), and that response already contains the
created review's id. Capture `.id` from that single call instead of a second
read-back, dropping the array-ordering/pagination heuristics entirely — the id is
the exact review just created.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(review): send the review as a JSON body so inline comments are a real array
gh api -f/-F cannot build an array of objects: comments[][path] is sent as a
literal key, so the inline comments are dropped — defeating the reason for using
gh api over gh pr review. Post the review via --input JSON instead, keeping the
.id capture and the approve/COMMENT fallback.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore: format with prettier [skip ci]
* chore(review): drop accidentally committed reviewer scratch, write review out of tree
review.md was the reviewer agent's own writeup, swept onto the worker branch by a
stray `git add -A` in 5df20c9. Remove it, gitignore `/review.md` as a backstop,
and change the reviewer prompt to write its review to a temp file outside the
checkout instead of into the worktree (where it could be committed onto the
worker's branch).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(review): message the worker before marking the run complete
If messenger.Send failed after UpdateReviewRunResult had already flipped the run
to complete, a retried `ao review submit` tripped the status='running' guard and
could never record the result. Send first; only mark the run complete once the
worker has been notified, so a failed send leaves the run retryable. A landed
message followed by a failed DB write degrades to one extra nudge on retry — the
same trade lifecycle's sendOnce makes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(review): accept the review body on stdin so the reviewer writes no file
`ao review submit --body -` now reads the review from stdin, and the reviewer
prompt pipes its writeup via a heredoc instead of writing a file. Previously the
reviewer wrote review.md into its checkout to pass as --body, which could be
committed onto the worker's branch (as it just was). A file path is still
accepted for backward compatibility.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(review): always post approvals as COMMENT, drop the APPROVE attempt
The reviewer posts from the PR author's own GitHub account, so event=APPROVE
always 422s. Drop it: request changes with REQUEST_CHANGES, approve with a
COMMENT-event review whose body states it is an approval.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(review): post every review as event=COMMENT (author can't APPROVE or REQUEST_CHANGES own PR)
The reviewer posts from the PR author's own account, where GitHub rejects both
APPROVE and REQUEST_CHANGES. Always post a COMMENT-event review and state the
verdict in the body; the machine-readable verdict still reaches AO via
`ao review submit --verdict`.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(cli): accept underscore flag names on `ao review submit`
Reviewer agents routinely invoke the submit command with --review_id
instead of --review-id, which cobra rejected as an unknown flag and
dropped the GitHub review id from the worker notification. Normalize
underscores to hyphens on the command's flags so both spellings resolve
to the same flag.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix: sanitize review id in worker notifications
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Vaibhaav <user@example.com>
Electron's main process set app.setName(...) but never overrode userData,
so Chromium runtime state (cache, cookies, local/session storage, crash
dumps) defaulted to ~/Library/Application Support/<name>. Older dev builds
also wrote the daemon DB there. Pin userData to ~/.ao/electron so the
entire app footprint lives under the canonical AO home alongside the
daemon data dir and running.json; sessionData and crashDumps derive from
userData, so the single override reparents them all.
Document the ~/.ao-only rule as a hard boundary in AGENTS.md and CLAUDE.md.
Closes#368
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix: recover terminal reattach after daemon idle
* chore: format with prettier [skip ci]
* fix: harden daemon start recovery
* fix: cancel stale daemon start attempts
* fix: quarantine untrusted daemon base url
* chore: format with prettier [skip ci]
* fix: close daemon status race windows
* chore: format with prettier [skip ci]
* fix: bootstrap daemon trust before shell load
* test: trust mocked API base in PR hydration
---------
Co-authored-by: Vaibhaav <user@example.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
The `Desktop testing build` workflow (tag 0.0.0-testing-*) failed on two of
its three runners, and the unsigned macOS artifact has no real app icon.
Linux (rpm): maker-rpm reported "cannot run on linux" because
electron-installer-redhat is only a deeply-nested optional dependency and npm
non-deterministically skipped it on the runner (debian installed, redhat did
not). Promote both electron-installer-debian and electron-installer-redhat to
top-level optionalDependencies so npm reliably installs them on linux/darwin
and still skips them cleanly on win32. Also give the rpm maker an explicit
License (rpmbuild rejects an empty License field) and a maintainer/homepage.
Windows (squirrel): NuGet pack exits 1 when <authors> is empty. package.json
had no author, so add author/license/homepage and set authors + setupIcon on
the squirrel maker.
App icon: generate icon.icns/.ico/.png from src/landing/public/og-image.png
(1024x1024) and wire packagerConfig.icon, the deb/rpm/squirrel makers, and the
runtime BrowserWindow icon (Linux/Windows; macOS uses the bundle .icns).
Verified on macOS: npm run make builds the zip, the packaged app's icns
matches the generated icon, typecheck + 160 vitest tests pass.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(sidebar): always show project row action icons
The hover-reveal mechanism (opacity-0 → group-hover/menu-item:opacity-100)
was not reliably triggering in the Electron app — icons never appeared on
hover. Instead of debugging CSS :hover group propagation in Chromium, make
the action icons always visible.
Changes:
- Remove opacity-0/opacity-100 hover gating from the action cluster div
- Change button padding from hover-gated pr-[84px] to always pr-[84px]
- Hide the session count badge (it was only shown when icons were hidden)
- Keep z-10 on the action cluster so session rows don't paint over it
Reported by phylolver(vaibhaav).
* fix(sidebar): correct inverted collapsed-count class
The count badge was set to hidden-by-default but shown-as-grid in
collapsed/icon mode — the opposite of correct. In icon mode there's no
room for a count badge and the action cluster is hidden anyway. Since the
action icons are now always visible and permanently replace the count,
the badge should be hidden in all states.
Reported by phylolver(vaibhaav).
---------
Co-authored-by: AO Bot <ao-bot@composio.dev>
* fix(sidebar): make project row hover icons clickable and non-overlapping
Three fixes for project row hover actions (dashboard, orchestrator, kebab):
1. Count badge pointer-events: The session count span fades to opacity-0 on
hover but kept pointer-events active, intercepting clicks meant for the
action icons. Added pointer-events-none on hover/focus/menu-open states.
2. Action icons z-index: The absolutely-positioned action cluster had no
z-index, so positioned session rows (SidebarMenuSubItem with position:
relative) in expanded projects painted on top and blocked hover/click on
the first project's icons. Added z-10 to lift the action cluster.
3. Padding clearance: Bumped pr-[78px] to pr-[84px] to give more room for
the three 20px action buttons + right offset.
Reported by aditi and phylolver(vaibhaav).
* fix(test): update Sidebar padding assertion from 78px to 84px
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: AO Bot <ao-bot@composio.dev>
Co-authored-by: Vaibhaav <user@example.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(frontend): surface multiple PRs per session
Replace the single optional pullRequest on a session with a prs[] list and
render it across every PR surface: the inspector stacks one card per PR, the
PR board lists one row per attributed PR, and the board card shows a PR count
summary. useWorkspaceQuery now maps the live /api/v1/sessions prs[] into the
query (the frontend surfaced no PRs before this). Ordering is actionable-first
(open, draft, merged, closed).
Adds unit tests (SessionInspector, PullRequestsPage), a Playwright e2e spec,
and a multi-PR mock-data fixture.
* chore: format with prettier [skip ci]
* feat(frontend): swap inspector Changes tab for empty Reviews placeholder
The inspector rail's Summary tab already renders the multi-PR stack from
PR #237 work, so the Changes (Git rail) tab is the next inspector surface
to evolve. Reviews will land on its own backed by PR-scoped review data
(separate workstream); reserve the slot now with an empty placeholder so
the navigation lands before the data does.
- Tabs are now Summary, Reviews, Browser. The InspectorView union and
the VIEWS array are updated; Reviews gets a message-bubble icon to
distinguish it from Summary's list icon.
- ChangesView and its lucide imports (GitBranch, GitCommitHorizontal,
Plus, Square, Trash2) and the unused Button import are removed; a
small ReviewsView mirrors BrowserView's empty-state shape.
- styles.css drops the orphaned .inspector-changes__* block.
- SessionInspector.test.tsx asserts the new tab labels and that Reviews
shows the empty placeholder.
* feat(frontend): wire the reviewer feature into the inspector Reviews tab
The multi-PR transplant left the Reviews tab an empty placeholder, which would
have regressed the existing session review feature. Move the reviewer panel
(trigger/re-run a review, open the reviewer terminal, surface verdict + status)
into the Reviews tab, gated on the multi-PR model: it reads the session's prs[]
to decide between the reviewer card and the "no PR yet" empty state, and pulls
review runs + project reviewer config straight from the daemon.
Reconciles the prs[]-from-session-list model across the suite: ShellTopbar and
pr-hydration fixtures carry prs[], useWorkspaceQuery tests drop the obsolete
per-session /pr fetch, and SessionsBoard restores the dropped board title.
Adds a Playwright reviews-tab e2e spec (reviewer card for a session with PRs;
empty state for one without).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* test(frontend): adapt multi-PR e2e specs to main's expanded fixture
The rebase pulled in main's larger mock-data fixture (4 workspaces, 13 PRs)
and its renamed refactor-mux title. Rewrite the PR-board assertion to verify
the actionable-first ordering invariant instead of a brittle full-list match,
and update the empty-state selector to the session's current title.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: format with prettier [skip ci]
---------
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Polish the board and task action chrome.
- move board actions into the board header and remove empty board topbar space
- refine session topbar actions with notification, Kill, and Orchestrator controls
- add pointer cursors for clickable controls and clean sidebar child-session styling
Verified with frontend typecheck and targeted renderer tests.
Improve the frontend board and session workflow presentation.
- refine project sidebar hierarchy and child session display
- clean up kanban task cards and status labels
- add direct task creation from project boards
Verified with frontend typecheck and targeted renderer tests.
* fix(storage): renumber telemetry migration to 0015 to resolve goose version collision
* chore: format with prettier [skip ci]
---------
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
* feat(frontend): refresh dashboard, orchestrator, and AO logos
Replaces the dashboard and orchestrator glyphs and the AO brand logo
per issue #315.
- Dashboard buttons (sidebar action + Open Kanban) now use the
asymmetric LayoutDashboard icon instead of the equal-cell LayoutGrid.
- Orchestrator buttons (sidebar action + every topbar badge/button) use
a new org-chart OrchestratorIcon (parent fanning to three children),
authored in lucide's stroke style since lucide has no matching glyph.
- The AO brand logo (sidebar header mark, landing nav, docs header) is
now the pixel-mascot image; the old ao-logo.svg monogram is removed.
Closes#315
* chore: format with prettier [skip ci]
* fix(landing): use next/image and next/link for the AO logo and docs link
Addresses react-doctor review on #317: the Next.js landing app should use
next/image for the brand logo (optimized formats, responsive srcset, lazy
loading) and next/link for the internal /docs navigation (client-side
routing + prefetch) instead of plain <img>/<a>.
---------
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
The onRender convergence loop added in #312 recovered the under-counted
rows from #280, but on a HiDPI display it could lock the Codex
orchestrator terminal at half size with a ghosted composer (#313).
FitAddon derives the grid by dividing the pane box by the renderer's
measured css cell box. During the WebGL atlas warm-up that cell box can
emit a one-frame transient (a doubled box on a 2x display), which halves
the proposed cols/rows. The loop committed that single frame's proposal,
resized the grid to half, then detached after a few "stable" frames — so
nothing re-fired the PTY resize that makes zellij repaint, leaving the
grid stuck at half width and the stale composer un-cleared.
Require a differing proposal to repeat identically across two consecutive
renders before applying it, so a one-frame transient only updates the
pending value and is never committed. Add 600ms/1200ms settle fits as a
session-bounded backstop: by then the atlas and font metrics are warm, so
even if the loop detached at a briefly-stable wrong measurement, a late
re-measure corrects the grid and fires the resize zellij needs to repaint
cleanly. fit() is idempotent, so a correct terminal never reflows.
Fixes#313
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
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>
* 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>
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>
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>
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
* 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>
* 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>
* 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>
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.
* 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>
- 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>
* 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
* chore: add prettier config and CI auto-formatter
Adds .prettierrc and .prettierignore (config only, no local enforcement).
Formatting runs in CI via the prettier.yml workflow: on every push to a
non-main branch, Prettier rewrites changed files and commits the result back
using GITHUB_TOKEN. Developers never need to run Prettier locally.
Intentionally excludes husky/lint-staged — local pre-commit hooks are the
wrong layer for a formatter that the whole team doesn't need installed.
Also adds .envrc.local to .gitignore for personal local shell overrides.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Entire-Checkpoint: 27336650d2ee
* chore: format with prettier [skip ci]
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
* feat(config): persist per-project agent config and resolve it at spawn
Each project can now carry its own agent config (model, permissions,
adapter-specific keys) that survives daemon restart and is resolved into
the launch command when a session spawns.
- storage: add nullable projects.agent_config JSON column (migration 0008);
marshal/unmarshal in the store so the domain carries map[string]any
- resolution: session manager loads the project row and populates
LaunchConfig.Config before GetLaunchCommand
- validation: claude-code declares a ConfigSpec (model, permissions) and
rejects unknown keys / bad types / bad enums at spawn; it applies the
model override and config-driven permission mode (explicit Permissions
still wins)
- surface: PUT /projects/{id}/agent-config + `ao project set-config`
(--set/--config-json/--clear), config shown in `ao project get`
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(claudecode): validate string-list/required config keys and unhandled types
Address review on per-project agent config validation:
- handle ConfigFieldStringList (list of strings) explicitly
- reject unhandled ConfigFieldType via a default case rather than
silently passing
- enforce Required fields are present
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* refactor(config): make per-project agent config a typed struct
Replace the free-form map[string]any agent config with a typed
domain.AgentConfig{Model, Permissions} so values are validated when set
(CLI/API) instead of silently dropped at spawn, and the OpenAPI/TS schema
and UI get real typed fields.
- domain: AgentConfig struct + Validate(); PermissionMode moves to domain
and ports re-exports it as a type alias (zero adapter churn)
- storage: marshal/unmarshal the typed struct (IsZero → SQL NULL)
- service: validate on Add and SetAgentConfig; read-model exposes a typed
*AgentConfig
- claudecode: read typed cfg.Config.Model/.Permissions; drop the
map/spec-based validateConfig in favor of the typed Validate()
- cli: typed `ao project set-config --model/--permission/--clear`
- docs: add docs/design/per-project-config.md blueprint sequencing the
remaining # Projects fields toward fully typed per-project config
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(config): full typed per-project ProjectConfig (store, resolve, surface)
Expand per-project config from agentConfig-only to the full legacy
`projects.<id>` surface, modeled as one typed domain.ProjectConfig
persisted in a single projects.config JSON column.
Wired end-to-end at spawn:
- defaultBranch → base branch for the session worktree (ports.WorkspaceConfig.BaseBranch)
- env → merged into the runtime env (AO-internal vars still win)
- symlinks → repo files linked into the workspace
- postCreate → commands run in the workspace (OS-agnostic shell)
- agentRules / agentRulesFile / orchestratorRules → merged into the prompt
- worker/orchestrator role overrides → harness + agent-config resolution
Stored + validated + surfaced now, consumption deferred (no consumer yet):
tracker, scm(+webhook), opencodeIssueSessionStrategy; sessionPrefix feeds
the display prefix only (session-id generation unchanged).
Validation lives on domain.ProjectConfig.Validate() and runs when config is
set (CLI/API). PermissionMode/AgentConfig stay typed; harness names validated
via domain.AgentHarness.IsKnown().
Surface: PUT /projects/{id}/config (replaces /agent-config) + typed
`ao project set-config` flags (--default-branch/--env/--symlink/--post-create/
--agent-rules/--worker-agent/… or --config-json). OpenAPI + TS regenerated.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(lint): tighten symlink dir perms to 0o750 (gosec G301)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(config): centralize default project config + tests
Add domain.DefaultProjectConfig / ProjectConfig.WithDefaults with a single
DefaultBranchName ("main") source of truth, replacing the literal "main"
scattered in the read-model and the gitworktree adapter. Unconfigured
projects now resolve the default branch through one path; every other field
defaults to its zero value.
Tests: defaults present for all fields (DefaultProjectConfig/WithDefaults),
and an unconfigured project reports the default branch + derived session
prefix while omitting the empty config object.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(config): encode documented defaults (branch=main, tracker=github)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(config): fail-safe paths for missing/corrupt per-project config
Address review on default-config / fail-safe spawning:
- projectRules: a missing AgentRulesFile is optional context, skipped
rather than aborting every spawn (only a real read error surfaces)
- store: a corrupt config JSON column degrades to a zero config instead
of failing GetProject/ListProjects/FindProjectByPath for that row
- restore: re-apply the project's resolved AgentConfig so a configured
model/permissions carry across a restore (matches fresh spawn)
Tests: missing rules file skips, corrupt config degrades to zero, restore
applies the project agent config.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* refactor(config): trim per-project config to consumer-backed fields
Drop config that has no live consumer yet, so this PR lands only the
fields actually read at spawn/display:
- Remove prompt rules (agentRules, agentRulesFile, orchestratorRules)
from ProjectConfig. Project/agent instructions belong on the system
prompt path or repo-local AGENTS.md, not another rules family.
- Remove future-only integration config with no consumer: tracker, scm,
scm.webhook, and opencodeIssueSessionStrategy (plus their types,
constants, the github tracker default, CLI flags, and spec schemas).
These return in focused PRs alongside the code that reads them.
Kept: defaultBranch, sessionPrefix, env, symlinks, postCreate,
agentConfig (model/permissions), and worker/orchestrator role
overrides. Cross-agent model/permissions support stays follow-up (#157).
Regenerated openapi.yaml + frontend schema.ts.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(config): reject unknown config JSON keys; confine symlink paths
Two review hardenings on the now-trimmed per-project config surface:
- Project add/set-config endpoints decode with DisallowUnknownFields, so
a misspelled or removed config field surfaces as a clear 400 instead
of being silently dropped. Locks the removals from e213b68 (and any
future trims) at the API gate. Covered by new controllers test.
- applySymlinks now refuses absolute paths and any ".." segment via a
safeRelPath guard, so a project config cannot escape the project or
workspace tree via a malicious symlinks entry. Covered by new
session_manager test.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(config): reject symlink path traversal at config write time
greptile flagged ProjectConfig.Symlinks as a write-time path-traversal
gap on PR #154 — the runtime guard in applySymlinks catches a malicious
entry on every spawn, but the config itself accepted it. Move the check
into ProjectConfig.Validate so a bad symlinks entry surfaces as
INVALID_PROJECT_CONFIG when set (CLI/API) instead of silently sitting in
the row until the next spawn. The runtime guard stays as
defense-in-depth.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: harshitsinghbhandari <24b4506@iitb.ac.in>
* fix(cdc): emit pr_review_thread_resolved on replace polls (#152 bug 5)
writePRRows was DELETE-then-UPSERT on the Replace path, so every poll's
upserts hit the INSERT branch and the AFTER UPDATE trigger that emits
pr_review_thread_resolved never fired in production. Replaces the
blanket delete with a set-diff: upsert observed threads first (so
unchanged thread_ids go through ON CONFLICT DO UPDATE and fire the
UPDATE trigger when resolved flips), then delete orphans whose
thread_id is not in the observed set, all inside the existing tx.
Adds DeletePRReviewThread query (sqlc-generated form hand-edited; no
sqlc binary available locally — sqlc generate from backend/ produces an
identical file).
Tests: TestPRReviewThreadsCDC_EmitsResolvedOnReplacePoll (regression —
fails without fix) and TestPRReviewThreadsReplace_PrunesOrphansWithoutReinserting.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(observe): emit scm-disabled log on startup with no subjects (#152 bug 7)
checkCredentials lived only inside Poll, which short-circuits when
discoverSubjects is empty. On a fresh daemon with no tracked PRs the
documented "scm observer disabled: provider credentials unavailable"
warn was unreachable, leaving users with no signal that the SCM
observer was a no-op.
Calls checkCredentials once in Observer.loop before the first Poll.
The existing credentialsChecked guard preserves once-per-process
semantics; provider construction still uses SkipTokenPreflight so
daemon readiness doesn't block on gh.
Test: TestStart_LogsDisabledWarningWhenNoTokenAndNoSubjects with a
race-safe syncBuffer for capturing slog from the observer goroutine.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(api,spawn): typed errors + project/branch/binary preflight (#152 bugs 1-4,6)
Closes the long tail of opaque-500-and-orphan-row failures that
discussion #149's smoke walk surfaced. The common shape: spawn created
the session row before validating preconditions, and the underlying
errors weren't typed, so toAPIError defaulted to INTERNAL_ERROR.
Bug 1 (orphan row + opaque 500 on unknown projectId):
Service.Spawn / SpawnOrchestrator now call store.GetProject first and
return apierr.NotFound("PROJECT_NOT_FOUND", ...) before manager.Spawn,
eliminating the create-row-then-fail-workspace ordering.
Bug 2 (Restore opaque 500 on half-spawned/terminated session):
Manager.Restore gained the ErrIncompleteHandle guard that Kill has at
manager.go:189-193. toAPIError now maps both restore and kill to the
same SESSION_INCOMPLETE_HANDLE 409 envelope.
Bug 3 (--branch unfetched / checked-out-elsewhere → opaque 500):
gitworktree pre-checks listRecords for branch-in-other-worktree, falls
back to refs/tags on missing local/remote head, and emits two new port
sentinels (ErrWorkspaceBranchCheckedOutElsewhere,
ErrWorkspaceBranchNotFetched) mapped to BRANCH_CHECKED_OUT_ELSEWHERE
(409) and BRANCH_NOT_FETCHED (400).
Bug 4 (orphan terminated row on claim-pr rollback):
Adds Store.DeleteSession gated to seed-state rows only (preserves the
no-resurrection guarantee for live sessions), transactional change_log
cleanup, Manager.RollbackSpawn (delete-then-fallback-to-kill), a new
POST /sessions/{id}/rollback endpoint, and rewires
cli/spawn.rollbackSpawnedSession to use it. The exit-0 sub-symptom was
unreproducible from current source and is left unaddressed.
Bug 6 (agent binary not on PATH → silent idle session):
Drops the "return name, nil" anti-pattern from all 21 agent adapters
and returns the new ports.ErrAgentBinaryNotFound on exec.LookPath miss.
Manager.Spawn gained a validateAgentBinary pre-flight (with injectable
LookPath so tests don't need real binaries on PATH) that aborts before
runtime.Create. Mapped to AGENT_BINARY_NOT_FOUND (400). Integration
tests in internal/integration/ stub LookPath to /usr/bin/true.
Tests cover each bug end-to-end. OpenAPI regenerated for /rollback.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: gofmt + regen frontend schema.ts for /rollback
CI fixes for #153:
- gofmt/goimports on kilocode and kiro adapters that the bug 6 audit
left mis-grouped.
- openapi-typescript regen against the new /rollback endpoint added in
the Lane A commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(store): guard change_log delete behind seed probe + regen sqlc (#152, PR #153 review)
Addresses @greptile-apps P1 and P2 review feedback on PR #153.
P1 (CDC events deleted for live sessions in rollback fallback):
DeleteChangeLogForSession ran unconditionally inside the transaction
before DeleteSeedSession's seed-state predicates filtered the session
delete to a no-op. For a live session reaching DeleteSession (the
delete-then-kill fallback path inside RollbackSpawn), the seed delete
returned 0 rows but the session_created/session_updated CDC events
had already been purged. Now probes via a new SessionIsSeed query
first and short-circuits the whole tx — including the change_log
cleanup — when the row is not in seed state.
P2 (regen sqlc): installed sqlc 1.31.1 and ran `sqlc generate` from
backend/, replacing the hand-edited pr_review_threads.sql.go (and
producing minor format-only churn in models.go, pr.sql.go,
sessions.sql.go, changelog.sql.go).
The regen surfaced two issues:
1. GetPR / ListPRsBySession had their return types hand-changed to
gen.PR by the previous PR; sqlc actually emits GetPRRow /
ListPRsBySessionRow when queries enumerate columns. Fixed by
collapsing those two queries to `SELECT * FROM pr` so sqlc returns
gen.PR (which is what the store's prRowFromGen converter expects),
and pr.last_nudge_signature now lands in the result alongside the
existing 37 columns.
2. sqlc 1.31.1's SQLite parser silently strips trailing `?`
placeholders and string literals from DELETE statements (reproduced
with sqlc.arg, IFNULL, rowid subquery, and second-predicate
workarounds — all eaten). DeleteSeedSession and
DeleteChangeLogForSession both tripped it. They are now run as
plain tx.ExecContext calls inside Store.DeleteSession, inside the
same write transaction as SessionIsSeed; both queries are removed
from the queries/ directory and the workaround context is
documented inline in queries/sessions.sql and queries/changelog.sql
to keep future contributors from re-adding them.
Verified: go build ./... clean, go test -race ./... 1097/1097 pass.
Introduces the shared platform that per-agent adapters plug into, wired for the
three shipped harnesses (claude-code, codex, opencode):
- adapters/agent/registry: single source of truth for shipped adapters
(Constructors), consumed by the daemon to resolve a session's harness.
- adapters/agent/activitydispatch + 'ao hooks' command: maps an agent's native
hook callbacks onto AO activity states (active/idle/waiting/...).
- claudecode/codex/opencode: emit SessionStart/UserPromptSubmit/Stop activity.
- HTTP + OpenAPI: report session activity state.
- db: single migration widening sessions.harness to all shipped harnesses, so
adding an adapter needs no further migration.
- domain: harness constants + --agent alias for 'ao spawn'.
Adding a new agent is now one adapter package plus a line in Constructors().
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Harshit Singh Bhandari <claudeagain@pkarnal.com>
* feat: add ao hooks activity command
* fix(activity): address review nits
- lcm: sameActivity ignores LastActivityAt so same-state repeats no-op
and don't churn UpdatedAt / CDC events.
- cli/hooks: surface stdin read errors to stderr for parity with the
daemon-error path; still exit 0 so a failed hook can't break the agent.
- claudecode: GetAgentHooks docstring covers Notification + SessionEnd
(the slice already included them; only the comment was stale).
---------
Co-authored-by: harshitsinghbhandari <dev@theharshitsingh.com>
* feat(cdc): add SSE event stream replay
* fix(cdc): document SSE route registration
* test(httpd): cover SSE dedupe and Last-Event-ID cursor paths
Two gaps in events_test.go coverage:
- TestEventsStreamDeduplicatesLiveEventOverlappingReplay: a live event whose
seq falls within the already-replayed range must be silently dropped by
writeSSEEvent so the client sees each seq exactly once. Publishes seq=5
(duplicate of replay) and seq=6 (new) into the live buffer before replay
returns; asserts the client receives [5,6], not [5,5,6].
- TestEventsStreamParsesLastEventIDHeader: Last-Event-ID header must be used
as the replay cursor when the after query param is absent. Source returns
after+1, so receiving seq=8 proves the header was parsed as 7.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore: regenerate schema.ts for GET /api/v1/events
Regenerated with npm run api after merging the OpenAPI spec generation
tooling from main. Adds the streamEvents operation and its after cursor
parameter to the TypeScript API types.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: harden SSE event stream headers
---------
Co-authored-by: Vaibhaav <user@example.com>
Co-authored-by: Pritom14 <pritommazumdar1995@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: add npm run api scripts and document API contract change flow
Adds three root package.json scripts mirroring the sqlc convention:
- api:spec — runs go generate to regenerate openapi.yaml
- api:ts — runs openapi-typescript@7.4.4 to generate frontend/src/api/schema.ts
- api — runs both in sequence (the single contributor command)
Documents the full flow in a new AGENTS.md "API contract changes" section
so contributors know which files to edit, how to regenerate, and what to
verify. Implements slice 1 of issue #102.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: correct paths in AGENTS.md API contract changes section
- api:ts equivalent command: use full relative path
backend/internal/httpd/apispec/openapi.yaml, not bare openapi.yaml
- verify command: prefix with `cd backend &&` so it works from repo root
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat: add CI drift guard for OpenAPI spec and frontend TS types
- Adds api-drift job to go.yml: regenerates openapi.yaml + schema.ts
via `npm run api`, then fails if git diff detects any change
- Expands go.yml path filter to trigger on package.json and schema.ts
- Commits initial schema.ts generated from the current spec
- Notes the CI guard in AGENTS.md API contract changes section
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: address review feedback on api-drift CI job
- Scope git diff to schema.ts only; openapi.yaml drift is already
caught by TestBuild_MatchesEmbedded in the build-test Go job
- Add npm ci step so openapi-typescript is installed from the lockfile
rather than re-fetched by npx on every run
- Move openapi-typescript from npx -y to a pinned devDependency
(exact 7.4.4, no caret) with a committed package-lock.json
- Note in AGENTS.md Verify block that go test does not cover schema.ts drift
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Initial buildable skeleton for the agent-orchestrator rewrite, splitting
the repo into a Go backend daemon and an Electron + TypeScript frontend.
- backend/: go.mod (Go 1.22) + main.go that compiles and prints a startup line
- frontend/: package.json, strict tsconfig.json, Electron main-process stub
- .gitignore for Node/Electron/Go/OS/editor/env artifacts
- README note describing the new two-folder structure
No app logic or architecture layering yet (routes/controllers/services/etc.
come in a later task). go build and tsc --noEmit both pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>