* feat: add agent catalog API and integrate with project settings
- Implemented AgentsController to handle /agents endpoint, returning a list of supported and installed agents.
- Created agent inventory service to manage agent data and detect installed agents.
- Updated ProjectSettingsForm to fetch and display agent information, including installed and supported agents.
- Enhanced error handling for agent detection and orchestrator restarts.
- Added tests for agent catalog and service to ensure correct functionality and error handling.
* Implement agent authorization status checks and update frontend to reflect changes
- Added `AuthStatus` method to various agent plugins to check authorization status using CLI probes.
- Introduced `authprobe` package to handle common CLI command checks for agent authorization.
- Updated backend tests to include scenarios for authorized and unauthorized agents.
- Modified frontend API schema to include `authorized` counts and `authStatus` for agents.
- Enhanced `ProjectSettingsForm` to display authorized agents and their statuses, including prompts for login when necessary.
- Adjusted agent selection logic to prioritize authorized agents and provide feedback for unauthorized or uninstalled agents.
* fix: simplify orchestrator replacement retry flow
* refactor: cache agent catalog ,remove AgentCounts schema and related references from API and frontend
* fix: clarify orchestrator replacement recovery state
* feat: enhance project settings and agent management
- Updated NewTaskDialog tests to increase timeout for async operations.
- Modified ProjectSettingsForm tests to improve agent handling and validation messages.
- Refactored ProjectSettingsForm component to streamline agent selection and validation logic.
- Introduced new agent service to manage agent inventory and authentication status.
- Improved Sidebar tests to ensure proper agent options are loaded and handled.
- Enhanced SessionsBoard component by removing unused imports and optimizing state management.
- Fixed Select component styling for better consistency in UI.
- Added error handling for AO daemon readiness in ShellLayout.
* chore: format with prettier [skip ci]
* feat: add AuthStatus method documentation and improve error handling in session manager
* feat: enhance agent authentication and configuration management
- Implement local authentication status checks for PI, Qwen, and Vibe agents.
- Introduce JSON-based authentication status retrieval for PI agents.
- Add environment variable checks for Qwen agents and improve settings file parsing.
- Enhance Vibe agent authentication with support for environment variables and session logs.
- Update agent service to handle asynchronous probing for installed and authorized agents.
- Modify session manager to support prompt delivery strategies based on agent capabilities.
- Improve frontend agent selection UI with loading states and error handling.
- Add tests for new authentication logic and session management features.
* chore: format with prettier [skip ci]
* test: fix session manager fake after rebase
* feat: enhance agent authentication status checks
- Implement local authentication status checks for the Devin, Droid, Kiro, and other agents.
- Add support for reading credentials from specific configuration files and environment variables.
- Introduce new tests for various agents to ensure proper authentication status reporting.
- Refactor existing authentication logic to improve clarity and maintainability.
- Remove deprecated agent setup warnings from the SessionsBoard component in the frontend.
* feat: clear environment variables in auth status tests for Aider and OpenCode
* feat: enhance error handling in authentication status functions and update component props
* feat: integrate fallback agents in RequiredAgentField and streamline props handling
* fix: refactor Sidebar test imports and parameters for clarity
* refactor: remove orchestrator retirement logic and related tests
- Deleted the RetireOrchestrator function and its associated error handling.
- Removed tests related to orchestrator retirement and state management.
- Simplified ProjectSettingsForm by eliminating orchestrator restart logic and related UI elements.
- Updated API client mocks to reflect the removal of orchestrator-related functionality.
* feat: enhance agent management and error handling
- Added agent refresh functionality in ProjectSettingsForm with UI updates for agent availability.
- Implemented `refreshAgents` API call to fetch the latest agent catalog.
- Updated agent selection logic to disable unavailable agents and show appropriate error messages.
- Enhanced error handling in `apiErrorMessage` to include daemon error codes alongside messages.
- Created new test cases for agent availability and error handling in Sidebar component.
- Introduced `ResolveBinary` method for multiple agent adapters to standardize binary resolution.
- Added new agent adapter files for various agents (e.g., Aider, Claude Code, etc.) to support binary resolution.
* chore: format with prettier [skip ci]
* fix: satisfy backend lint checks
* refactor: clean up authentication logic and improve error handling across agents
* chore: format with prettier [skip ci]
* feat(authprobe): enhance status classification for authentication outputs and add tests for explicit false/true keys
chore(docs): update README to remove outdated agent adapter contract references
fix(components): improve agent selection logic to handle unknown auth status and update related tests
* chore: format with prettier [skip ci]
* refactor: remove shell environment authentication logic and update related tests
* feat(tests): integrate QueryClient for agent data in CreateProjectAgentSheet tests
---------
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
* feat(tracker): GitHub issue intake observer
Adds an opt-in daemon poll loop that spawns one worker session per
eligible open GitHub issue, keyed by the canonical "github:<native>"
id so restarts cannot double-spawn. Eligibility requires an explicit
label or assignee rule to avoid draining an entire backlog.
Provider dispatch goes through a TrackerResolver interface
(SingleTrackerResolver for now) so Linear/Jira can be added later as
new adapters plus a resolver-map entry, without touching the poll,
eligibility, or backoff logic. Reuses the existing rate-limit-aware
GitHub tracker adapter and backs off per-project on any poll failure
(including rate limits) instead of retrying in a tight loop.
Closes#2324.
* feat(frontend): surface GitHub tracker intake in project settings
Adds a Tracker Intake card to project settings (enable, repository
override, labels, assignee) with inline validation requiring at
least one label or assignee before intake can be saved. Session
cards and the inspector show the canonical "github:<native>" issue
id when a session was spawned by intake.
Regenerated frontend/src/api/schema.ts against the backend's
TrackerIntakeConfig. The provider is currently fixed to "github";
adding a picker for future providers is additive once the backend
enum grows.
Closes#2324.
* fix(tracker): start the intake observer unconditionally at boot
startTrackerIntake scanned projects once at daemon startup and skipped
starting the observer loop entirely when none had intake enabled yet.
Poll() already re-reads every project's config on each tick and skips
disabled projects there, so the boot-time gate only broke the common
case: enabling intake on a project after the daemon is already running
silently never got picked up until the next restart, with no error or
log line to explain why.
Always start the loop; the adapter (and its token resolution) stays
lazy regardless, so there's no added cost when intake is unused.
Found while manually verifying issue intake end-to-end against a live
dev daemon: enabled intake via the settings UI, saved successfully,
but no session ever spawned for a matching labeled issue.
* fix(frontend): show auto-detected repo link, hide labels input for now
The Electron app only registers git projects today, so the daemon
always has a usable git origin to derive owner/repo from when
trackerIntake.repo is unset (trackerRepo() in observer.go, already
covered by every Poll-level test in observer_test.go). Replace the
manual Repository input with a read-only link derived client-side
from the project's own git origin, purely for display — the daemon's
own derivation at poll time is unaffected either way.
Comment out the Labels input; Assignee is the only intake eligibility
rule editable from this form for now. form.intakeLabels/intakeRepo
stay wired into buildIntake so a value set via the CLI round-trips
on a UI save instead of being silently cleared.
* chore: format with prettier [skip ci]
* feat(frontend): offer issue intake at project creation
Add the GitHub tracker intake controls to the create-project sheet so a
project can opt into issue-driven worker spawning at creation time, not
only later via Settings.
- Extract the shared IntakeFields component (enable toggle, assignee
input, validation, credential hint) plus buildIntake/intakeNeedsRule/
deriveGitHubRepo helpers into IntakeFields.tsx, and consume it from
ProjectSettingsForm so the two surfaces can't drift.
- CreateProjectAgentSheet renders IntakeFields (no repo-preview row,
since the git origin isn't known there; the daemon derives the repo).
The selection now carries an optional trackerIntake payload, gated by
the same "requires a label or assignee" rule the backend enforces.
- Thread trackerIntake through Sidebar's onCreateProject into the
POST /api/v1/projects config. No backend change: the endpoint already
accepts a full ProjectConfig and the intake observer picks up newly
enabled projects on its next tick.
Verified in the web preview: enabling reveals the assignee field and
gates submit until a rule is set; submit builds the intake payload.
* chore: format with prettier [skip ci]
* feat(frontend): simplify intake controls in the create-project sheet
Reduce the create sheet's tracker-intake block to the essentials: the
enable toggle plus an info icon whose hover tooltip explains what
enabling does, then the assignee input. Drop the descriptive intro
paragraph, the inline "requires a label or assignee" guard text, and
the credential hint — the sheet stays minimal and submit gating already
communicates the missing rule via the disabled button.
- IntakeFields gains a `compact` prop: hides the prose, folds the
explanation into an Info tooltip, and drops the trailing help text.
The tooltip is wrapped in its own TooltipProvider so the component is
self-contained regardless of ancestor. The verbose settings card is
unchanged (renders without `compact`).
- Assignee placeholder reworded to "type username or * for any".
Verified in the web preview: the info tooltip surfaces the one-line
description on hover, the assignee placeholder is updated, and no other
copy remains in the create sheet.
* perf(tracker): cache GitHub issue lists with ETag conditional requests
The intake observer polls Tracker.List for the same repo+filter every
tick, and each poll did an uncached GET + full JSON decode even when
nothing changed. Add HTTP conditional requests so unchanged polls are
cheap and don't consume the primary rate limit.
- Tracker holds a per-request-path cache of {etag, mapped issues},
guarded by a mutex. The key space is bounded by intake-enabled
repo/filter pairs, so no eviction is needed.
- List sends If-None-Match with the cached ETag (verbatim, preserving
weak validators). On 304 it returns the cached issues without
re-decoding (GitHub 304s don't count against the primary rate limit);
a rotated ETag on the 304 is recorded. On 200 it stores the new
{etag, issues}, or drops a stale entry if the response omits an ETag.
A 304 without a prior validator falls back to an unconditional refetch.
- Extract the HTTP round-trip into roundTrip(); do() delegates to it so
Get and Preflight keep byte-for-byte identical behavior. roundTrip
owns If-None-Match, ETag capture, and 304 handling before
classifyError.
Tests cover revalidation returning cached issues on 304, ETag rotation
on change, separate cache keys per filter, and no caching when the
response carries no ETag.
* feat(frontend): trim tracker intake copy in project settings
Reduce the settings Tracker Intake card to a one-line description
("Auto-spawn worker sessions from matching tracker issues.") and drop
the trailing credential/daemon-restart hint. The compact create sheet is
unaffected (it already renders neither).
* feat(tracker): scope v1 intake to assignee-only
Per PR review, labels were a persisted/API/CLI eligibility rule while the
UI only ever exposed assignee — surface beyond the intended v1 scope.
Remove labels from intake end-to-end; assignee becomes the required and
only eligibility rule.
- domain: drop TrackerIntakeConfig.Labels; Validate() now requires a
non-empty assignee when enabled (was "labels or assignee").
- observer: pass only State+Assignee into ListFilter; drop the label
match loop in issueMatchesConfig.
- CLI: remove --tracker-label and its plumbing.
- Regenerate openapi.yaml + schema.ts (labels gone from the schema).
- frontend: drop labels from IntakeForm/buildIntake/intakeNeedsRule and
the settings form state; validation copy now "requires an assignee".
Remove the label round-trip test; keep assignee coverage.
The generic domain.ListFilter.Labels adapter capability is retained;
only intake stops using it.
* fix(tracker): paginate GitHub issue listing with page-aware ETag cache
Per PR review, single-page listing is a correctness bug for intake, not
just a bounded v1 tradeoff: with more than a page of eligible open
issues, AO re-sees the same first page every poll, the persisted
issue_id dedupe skips the already-spawned first-page issues, and later
pages are never fetched or spawned.
- List now requests per_page=100 and follows the GitHub Link header
rel="next" until no next link remains, accumulating issues across all
pages. A maxListPages guard fails loud on a pathological Link cycle.
- The ETag cache is page-aware: each entry stores {etag, issues,
nextPath} keyed by the per-page request path, so a 304 Not Modified
still continues to the cached next page. roundTrip now surfaces the
parsed next path alongside the ETag; do() delegates unchanged so
Get/Preflight keep identical behavior.
- ListFilter.Limit becomes an optional total-result cap (page size is
fixed at the provider max).
Tests cover multi-page accumulation, all-304 revalidation continuing the
cached chain, page-count shrink orphaning a stale entry, and Link
header parsing.
* style(session_manager): gofmt manager_test.go to unblock CI
The session-restart test carried a struct-literal alignment that gofmt
(and golangci-lint's goimports) reject, failing the build-test and lint
jobs. Whitespace-only reformat; no test logic changes.
---------
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: whoisasx <adil.business4064@gmail.com>
manager_test.go landed from #2350 with a gofmt violation (extra spaces
aligning a struct field), turning main red: build-test's `gofmt -l .`
and lint's goimports both flag it. #2350's merge push never ran the Go
workflow, so it went unnoticed until the next push surfaced it.
Pure `gofmt -w` output, one whitespace line, no behavior change.
* feat(sidebar): purple nightly badge on the wordmark
Add a small purple pill labeled "nightly" next to the "Agent Orchestrator"
wordmark in the sidebar header. The badge:
- is derived from the running app version string (contains "-nightly."),
not the update-channel setting, so it reflects the actual binary identity
- is hidden in the collapsed icon rail (group-data-[collapsible=icon]:hidden)
- uses a new --purple CSS token added to both dark (#a78bfa) and light
(#7c3aed) palette blocks in styles.css, styled via color-mix following
the existing reviewer-status idiom
- is shrink-0 so it does not crowd the flex-1 truncate wordmark span
Stable builds show no badge. Dev builds ("0.0.0-preview") show no badge.
Part of AgentWrapper/agent-orchestrator#2377.
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>
* fix(frontend): show session display name in terminal toolbar header
The terminal toolbar showed the raw session id. Show the display name
(session.title, same field the sidebar renders) instead, and
'Orchestrator' for orchestrator sessions.
Fixes#2380
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test(frontend): cover terminal toolbar session label
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
The SCM observer only polled a project's push origin for open PRs, so a
PR opened from the fork against an upstream base repo (the standard
fork -> upstream flow) was never discovered: GitHub lists a PR under its
base repo, not its head, so an origin-only scan can't see it. Sessions
that opened such PRs showed no PR on the dashboard.
Scan every GitHub remote in the project checkout (origin + upstreams /
mirrors) for open PRs, and attribute a PR to a session only when its
head branch lives in that session's push origin. This surfaces
cross-fork PRs while preserving the no-misattribution guarantee: a
stranger's fork PR has a head repo no session owns and is dropped.
Tracked cross-fork PRs are keyed for refresh by their own recorded repo
(the upstream base) so the GraphQL refetch targets the right repo.
* test(session_manager): prove sessions survive daemon restart and re-adopt (#2335)
Add an end-to-end Reconcile() durability test covering the full mix of
session states a daemon restart/upgrade leaves behind: an alive
orchestrator and worker are adopted in place under their original ids
(no id increment, runtime never torn down), a worker whose runtime died
with the daemon is captured and relaunched under its original id, and a
truly-dead unmarked session is not resurrected.
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(prompt): point sessions at using-ao skill for CLI usage
Append a short standing-prompt pointer to skills/using-ao/SKILL.md so
orchestrator and worker sessions read the skill catalog before using the
ao CLI, instead of inlining the whole command surface.
* docs(skills): add using-ao skill cataloging the ao CLI
Full command catalog sourced verbatim from 'ao --help' recursively:
SKILL.md (top-level catalog), commands/*.md (one per command with every
subcommand, flag, and examples), and references.md (natural-language to
command table).
* feat(skills): install using-ao skill under the data dir on daemon boot
A repo-relative skills/ path only resolves when a worker's project is the
AO repo itself; the ao CLI is used in every session regardless of project.
Move the skill into the Go module, embed it, and clobber-install it into
<dataDir>/skills/using-ao at daemon boot (before any session spawns, so no
locking is needed). The embedded copy is the single source of truth: a new
daemon build always refreshes it, so there is no version marker to drift.
Flip the session-prompt pointer to the absolute installed path so it
resolves from any project's worktree.
* fix(skillassets): tighten install perms to satisfy gosec
golangci-lint gosec flagged G301 (dir perms) and G306 (file perms).
Use 0o750 for dirs and 0o600 for files, matching the repo convention
for daemon-owned single-user state.
* Update preview.md with dev server instructions
Added note about using the correct URL for the dev server.
---------
Co-authored-by: Vaibhaav Tiwari <155460282+Vaibhaav-Tiwari@users.noreply.github.com>
Convert the Prettier workflow from auto-formatting and pushing commits
back to the PR branch into a check-only job. It now runs
`prettier@3 --check .`, which annotates and fails on unformatted files
but never writes, commits, or pushes anything. Drops the auto-commit
step and narrows permissions to `contents: read`.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* docs(bug-triage): retarget skill at the Go rewrite and upstream repo
Rewrite the bug-triage skill for this repository: file issues/PRs on the
upstream AgentWrapper/agent-orchestrator, swap the Zellij runtime notes for
tmux (ConPTY on Windows), refresh the label list to match upstream, and drop
ReverbCode-specific naming. Port 3001, ~/.ao paths, and the CLI/daemon layout
are unchanged and were re-verified against backend/.
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(review): support more reviewer harnesses
* fix(review): preserve reviewer daemon context
* fix(review): allow printf-piped review commands in reviewer allowlists
The review prompt now pipes JSON into `gh api` and `ao review submit`
via `printf '%s' ... | cmd` (heredocs break in interactive PTY panes).
Widen the claude-code allowlist with `Bash(printf:*)` and the opencode
permission policy with `printf * | gh api *` / `printf * | ao review
submit *` so those piped commands pass each tool's allowlist. Cover
both with tests that model each tool's matching semantics.
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>
Pre-fill both role agents with claude-code in the create-project dialog and project settings instead of requiring a selection; the required-agent validation gate is removed. Users can still change either agent at creation or later in settings.
Replaces the inaccurate opt-in framing. Documents that telemetry is on
by default in the renderer, covers session recording, privacy redaction,
the persistent install ID, and how to override or disable via build-time
env vars.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(sidebar): inline-edit session display name via hover pencil
Reuse the merged PATCH /sessions/{id} rename endpoint (#2302). A pencil
reveals on session-row hover/focus; clicking it swaps the label for an
inline input capped at 20 chars (same as spawn --name). Enter/blur saves
and invalidates the workspace query so the rename survives reload; Escape
cancels.
Refs #2301
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(sidebar): cover inline session rename save/cancel/cap
Refs #2301
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>
After Cmd+Q quit + reopen, sessions the user killed reappeared as alive.
The session_worktrees "shutdown-saved" restore marker was write-only:
RestoreAll auto-relaunches any terminated session that still has a marker,
but Kill never deleted it and RestoreAll never consumed it. A session that
survived one reopen cycle (gaining a marker via reconcileLive) and was then
killed still carried the marker, so the next boot resurrected it.
Two-layer fix:
- Kill now deletes the marker (best-effort) after MarkTerminated: a user
kill is explicit terminal intent and must not leave a resurrection marker.
- RestoreAll deletes the marker after consuming preserveRef and attempting
relaunch, making it one-shot so it never outlives a single restart. A
still-live session re-acquires a fresh marker at the next quit.
Manual Restore stays marker-independent, so killed sessions remain
restorable on demand, just not auto-resurrected on boot.
Adds DeleteSessionWorktrees to the Manager Store interface (the concrete
store already implements it) and tests covering all three behaviors.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat(spawn): add required --name flag for sidebar display name
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(spawn): require --name for sidebar label; cap at 20 chars
Add a required --name flag to `ao spawn` that sets the session's
sidebar display name. The CLI rejects a missing or >20-character name
before contacting the daemon.
The daemon's POST /sessions keeps displayName optional (the desktop
new-task dialog omits it and the read model falls back to the session
id) but enforces the same 20-character cap when present, so a direct
API call cannot exceed it. The value flows CLI -> SpawnSessionRequest
-> SpawnConfig -> session record, and the existing read-model fallback
(displayName ?? issueId ?? id) renders it in the sidebar unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Two PRs (#2193, #2200) merged ~2h apart each added a migration
file numbered 0020, since each branch only saw one such file at
CI time and neither was rebased against the other before merge.
Renumbers 0020_pr_reviews.sql to 0021_pr_reviews.sql.
TestMigrationVersionsAreUnique already existed and is correct;
it didn't catch this because it only runs against each PR's
branch state, not the merged result of both PRs together.
The reviewer's read-only guarantee was enforced only by the prompt. Add
AllowedTools/DisallowedTools to ports.LaunchConfig and plumb them through
the claude-code agent adapter to --allowedTools/--disallowedTools (each list
comma-joined into one value so a rule containing spaces like "Bash(git
diff:*)" is not split into separate tool names). Empty lists emit nothing, so
worker sessions are unaffected.
Launch the reviewer off bypassPermissions (which skips the permission system
and ignores allow/deny rules) in the default auto mode, with an allowlist
scoped to Read/Grep/Glob and the few Bash commands a reviewer needs (gh, git
diff/log/show/status, ao review submit) and an explicit deny list for
Edit/Write/NotebookEdit/git push/git commit as defense in depth.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(ui): use popper position for SelectContent to prevent scroll reset
* fix(ui): use React.memo to prevent scroll resets without breaking popper position
* fix(ci): pin publish step to bash so the retry loop runs on Windows
The 3x retry wrapper added in #2266 is bash syntax, but the release
matrix Publish step inherited the runner default shell, which is
PowerShell on windows-latest. That made every Windows publish fail with
a ParserError. Pin shell: bash on all four publish steps.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(ci): point fresh-install smoke check at an empty release repo (#2267)
The container check asserts `ao start` fails cleanly on a fresh box with
no published asset. Now that AgentWrapper publishes a linux-x64 AppImage,
an unpinned smoke binary downloads it and exits 0, tripping the
assertion. Build the smoke binary against a release repo with no assets
so the fetch path deterministically 404s and start exits non-zero with a
clear error, preserving the test's intent without depending on what the
real repo publishes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Releases fail intermittently on transient macOS issues that hit EITHER
macOS leg, not just Intel: Apple notary -1009 connection-offline (seen on
macos-latest/arm64) and osx-sign keychain races, code object is not signed
at all (seen on macos-15-intel). Both pass on a manual re-run.
Wrap npm run publish in a 3x retry with backoff in all four release/nightly
publish steps so these self-heal in-place instead of failing the run (and,
for nightly, skipping the whole feed). Keeps the Intel leg coupled so the
x64 feed entry stays guaranteed.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Nightly builds publish as GitHub prereleases. configureFeed set channel
and allowDowngrade but never allowPrerelease, which defaults to false, so
electron-updater only inspected the latest NON-prerelease release
(e.g. v0.10.0) and 404d looking for nightly-mac.yml there. The nightly
channel therefore never resolved, regardless of whether a nightly was
published.
Set allowPrerelease = (channel === nightly): nightly scans prereleases
and finds the nightly feed; stable stays on non-prereleases only.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(update): generate app-update.yml at build time; drop runtime setFeedURL
electron-forge does not emit app-update.yml; electron-updater requires it at
process.resourcesPath to resolve the GitHub release feed. Without it every
packaged app threw ENOENT on the first download attempt, making updates
detected but never installed.
Two-part fix:
- forge.config.ts: add postPackage hook that writes app-update.yml into
each platform's Resources dir (baked from AO_RELEASE_REPO so fork builds
point at the fork, prod at AgentWrapper/agent-orchestrator).
- auto-updater.ts: remove setFeedURL + repo() + DEFAULT_RELEASE_REPO;
configureFeed now only sets channel + allowDowngrade. electron-updater
auto-loads the bundled yml on the first checkForUpdates call.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore: format with prettier [skip ci]
* fix(update): generate app-update.yml before signing, not after
The postPackage hook wrote app-update.yml into Contents/Resources AFTER osxSign
sealed the bundle, so the added file was unsealed and macOS reported the app as
"damaged" (codesign: "a sealed resource is missing or invalid"). Generate it in
a prePackage hook and ship it via packagerConfig.extraResource, so it is copied
into the bundle and signed as part of the seal. The generated app-update.yml is
gitignored.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore: format with prettier [skip ci]
* ci(release): move Intel leg to macos-15-intel and gate the feed on it
macos-13 is deprecated and has no runner capacity (multi-hour queues),
so the detached release-intel leg never completed. Switch it to the
supported macos-15-intel image in both the release and nightly workflows.
Now that the Intel leg gets a runner and builds + signs the x64 installer
reliably (verified on fork v0.10.10), re-couple it into publish-feed
(needs: [release, release-intel]) so latest-mac.yml / nightly-mac.yml
always carry the x64 entry and Intel macOS users receive auto-updates.
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>
Stable release builds read the version from frontend/package.json (the
release workflow does not stamp from the git tag, unlike nightly), so the
0.10.0 prod cut needs this committed at the tagged commit. Otherwise the
app ships as 0.0.0 and the stable update channel never advances.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
The Desktop release fires on any desktop-v* tag push and consumes the
repo-level signing secrets, so any write-access collaborator can cut a
signed prod build. Reference the `release` environment from the build
jobs so a designated reviewer must approve before the secrets are used.
Takes effect once required reviewers are configured on that environment.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Remove macos-13 from the release matrix in both frontend-release.yml and
frontend-nightly.yml, replacing it with a standalone release-intel job
that runs on macos-13 independently. publish-feed keeps needs: release
(the three fast legs), so it no longer waits hours for the scarce Intel
runner. The darwin-x64 installer and stable alias are still built and
uploaded; feed.mjs includes the x64 entry on a best-effort basis.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat(settings): IPC bridge for global update settings (get/set)
Expose readUpdateSettings/writeUpdateSettings to the renderer via
updateSettings:get/set so the Global Settings page can read and persist
the auto-update channel choice. Refs #2207.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(cli): show ao start download + install progress
The fetch path streamed a hundreds-of-MB asset and ran a silent install
with zero output, so a slow download looked like a hang. Print a start
line (asset, ~size, repo), stream a TTY-aware progress reader (live \r
percentage on a terminal, start+done lines off a TTY), and announce the
otherwise-silent unpack/install step. stderr only, so --json stdout stays
clean; no new dependency. Closes#2234.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(settings): Updates (channel) + Migration sections in Global Settings
- UpdatesSection: pick Stable vs Nightly auto-update channel, persisted to
~/.ao/update-settings.json via the new IPC bridge. Closes#2207.
- MigrationSection: drop-in card showing migration status + last report/error
and a Run/Re-run button hitting the idempotent POST /api/v1/import; works
even after Don't Migrate. Closes#2205.
- Wire both into the (previously blank) GlobalSettingsForm; cover both with tests.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore: format with prettier [skip ci]
* fix(browser): re-measure preview bounds after layout transitions
The native WebContentsView preview is positioned from a renderer-measured
rect, re-measured on ResizeObserver + window resize/scroll. But a
ResizeObserver fires on size changes only, so a position-only layout shift
(entering/leaving pop-out moves the slot to another panel; opening the
inspector, which ao preview does, reflows its x) left the overlay at stale
bounds, visibly spilling over the sidebar/terminal until an unrelated resize
fixed it. Drive a settle re-measure on mount and on active/pop-out
transitions (immediate frame + once the ~240ms panel transition settles) so
the final geometry always wins.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore(backend): gofmt + goimports pre-existing files (fix red CI)
config.go, project_test.go, importer_test.go and spawn_windows.go were
committed unformatted earlier and fail go.yml's gofmt check and golangci's
goimports formatter on main. Apply `golangci-lint fmt`; formatting only, no
logic change. Unblocks this PR's lint/build-test jobs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(settings): manual Check for updates + Update button
Auto-update only checked at launch, so users with auto-updates off (or who
just want it now) had no way to update from the app. Add an on-demand flow:
checkForUpdatesNow/downloadUpdateNow/quitAndInstallUpdate in auto-updater
(works regardless of the opt-in; not-packaged dev surfaces 'unsupported'),
forwarded to the renderer as a live UpdateStatus over updates:status. The
Updates section gains a Check for updates button and an Update button that
downloads then installs (Restart & install), with checking/available/
downloading%/downloaded/error status. Refs #2207.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore: format with prettier [skip ci]
* fix(sidebar): always offer Global settings in the footer menu
The footer Settings menu showed either Project settings (with a project
selected) or Global settings (with none), so global settings was
unreachable while inside a project. Always list Global settings; add
Project settings above it when a project is active. Refs #2205.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(settings): show current app version in Updates section
Surface app.getVersion() as a 'Current version vX.Y.Z' line above the
update controls so users can see what they're running. Refs #2207.
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 gitleaks job scans full git history on push to main (gitleaks-action
v1.6.0 runs `gitleaks --path` with no commit filter on push events). The
ReverbCode rewrite graft (#2166) carried 279 commits of prior history, 10 of
which trip 42 findings: example secret-patterns in security docs and
secret-redaction test fixtures. All offending files are already deleted from
the tree, so PRs pass (they scan only their own diff) but every push to main
re-scans history and fails indefinitely.
Add `.github/.gitleaks.toml` (the action's default config-path) carrying the
full gitleaks v7.4.0 default ruleset plus a commit allowlist for the 10
historical commits. Detection on new code is unchanged; only these specific
historical commits are skipped.
Verified with gitleaks v7.4.0: full-history scan goes 42 leaks -> 0.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
waitForStopped removed the run-file and then additionally waited for the
daemon process to fully exit, erroring with "removed run-file but did not
exit within 10s" if it lingered past the stop timeout. The run-file is the
daemon's own liveness marker: once it is gone the daemon has committed to
stopping. When no desktop client is connected the daemon can drain its
background workers slower than the stop timeout, which made ao stop
spuriously report failure (the TestE2E_Lifecycle failure in #2214).
Treat run-file removal as stopped: keep polling for full process exit as a
best effort (so Windows releases inherited handles before callers clean the
data dir) but no longer error when that grace elapses.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Add a verbatim Apache License 2.0 LICENSE file at the repo root, with the
appendix copyright line filled as "Copyright 2026 Untrivial".
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* docs(spec): feed-publishing + macOS signing design (#2220)
Design for the electron-updater feed-publishing workstream plus the macOS
code-signing half of Track B, shipped as one PR so a single review unblocks
auto-update. Sidecar-only post-matrix join job emits latest*.yml / nightly*.yml
+ .blockmap sidecars on all three platforms (no maker changes, no artifact
mutation). macOS signing reproduces the proven local runbook in CI: keychain
provisioning, hardened-runtime entitlements, and notarization via the App Store
Connect API key (.p8) path, with the osxNotarize cast fixed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(plan): feed-publishing + macOS signing implementation plan (#2220)
Six TDD tasks: blockmap wrapper, feed module (selection + yml), osxNotarize
API-key rewire, macOS signing-setup composite action, and the two workflow
wirings (latest + nightly feed jobs). Two evidence-based simplifications vs the
spec: no custom entitlements plist (default osxSign entitlements are proven by
the local runbook) and no Node bump (CI already below the crash ceiling).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(feed): blockmap sidecar wrapper over app-builder-lib
Adds frontend/scripts/blockmap.mjs isolating the single fragile internal
import (app-builder-lib/out/targets/blockmap/blockmap.js) behind a thin
writeBlockmap(filePath) wrapper. Returns { sha512, size } only, omitting
blockMapSize to force the sidecar differential path in electron-updater.
Test added with // @vitest-environment node directive (required: project
vitest config defaults to jsdom; same pattern as nightly-version.test.mjs).
TDD: red on missing module, green after implementation.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(feed): installer selection + electron-updater yml assembly
Implement selectInstallers, feedFilename, and buildYml pure functions
for packaging versioned installers and generating platform-specific
feed metadata. Excludes ao-start aliases and deb/rpm packages.
Generates blockmap sidecars and yml with deprecated top-level
path/sha512 for compatibility, no blockMapSize (forces sidecar diff).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(sign): notarize via App Store Connect API key; drop osxNotarize cast
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(ci): macOS signing-setup composite action
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(ci): sign macOS + publish latest feed in the release workflow
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(ci): sign macOS + publish nightly feed in the nightly workflow
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>
* docs(spec): auto-update with stable + nightly channels
Design spec for channel-aware auto-update via electron-updater: stable
(manual semver releases) and nightly (daily CI cron, version
X.Y.(Z+1)-nightly.<UTC-ts>+<sha>). Covers runtime wiring, the feed,
the version scheme and its ordering/channel rationale, the nightly
pipeline, in-app UX (opt-in + channel + nightly disclaimer), error
handling, testing, and the deferred/prereq items (CI signing for macOS
updates, stable version stamping, polished UI in #2207).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(plan): auto-update channels implementation plan
Bite-sized TDD plan for the in-app channel-aware updater + nightly pipeline:
nightly version compute module, daily cron workflow, macOS feed metadata,
~/.ao update-settings module, electron-updater shell + main.ts wiring, and the
first-run opt-in/channel/nightly-disclaimer prompt. Deferred items and Track-B
prereqs (CI signing, stable stamping) are called out.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(release): nightly version compute module
computeNightlyVersion -> X.Y.(Z+1)-nightly.<UTC-ts>+<sha>: next-patch base,
fixed-width UTC timestamp for monotonic prerelease ordering, sha as build
metadata. Pure ESM so CI runs it and vitest tests it.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(release): daily nightly build + publish workflow
Cron computes the nightly version from the latest stable tag, stamps it,
builds all platforms, and publishes a prerelease (nightly channel). Skips
when HEAD is already covered by the latest nightly. forge publisher prerelease
flag is now env-driven (AO_RELEASE_PRERELEASE) so stable stays non-prerelease.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(release): pass nightly version via env, not inline interpolation
Avoids interpolating ${{ }} into the run script (GitHub Actions injection
hardening); the version now arrives via the NIGHTLY_VERSION env var.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(update): persist auto-update settings under ~/.ao
readUpdateSettings/writeUpdateSettings store {enabled, channel, nightlyAck}
with safe defaults and channel coercion, atomic temp+rename like app-state.ts.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(update): channel-aware electron-updater wiring
Replace update-electron-app (stable-only, no channels) with electron-updater
driven by the user's ~/.ao settings: channel from settings, allowDowngrade for
channel switches, auto-download gated on opt-in, errors swallowed. Feed
configured via setFeedURL since forge does not emit app-update.yml.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* style(update): tabs in auto-updater.ts to match repo prettier config
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(update): first-run opt-in + channel + nightly disclaimer prompt
Minimal main-process dialog flow: opt into auto-updates, pick stable/nightly,
and acknowledge a nightly instability/data-loss disclaimer. Persists the choice
to ~/.ao. Polished Settings-page selector is tracked in #2207.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs(spec): correct feed-yml premise (no platform emits it as built)
Final review verified that publish:null in both custom makers makes
electron-builder skip update-info yml generation on Windows and Linux too
(app-builder-lib PublishManager returns early before createUpdateInfoTasks),
not just signing-blocked macOS. The runtime updater is therefore inert on all
platforms until a separate feed-publishing workstream (yml gen+upload across
all 3 platforms, coupled to Track-B signing) lands. Recorded in the Feed
section and the prerequisites table.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(release): nightly skip-guard must match the v<version> tag namespace
The forge publisher tags nightly releases v<version> (e.g.
v0.10.4-nightly.<ts>), not desktop-v..., so the no-new-commits guard queried a
namespace that never exists and never fired. Query v*-nightly.* instead.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore: format with prettier [skip ci]
* chore(release): run nightly at 19:00 IST (13:30 UTC)
Was 03:00 UTC (08:30 AM IST); move to 13:30 UTC = 7:00 PM IST.
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>
* docs: design for dashboard legacy-migration popup + app-state marker
Spec for the app-side migration trigger (Approach A): projects-only import
daemon API + a migration marker in ~/.ao/app-state.json, with a launch-time
popup (Proceed / Skip / Don't Migrate). Settings redo path deferred to #2205.
Includes the projects-only import-offer backend plan it consumes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* docs: implementation plan for legacy-migration popup + marker
Part A reuses the projects-only import API (import-offer plan) with an
availability-only Status; Part B adds the app-state migration marker (schema v2),
IPC, the useMigrationOffer gate, and the MigrationPopup (Proceed/Skip/Don't
Migrate). Settings redo path deferred to #2205.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* refactor(legacyimport): scope import to projects + settings only
Remove orchestrator/transcript import code (orchestrator.go, claude.go,
session_import_store.go and their tests). Trim Store, Options, Report to
projects-only fields. Drop defaultClaudeProjectsDir and projectSessionsDir
from paths.go. Add yaml.TypeError robustness in config.go. Update cli/import.go
confirm prompt and summary. Update importer_test.go to projects-only assertions.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(importer): availability probe + projects-only run
Create service/importer.Manager with Status (physical availability check only,
no DB heuristic) and Run (delegates to legacyimport.Run). The app-state.json
marker governs whether to prompt; this service only answers whether legacy data
is physically present.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(httpd): import controller + DTOs (GET/POST /api/v1/import)
Add ImportStatusResponse/ImportRunResponse DTOs to dto.go. Create
ImportController with GET (status probe) and POST (run) handlers, both
returning 501 when Svc is nil. Wire APIDeps.Import + API.imports in api.go.
Add controller tests (status, status error, run, run error).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(apispec): describe /api/v1/import; regenerate openapi + schema.ts
Add import tag, importOperations() (GET + POST /api/v1/import), and schema
name mappings (ImportStatusResponse, ImportRunResponse, ImportReport) to
build.go. Regenerate openapi.yaml and frontend/src/api/schema.ts. Route-spec
parity test passes. Restore the nil-svc-501 import controller test now that
the spec includes the operation.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(daemon): mount import service on the API
Wire importsvc.New(importsvc.Deps{Store: store}) into APIDeps.Import in
daemon.go so the daemon serves GET/POST /api/v1/import backed by the live
sqlite store. Projects-only; no DataDir.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore(cli): drop resolved §6.4 first-boot-import TODO
The legacy import is now a daemon API (GET/POST /api/v1/import) served by
the importer service and the desktop app handles the popup prompt via the
app-state.json migration marker. The TODO comment is resolved.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore(legacyimport): remove dead isDir helper
isDir was only used by the deleted projectSessionsDir function.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore: update package-lock.json after npm install for api:ts
openapi-typescript was missing from root node_modules; npm install
populated it so npm run api:ts could regenerate schema.ts.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(httpd): drop em-dash comment + unused ImportStatusResult alias
Review findings M1/M2 from G1 task review.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(app-state): migration marker (schema v2) + updateMigration
Bump SCHEMA_VERSION to 2, add MigrationStatus/MigrationState types and
migration? field to AppStateMarker. Extract atomicWriteMarker helper and
reuse it. writeAppStateMarker now preserves an existing migration block
across launch writes. Add updateMigration (IPC setter) and readMigrationState
(IPC getter) exports. TDD: tests added first (red), then implementation (green).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(ipc): expose app-state migration getter/setter to the renderer
Add appState:getMigration / appState:setMigration IPC handlers in main.ts.
Add ao.appState.getMigration / setMigration to preload.ts (typed via AoBridge).
Add appState preview fallback in bridge.ts and test setup so AoBridge stays
satisfied in both browser preview and test environments.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* test(app-state): cover corrupt-marker case + clean up temp dirs
G2 review findings I2 (corrupt-JSON branch untested) and m1 (temp dirs not cleaned).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(renderer): useMigrationOffer gate (marker + availability)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(renderer): MigrationPopup (Proceed / Skip / Don't Migrate)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(renderer): surface MigrationPopup on the dashboard
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 desktop release only built on macos-latest (Apple Silicon, arm64),
so the darwin-x64 stable alias was never produced and Intel-Mac
ao start 404s fetching agent-orchestrator-darwin-x64.zip.
Add the macos-13 (Intel x64) runner to the job matrix and broaden the
macOS alias-upload step's condition from matrix.os == 'macos-latest' to
startsWith(matrix.os, 'macos') so it runs on both macOS runners. The
step already derives arch via uname -m, so the Intel runner now emits
and uploads the x64 alias. Updated the now-stale gap comment.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat(frontend): add blank Global Settings page
Add a Global Settings page to the desktop renderer, reachable from the
sidebar Settings menu when no project is selected. The project-scoped
Settings entry stays when a project is active; otherwise the menu shows
Global settings, which opens the new /settings route. The page body is
intentionally empty for now and will be filled in incrementally.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* feat(frontend): regenerate route tree + test Global Settings menu
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>