Commit Graph

204 Commits

Author SHA1 Message Date
NIKHIL ACHALE d51ddd6130
feat: enhance Kiro agent integration with custom AO agent prompts (#2444)
* feat: enhance Kiro agent integration with system prompts and agent configuration

* fix(kiro): clear stale model from hooks when configuration is removed
2026-07-06 02:58:08 +05:30
NIKHIL ACHALE 821cda64a8
feat(cli): context-aware agent spawn with agent catalog and auth preflight (#2411)
* feat(cli): add agent command for listing and refreshing agent catalog

- Implemented `ao agent ls` command to list supported agents and their installation/auth readiness.
- Added `--refresh` flag to refresh local install/auth probes before listing agents.
- Introduced JSON output option with `--json` for raw agent catalog response.
- Updated tests to cover new agent command functionality, including cached catalog usage and refresh behavior.
- Enhanced error handling and output formatting for better user experience.

* docs: add CLI Guide link to README for direct usage instructions

* docs: clarify direct CLI usage

* fix: tighten spawn agent preflight

* feat(cli): implement agent readiness probe and update API specifications
2026-07-04 21:23:03 +05:30
Harshit Singh Bhandari 2c08597ee6
refactor(adapters): cut ~3,100 LOC of redundancy in the agent adapter layer (#2349) (#2355)
* refactor(adapters): add shared helpers for adapter dedup (#2349)

Introduce the shared building blocks the per-adapter cleanup will use:
- ports.NormalizePermissionMode (finding 3)
- hookutil.FileExists (finding 10)
- binaryutil.ResolveBinary + BinarySpec (finding 2)
- activitystate.StandardDeriveActivityState (finding 4)
- agentbase.Base embed + StandardSessionInfo (findings 6-9)

No adapters wired up yet; behavior unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(goose): convert to shared adapter helpers (reference) (#2349)

Reference conversion proving the shared packages against real tests:
- hooks.go collapses onto hooksjson.Manager (finding 1)
- ResolveGooseBinary via binaryutil.BinarySpec (finding 2)
- gooseMode uses ports.NormalizePermissionMode (finding 3)
- activity.go deleted; dispatch points at activitystate (findings 4, 5)
- SessionInfo via agentbase.StandardSessionInfo; Base embed drops the
  GetConfigSpec/GetPromptDeliveryStrategy no-ops (findings 6-8)
- fileExists/atomicWriteFile copies removed (findings 5, 10)

Also adds hooksjson package + activitystate test. goose: 327->~90 LOC.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(adapters): dedup remaining 22 agent adapters onto shared helpers (#2349)

Applies the shared helpers across every remaining adapter:
- hooksjson.Manager for the matcher-group cohort (claudecode, qwen, droid)
- binaryutil.BinarySpec for 19 binary resolvers (aider/cursor/opencode/codex
  keep their special resolvers; all now use hookutil.FileExists)
- ports.NormalizePermissionMode replaces 15 private copies
- agentbase.Base embed drops the GetConfigSpec/GetPromptDeliveryStrategy no-ops
  (and GetAgentHooks/GetRestoreCommand/SessionInfo no-ops on hookless adapters)
- agentbase.StandardSessionInfo replaces the per-adapter metadata readers
- activitystate.StandardDeriveActivityState via dispatch for the 8 name-only
  derivers; their activity.go files removed (claudecode/codex/droid/agy/opencode
  keep payload-parsing derivers)
- 4 private atomicWriteFile copies missing fsync now use hookutil.AtomicWriteFile
- 23 private fileExists copies removed

Full backend build + vet + test suite green (1647 tests).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore: format with prettier [skip ci]

* fix(binaryutil): preserve per-adapter Windows candidate order (#2349)

Review feedback: the shared resolver hardcoded Windows candidate order as
APPDATA then LOCALAPPDATA, which flipped Kiro's lookup so the npm shim
(%APPDATA%\npm\kiro-cli.*) was probed before the native install
(%LOCALAPPDATA%\Programs\kiro\kiro-cli.exe). A fixed order can't preserve every
adapter's original order (goose/vibe want APPDATA first, kiro wants LOCALAPPDATA
first), so BinarySpec now takes an ordered WinPaths []WinPath list where each
entry names its base (WinAppData/WinLocalAppData/WinHome). Every adapter's list
reproduces its pre-refactor order exactly; kiro's native path is restored to
first.

go build / vet / test all green (1647 tests).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(adapters): drop unused resolvedBinary writes flagged by govet (#2349)

Embedding agentbase.Base (value receiver) lets govet's unusedwrite analyzer
prove that setting resolvedBinary in tests that only call Base-promoted methods
(GetConfigSpec/GetPromptDeliveryStrategy/SessionInfo/cancellation) is a dead
write. Drop the field from those 23 constructions; GetLaunchCommand/GetRestore
tests that actually read it keep it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* style: gofmt manager_test.go struct alignment (#2349)

Inherited via the merge of main: a SessionRecord literal aligned its Metadata
field across a multi-key line, which gofmt/goimports rejects. One-line reformat
to unblock CI.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(adapters): fix stale resolver fallback comments (#2349)

Review nit: 6 Resolve*Binary functions now delegate to binaryutil.ResolveBinary,
which returns a wrapped ports.ErrAgentBinaryNotFound rather than the bare binary
name. Update their doc comments (kiro, vibe, amp, agy, crush, cline) to match.

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>
2026-07-04 20:49:15 +05:30
NIKHIL ACHALE cc75a519ab
fix: replace orchestrators through safe canonical branch handoff (#2338)
* fix: replace orchestrators through canonical handoff

* fix(tests): correct logic in TestRetireForReplacementCapturesAndReleasesWorkspace

* fix: enhance tests and add project config handling in service and workspace components

* fix: update project summary to include orchestrator agent and adjust related tests

* fix: block orchestrator replacement on runtime teardown failure

* fix: enhance orchestrator handling in ProjectSettingsForm and SessionsBoard components

* fix: rename parameter for clarity in projectConfigPtr function
2026-07-04 11:35:03 +05:30
NIKHIL ACHALE a639e2025c
feat: add agent catalog/auth API and safer orchestrator switching (#2309)
* 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>
2026-07-04 10:59:34 +05:30
Anirudh Sharma d18ea87f57
feat(tracker): GitHub issue intake (backend + dashboard) (#2325)
* 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>
2026-07-04 02:52:14 +05:30
Harshit Singh Bhandari 8cadea5b16
fix(ci): gofmt manager_test.go to unbreak main (#2386)
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.
2026-07-03 22:42:27 +05:30
Harshit Singh Bhandari a32d507eb8
feat(scm): detect cross-fork PRs by scanning all project remotes (#2368)
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.
2026-07-03 20:50:41 +05:30
Harshit Singh Bhandari 3aa8e044b2
test(session_manager): prove agent sessions survive daemon restart/upgrade (#2335) (#2350)
* 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>
2026-07-03 19:59:27 +05:30
Harshit Singh Bhandari 57ba468fea
feat(skills): add using-ao skill, install it under the data dir, and wire it into session prompts (#2365)
* 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>
2026-07-03 18:11:15 +05:30
Dushyant Singh Hada 7e409d438b
fix: clear stale preview URL from SQLite and prevent stale white screen on preview clear (#2358)
* fix: remove auto-switch-to-summary effect, keep browser tab on clear

* fix: clear browser preview when session is terminated
2026-07-03 15:38:43 +05:30
VenkataSakethDakuri fadbdd26cd
Fix and enrich the orchestrator coordinator system prompt so it matches the current CLI and teaches harness selection + command discovery (#2361)
* Prompt changed for correct agent spawning

* Fix formatting issue
2026-07-03 01:36:14 +05:30
Anirudh Sharma e74fb65a85
feat(review): support additional reviewer harnesses (#2306)
* 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>
2026-07-02 19:25:18 +05:30
Khushi Diwan 6a7ba46078
fix: set terminal type for tmux attach (#2312)
- Force TERM for tmux attach processes
- Cover missing and unsupported TERM values
2026-07-01 03:27:29 +05:30
Harshit Singh Bhandari 60b651f20e
fix(session): clear shutdown-saved marker so killed sessions stay dead (#2319) (#2320)
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>
2026-07-01 03:00:09 +05:30
neversettle 010c7e4c1a
feat: default reviewer is always claude-code (#2241) - temporary change 2026-06-30 21:25:26 +05:30
Harshit Singh Bhandari 7c4a77d7cc
feat(spawn): add required --name flag for sidebar display name (#2302)
* 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>
2026-06-30 16:26:48 +05:30
Dushyant Singh Hada de9e90df45
fix: resolve duplicate migration version 20 collision (#2294)
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.
2026-06-30 11:23:54 +05:30
swyam sharma 2c4bf55e95
fix(spawn): fail fast when tmux is missing (#2259)
Co-authored-by: Swyam Sharma <cruzer@Swyams-MacBook-Pro.local>
2026-06-30 00:14:46 +05:30
Adil Shaikh a31cf1b582
fix: deep-link PR attention actions (#2200)
* fix: deep-link PR attention actions

* chore: format with prettier [skip ci]

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-06-29 21:16:35 +05:30
neversettle 8241868398
feat(review): enforce reviewer read-only via tool allowlist (#2194)
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>
2026-06-29 20:27:19 +05:30
neversettle cb456bb288
feat: show multi-PR review status (#2193)
* feat: support multi-pr review runs

* fix: avoid review state status stutter

* feat: batch review delivery by trigger

* test: update review inspector mocks

* chore: fold review batch migration into 0020

* fix: submit multi-pr reviews as one batch

* fix: make queued reviews autonomous

* fix: clarify multi-pr review submit prompt

* feat: show multi-pr review status

* chore: format with prettier [skip ci]

* feat: simplify multi-pr review summary

* chore: format with prettier [skip ci]

* fix: match multi-pr review card design

* chore: format with prettier [skip ci]

* fix: remove review session label

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Vaibhaav <user@example.com>
2026-06-29 18:20:28 +05:30
Harshit Singh Bhandari cfe505eb4d
feat: Global Settings (migration + update channel) + ao start download progress (#2235)
* 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>
2026-06-27 23:08:55 +05:30
Harshit Singh Bhandari 77f33c2864
fix(cli): treat run-file removal as stopped in ao stop (#2215)
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>
2026-06-27 20:24:02 +05:30
Harshit Singh Bhandari 572093de9d
feat: dashboard legacy-migration popup + app-state migration marker (#2219)
* 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>
2026-06-27 02:24:36 +05:30
Harshit Singh Bhandari bc84b6d2a3
feat(cli): ao start fetch/open for Windows and Linux (T6/T7) (#2204)
* feat(start): implement ao start fetch/open for Windows and Linux

Fill in the non-darwin branches of the bootstrapper (T6/T7):

- assetName() selects the per-GOOS stable release asset: windows ->
  agent-orchestrator-win32-x64.exe (NSIS installer), linux ->
  agent-orchestrator-linux-x64.AppImage. amd64-only for now via
  requireAMD64(), which returns a clear unsupported-arch error.
- fetchApp() dispatches per GOOS. Windows downloads the NSIS installer and
  runs it silently (/S) to the default per-user dir, then resolves the
  installed exe under %LOCALAPPDATA%\Programs. Linux downloads the AppImage
  to a stable path under ~/.ao (atomic temp+rename), chmods it executable,
  and skips any install step so re-runs resolve without re-fetching.
- knownAppLocations() scans the per-user and per-machine Windows install
  dirs and the stable Linux AppImage path.
- isUsableBundle() treats a win exe / linux AppImage as a regular file
  (darwin stays a directory).
- openApp() launches win/linux detached via the existing StartProcess seam,
  forwarding --installed-via=npm-bootstrap, and falls back to manual-open on
  spawn failure.

The Windows silent-install path is marked ponytail (untestable on the macOS
build host); a wrong install dir surfaces as a clear not-found error. Tests
cover asset naming, arch gating, scan locations, regular-file vs dir, and
the detached-spawn + fallback paths.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(release): build + publish a Linux AppImage for ao start (T7)

AppImage is the Linux fetch-and-run artifact for the bootstrapper (spec
§11.3): a single self-contained executable ao start downloads and runs
directly, no system package manager.

- makers/maker-appimage.ts: a MakerBase subclass bridging to
  electron-builder's buildForge (appImage target), mirroring maker-nsis.ts,
  since Forge has no first-party AppImage maker. publish:null so Forge owns
  release uploads.
- forge.config.ts: register MakerAppImage for linux; keep deb/rpm for users
  who want a system package.
- frontend-release.yml: on ubuntu-latest, copy the built AppImage to the
  stable, space-free name agent-orchestrator-linux-x64.AppImage and upload
  it to the v<version> release with --clobber, mirroring the Windows step.

Build-untested on this macOS host: the first ubuntu CI run must confirm the
electron-builder AppImage target token and the out/make/*.AppImage output
glob.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(ci): green up ao start Win/Linux PR (lint, cross-OS test, container smoke)

Five golangci-lint findings in start.go, two cross-OS test failures, and the
fresh-install container smoke were broken by the Win/Linux bootstrapper diff.

Go lint (start.go):
- gocritic filepathJoin: build "/Applications/<bundle>" as a literal instead of
  filepath.Join with a separator-bearing arg.
- gocritic httpNoBody: pass http.NoBody, not nil, to NewRequestWithContext.
- gosec G302: annotate the AppImage chmod 0755 with a nolint; an AppImage is a
  self-contained executable and must be executable.
- nilerr: annotate openApp's intentional (false, nil) on launch failure; the
  failure is reported via the bool, not as an error.
- unparam: resolveApp's error result was always nil; drop it and update callers.

Cross-OS tests (start_test.go):
- makeBundle created a directory, which only stats as usable on macOS. Make it a
  regular file on Windows/Linux so the marker/scan resolve tests pass there,
  matching isUsableBundle's per-OS rule.

Container fresh-install smoke (test/cli/install-check.sh, Dockerfile):
- ao start is now the desktop-app launcher and no longer runs a daemon, so the
  old daemon status/shutdown/stop assertions could never pass. Assert instead
  that on a fresh box start reaches the fetch path and exits non-zero with a
  clear error (404 download on amd64, unsupported-arch on arm64). Refresh the
  stale daemon-reaping comments in the Dockerfile.

Verified locally: go build/vet ok, golangci-lint v2.12.2 reports 0 issues,
go test -tags e2e ./internal/cli/... passes (the only remaining failure,
TestE2E_Lifecycle, is a pre-existing daemon-shutdown flake that fails the same
way on upstream/main on this host), and the container smoke passes on both
linux/arm64 and linux/amd64.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 20:50:30 +05:30
Harshit Singh Bhandari 514946fd8b
feat(cli): ao start fetches + opens the desktop app; deprecate npm as an app channel (Track A) (#2201)
* docs: grounded spec for ao start bootstrapper + npm deprecation

Real-codebase-verified implementation spec (Track A: launcher + app-state
marker + release asset wiring). Replaces the somthing.md draft's aspirational
assumptions with file:line ground truth: correct bundle name (Agent
Orchestrator.app), publish repo (aoagents/agent-orchestrator), draft-release
+ asset-rename gaps, unsigned-build reality, and the already-wired
update-electron-app updater. Includes a dependency-ordered task breakdown
for AO execution.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: reframe spec around Go ao start subcommand (not a JS launcher)

The npm package ships the existing Go cobra CLI (backend/cmd/ao); this effort
rewrites the `ao start` subcommand to fetch+open the desktop app. Corrections:
- releases land on AgentWrapper/agent-orchestrator (aoagents was the temporary
  rewrite home; forge publisher must be repointed)
- ao start stops starting the daemon; the frontend owns the daemon
- adds the real Go CLI command surface (1.7), the npm-delivery gap for the Go
  binary (1.6), and the legacy first-boot import decision (6.4)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: separate prod vs test release targets in ao start spec

Dev/test loop must never cut a production release. Download repo + forge
publisher + npm scope are now build-time overridable:
- prod: AgentWrapper/agent-orchestrator + real package name
- test: harshitsinghbhandari/agent-orchestrator (fork) + @theharshitsingh/ao
T3/T5 now release+test against the fork and the test scope, with prod cut as a
separate gated step.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(cli): rewrite ao start to fetch + open the desktop app (T1)

ao start no longer starts the daemon (the desktop app owns it). It now
resolves the installed app (~/.ao/app-state.json marker -> stat -> known-
location scan), fetches the latest release zip and ditto-unpacks it on macOS
when absent, opens it with --installed-via=npm-bootstrap, and prints an honest
deprecation notice. releaseRepo is build-time overridable (-ldflags) so test
builds fetch from the fork. Windows/Linux fetch/open are stubbed for T6/T7.

Review fixes folded in: download() copies deps.HTTPClient and drops its 2s
loopback-probe timeout (a real release asset is hundreds of MB), and fetchApp
clears ~/.ao/staging before unpacking. Regression test covers the timeout.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(desktop): write ~/.ao/app-state.json marker on launch (T4)

The app is the sole writer of the marker ao start reads to locate the bundle.
New frontend/src/main/app-state.ts does an atomic temp+rename write mirroring
the daemon's runfile.Write, preserving installedAt/installSource across launches
and refreshing appPath/version/lastReconciledAt. main.ts hooks it into
app.whenReady ordered relocate (macOS) -> write marker -> createWindow, both
non-fatal. Bundle path is derived from process.execPath (not app.getAppPath,
which is the asar path); JSON keys match start.go's appState reader exactly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* ci(desktop-release): overridable repo, published release, stable asset aliases (T3)

Make the bootstrapper's constant releases/latest/download URL resolve:
- forge publisher repo reads AO_RELEASE_REPO (default AgentWrapper/agent-
  orchestrator); the workflow sets it to github.repository so a fork run
  publishes to the fork and never to prod
- draft:false so the release is immediately live (constant URL needs it)
- add ubuntu-latest to the matrix (issue #2191)
- post-publish steps upload stable space-free aliases
  (agent-orchestrator-darwin-arm64.zip, agent-orchestrator-win32-x64.exe)
  matching exactly what start.go fetches

Review fix: the alias upload targeted GITHUB_REF_NAME (the git tag), but
publisher-github creates the release as v<package.json version>; retargeted to
that and relaxed the guard so workflow_dispatch also produces aliases.

Known gaps (documented inline): macOS x64 needs an Intel runner (macos-latest is
arm64-only); the Linux stable asset name awaits the deb/rpm-vs-AppImage decision.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore: format with prettier [skip ci]

* feat(npm): deliver Go ao binary as @aoagents/ao via per-platform packages (T2)

Ship the ao CLI to npm with ZERO install scripts using the esbuild model:
- packages/ao: pure-JS @aoagents/ao with a bin/ao.js shim that resolves and
  execs the matching @aoagents/ao-<platform>-<arch> optionalDependency
- four platform packages (darwin-arm64/x64, win32-x64, linux-x64), each os/cpu
  gated so npm installs only the host's; binary cross-compiled CGO-free
  (modernc.org/sqlite), gitignored, shipped via files
- build-binaries.sh cross-compiles all four; releaseRepo keeps its prod default

Not added as root workspaces on purpose: os/cpu-restricted members make root
`npm ci` fail EBADPLATFORM (CI's api-drift job runs it). Packages publish
standalone; the shim was verified in a published-like layout.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(npm): set @aoagents/ao packages to 0.10.0 (above existing 0.9.5)

The existing @aoagents/ao on npm is 0.9.5; the launcher must publish a higher
version so npm latest advances and existing users get the new fetch-and-open
binary on update. All five packages plus the four optionalDeps pins set to
0.10.0.

* chore: format with prettier [skip ci]

* fix(start): capture install provenance before macOS relocation

moveToApplicationsFolder() relaunches the app from /Applications without
forwarding the --installed-via arg, and code past a successful move never
runs in the staging instance. The post-move instance therefore wrote
installSource="unknown", and writeAppStateMarker's sticky logic then locked
it there, losing the npm-bootstrap provenance in the exact path it exists for.

Write the marker before relocation when --installed-via is present so the
source is persisted while the arg is still available; the post-move launch
preserves it (sticky installSource) while refreshing appPath to /Applications.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore: format with prettier [skip ci]

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-06-26 19:31:56 +05:30
Harshit Singh Bhandari 1983e97bfa
fix(daemon): stop orchestrator id-increment + OS-native daemon liveness link (#2185)
* fix(daemon): do not tear down live sessions on shutdown; adopt them on boot

Remove SaveAndTeardownAll from the graceful shutdown path. Live tmux/ConPTY
sessions survive daemon exit; Reconcile on the next boot adopts them via
reconcileLive, preserving session IDs and preventing the id-increment bug.
Add runShutdownSessionLifecycle as a testable seam and narrow the
sessionLifecycle interface to Reconcile+RestoreAll only.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(daemon): make shutdown-teardown regression test falsifiable

Re-add SaveAndTeardownAll to the sessionLifecycle interface and to
fakeSessionLifecycle so TestShutdown_DoesNotCallSaveAndTeardownAll is
genuinely falsifiable: the flag flips if runShutdownSessionLifecycle
ever calls sl.SaveAndTeardownAll, making the assertion meaningful.
Name the sl parameter (was discarded with _) so the seam is visible.

RED: with a temporary sl.SaveAndTeardownAll call, test fails.
GREEN: without it, test passes. go test ./internal/daemon/... -race: 23/23.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(daemon): guard shutdown-teardown at compile time via narrowed interface

Remove SaveAndTeardownAll from sessionLifecycle so daemon.Run physically
cannot call teardown on shutdown. Delete the no-op runShutdownSessionLifecycle
seam and its test. The narrowed interface is the guard: re-introducing teardown
requires a visible, reviewable interface change.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(session): make ensure-orchestrator idempotent so POST cannot mint a duplicate

When clean=false, SpawnOrchestrator now checks for an existing active orchestrator
and returns it directly instead of always calling Spawn. Adds RED/GREEN tests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(session): drop redundant NoCleanSkipsKills, covered by SpawnsWhenNoneExists

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(daemon): supervisor watchdog

Adds backend/internal/daemon/supervisor: a transport-agnostic watchdog
that fires onLastClientGone() exactly once when the live connection count
drops to zero and stays zero for a configurable grace period. Arms only
after the first accepted connection (headless safety: CLI ao start never
self-stops). Reconnect before grace elapses cancels the pending timer.
Mutex guards liveCount/armed/pendingTimer; sync.Once guards the callback.
Tests use net.Pipe + a fake listener (no OS sockets); all three behavior
contracts verified with -race: never fires pre-connect, fires once on
last disconnect, reconnect within grace cancels and re-arms correctly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(daemon): supervisor watchdog review fixes (data race, ErrClosed, leak, cleanup)

- Fix data race: capture liveCount into local inside lock before logging
- Replace io.EOF with errors.Is(err, net.ErrClosed) for correct production behavior; update fakeListener to match
- Fix goroutine leak: derive cancellable child context in Serve so watcher always unblocks on return
- Simplify makePipe: drop dead error return, update 3 call sites
- Remove dead defensive pendingTimer nil-check in armGrace

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(daemon): OS-native supervisor listener triggers clean shutdown

Creates platform-split Listen() in supervisor package (Unix UDS sibling
of run-file; Windows named pipe via go-winio). Wires it into daemon.Run
before srv.Run: listener failure is non-fatal so headless ao-start works.
Adds RequestShutdown() on Server and three unit tests for listen_unix.go.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(daemon): log supervisor Serve error instead of discarding it

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(desktop): supervisor link; daemon self-stops (clean) on frontend exit

Add connectSupervisor() in frontend/src/main/supervisor-link.ts: holds one
client connection to the daemon's OS-native supervisor socket (Unix domain
socket on macOS/Linux, named pipe on Windows). Retries with bounded backoff
until the daemon is up; reconnects automatically on drop.

In main.ts, connect the link from reportBoundPort (once per daemon ready
transition). Remove the quit-time daemon teardown: the before-quit handler
now only disposes the browser view. The process.on("exit") killDaemon call
is also removed. When Electron exits for any reason the OS closes the fd,
the daemon detects EOF, and self-stops after its 5s grace period. tmux and
ConPTY sessions survive and are adopted on the next boot.

killDaemon and stopDaemon are kept for the explicit user-stop path
(ipcMain.handle("daemon:stop", ...)).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(desktop): guard against daemon orphan when supervisor link is down; tidy test setup

Expose a live `connected` getter on SupervisorLinkHandle and add a
last-resort process.on("exit") kill that fires only when the link is
not actually connected (UDS never bound or addr was null), preventing
orphan daemons while keeping the OS-fd teardown path for the normal
case. Log a warning when addr is null so the skip is diagnosable.
Invert the setup.ts guard to the natural form, dropping the empty
if/else skeleton.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(core): restore promptless sessions in place (reboot recovery, no increment)

Drop the empty-prompt early return in restoreArgv that returned ErrNotResumable
when the adapter could not resume and no prompt was saved. Now control falls
through to GetLaunchCommand unconditionally: a saved prompt is replayed, an
empty prompt (orchestrator) launches fresh with the system prompt only, same id,
same workspace. Removes the only producer of ErrNotResumable and deletes the
now-dead sentinel, its service mapping, and the corresponding tests.

Frontend reference to SESSION_NOT_RESUMABLE in TerminalPane.tsx is left intact
(the handler becomes harmlessly dead; the API simply stops returning that code).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(integration): dead-live session is restored, not abandoned, after reconcile

Task 5 made promptless sessions relaunch fresh instead of hitting ErrNotResumable.
The reconcile crash-recovery path (documented in reconcileLive) terminates a
dead-live session then RestoreAll relaunches it on the same boot. This test
asserted the old promptless-stays-terminated artifact; update it to the intended
restored end state (live again, same id, one fresh runtime Create).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* style(session): replace em dashes in service.go messages and comment

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(desktop): scope supervisor link to spawn path; dispose link on explicit stop

Document that the liveness link is established only when the app spawned the
daemon: the attach path intentionally does not link, to keep an `ao start`
daemon persistent (headless safety). Also dispose the link on an explicit
daemon:stop so its reconnect loop does not retry a deliberately-stopped daemon.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore: format with prettier [skip ci]

* docs: add daemon-lifecycle adopt-on-shutdown implementation plan

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore: format with prettier [skip ci]

* fix(daemon): keep supervisor watchdog alive across transient Accept errors

Review of #2185: Serve previously returned on the first unexpected Accept error,
silently disabling the watchdog (the 'restart is caller's job' comment described
a contract the caller never fulfilled). Back off and keep accepting instead, so
a transient error (e.g. EMFILE) cannot leave the daemon unable to self-stop on
frontend death. Also drop the stale Task 3/4 planning comment.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(core): leave promptless workers terminated on restore (orchestrators still relaunch fresh)

A promptless, unresumable KindWorker session had no prompt to replay and
no native session id to resume from. Blank-relaunching it via GetLaunchCommand
would silently drop its task. restoreArgv now returns ErrNotResumable for this
case, gated on (meta.Prompt == "" && kind != domain.KindOrchestrator). Orchestrators
are promptless by design and continue to relaunch fresh with the system prompt only.
Re-introduces ErrNotResumable sentinel and its Conflict API mapping.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(desktop): re-link supervisor on attach for app-owned daemons (close lingering-daemon gap)

- runfile.Info gets Owner field (omitempty): "app" when Electron spawned,
  empty for headless `ao start`. server.go reads AO_OWNER env to set it.
- daemonEnv() injects AO_OWNER=app so spawned daemons self-identify.
- Extracted establishSupervisorLink() from inline reportBoundPort code.
  Spawn path calls it unconditionally; attach paths call it only when
  shouldLinkOnAttach(owner) is true (owner === "app").
- Both attach paths (inspectExistingDaemon, resolveDaemonFromPort) now
  read the owner from the run-file and re-link when app-owned, preventing
  a lingering app-spawned daemon from self-stopping mid-session.
- Headless ao start daemons stay unlinked: persistent across app quit.
- New daemon-owner.ts + daemon-owner.test.ts (4 vitest cases, all pass).
- Go: 9 tests pass with -race; vet clean.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: quiet expected ErrNotResumable log in RestoreAll; note attach TOCTOU

Review polish on #2185: a promptless worker left terminated on boot is expected,
not an operational error, so log it at Warn not Error. Document the narrow
run-file re-read TOCTOU on the port-attach re-link path (worst case is linking a
headless daemon; the dispose-idempotency guard prevents any leak).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* style: gofmt the review-fix files (CI format/lint check)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-06-26 18:28:32 +05:30
Priyanshu Choudhary 8bbc4c94fe
fix: hide backend subprocess windows on Windows (#2179) 2026-06-25 16:21:18 +05:30
Harshit Singh Bhandari a96143b502
Zellij to tmux + ConPTY runtime, session save/restore, crash-proof reconcile (port #404) (#2183)
* Zellij to Tmux and some other fixes. (#404)

* feat(runtime): add tmux adapter package

Adds backend/internal/adapters/runtime/tmux implementing ports.Runtime via
the tmux CLI. Drop-in replacement for the zellij adapter on Darwin/Linux.

Key design points:
- Handle is a plain session id string (no pane-id split needed for tmux).
- Exact-match session targeting via = prefix for kill-session and has-session.
- Keep-alive shell appended to launch command so sessions survive agent exit.
- send-keys -l chunked for literal text delivery (no key-name interpretation).
- IsAlive distinguishes definitive-dead (missing/no-server output) from probe
  errors so the reaper never kills a session on a transient tmux failure.
- 34 tests pass: 32 unit tests via fakeRunner seam, 2 integration tests on
  real tmux 3.6b (TestRuntimeIntegration, TestRuntimeIntegrationExactSessionParsing).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(tmux): address four code-review findings in tmux runtime adapter

- Remove em dash from tmux_test.go:462 (project hard rule); replace with semicolon
- Derive integration test session IDs from t.Name() so concurrent runs do not collide on the same tmux session
- Remove dead scaffolding variables (r/fr, r2/fr2) in TestCreateDestroysAndReturnsErrorWhenNotAlive
- Quote \${SHELL:-/bin/sh} in buildLaunchCommand and update all asserting tests

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(runtime): wire tmux on Darwin/Linux via runtimeselect, keep zellij on Windows

- New package runtimeselect: Runtime union interface (ports.Runtime +
  SendMessage/GetOutput/AttachCommand) with compile-time assertions for
  both adapters. New(log) returns tmux on non-Windows, zellij on Windows
  (replicating the old daemon socket-dir setup).
- daemon.go: replace zellij-specific socket-dir block with
  runtimeselect.New(log); update comment to be runtime-neutral.
- lifecycle_wiring.go: startSession param changed from *zellij.Runtime
  to runtimeselect.Runtime.
- cli/doctor.go: runtime-aware checkTerminalRuntime (tmux on Darwin/Linux,
  zellij on Windows); added checkTmux.
- cli/spawn.go: attach hint prints tmux attach -t <name> on non-Windows,
  keeps zellij attach hint on Windows.
- wiring_test.go: startSession test uses runtimeselect.New(nil); zellij
  direct tests retained for zellij-specific coverage.
- doctor_test.go: replaced three zellij tool tests with tmux equivalents.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore: tidy runtime-neutral comments and doctor import grouping

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(tmux): drop unused runner.Start seam

tmux creates sessions detached via new-session -d, so the Start method
(carried over from the zellij runner shape, where it backs the Windows
fire-and-forget spawn) is never called. Remove it from the interface and
its implementations to shrink the seam.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(conpty): add protocol codec and output ring buffer (pure Go, OS-agnostic)

Ports the ConPTY named-pipe binary framing protocol and rolling output
buffer from pty-host.ts to Go. Implements EncodeMessage, MessageParser
(handles arbitrary chunk boundaries, payload copy guarantee), and Ring
(MaxOutputLines=1000, ANSI-safe, concurrent Append+Snapshot). All 15
unit tests pass on Darwin; GOOS=windows build is also clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(conpty): harden copy-safety and add concurrent ring test

Strengthen TestParserPayloadIsCopy to catch internal-buffer aliasing:
feed frame1, capture its payload, feed frame2 of the same length so the
parser's buffer overwrites the frame1 region, then assert frame1's bytes
are unchanged. The prior test only mutated the input slice post-Feed and
did not exercise the real aliasing risk.

Add TestRingConcurrent: 10 writer goroutines (Append) and 10 reader
goroutines (Snapshot + Tail) running concurrently with a WaitGroup. The
test is meaningful only under the race detector and catches any missing
mu coverage on Ring's exported methods.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(ptyregistry): port Windows pty-host sideband registry to Go

Adds package ptyregistry under backend/internal/adapters/runtime/conpty/ptyregistry.
Ports windows-pty-registry.ts: defensive read, atomic temp+rename write,
delete-on-empty, register-replaces-same-ID, and auto-pruning List.
PID liveness isolated behind build tags (syscall.Kill on Unix,
OpenProcess on Windows). 10 tests all green on Darwin.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(sdd): phase B briefs and progress for B1-B3

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(conpty): add pty-host serve engine with loopback TCP transport (B3)

Ports pty-host.ts behavior to Go: ptyConn interface seam, Serve engine
with ring replay, fan-out broadcast, MSG_* handlers, PTY-exit keep-alive,
and graceful shutdown (ConPTY dispose first, 50ms grace, then clients and
listener). Real conptyConn is Windows-only via build tag; non-Windows stub
keeps the package importable on Darwin/Linux. Tests use a fake ptyConn
with real loopback sockets and the B1 MessageParser, passing with -race.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(conpty): deliver scrollback snapshot and register client atomically

Review of Task B3 found one Important bug and two minors.

Important: in handleConn the ring Snapshot and the client registration
ran under two separate h.mu acquisitions. A PTY chunk arriving in that
gap was in neither the snapshot nor that client's broadcast, so it was
silently dropped (a hole in the client's stream). Now take the snapshot,
write it to the conn, and add the conn to the clients set all under a
single h.mu hold; broadcast also takes h.mu so it cannot interleave.
Added TestScrollbackLiveOrdering_NoDrop, which emits a contiguous
numbered stream while a client connects and asserts the client's stream
has no internal gap. It reliably fails against the old two-step code and
passes under -race -count=20.

Minor (faithfulness): conptyConn.Close() now also best-effort
Process.Kill() (nil-guarded) so a child that ignores ConPTY EOF still
exits and Done() fires, mirroring pty.kill() in pty-host.ts.

Minor (simplify): use os.Environ() instead of
exec.Command(shellCmd).Environ() for the child env.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(sdd): B4 brief

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(conpty): add runtime adapter with loopback pty-client and session management (B4)

Implements the conpty Runtime adapter: injectable spawn seam, loopback
TCP client helpers (SendMessage/GetOutput/IsAlive/Kill), and Runtime
methods (Create/Destroy/IsAlive/SendMessage/GetOutput). Session resolution
uses an in-memory map with B2 registry fallback for daemon-restart
recovery. Windows-only detached spawn in spawn_windows.go; stub errors
on other OSes. All adapter methods are unit-tested on Darwin against an
in-process B3 Serve and fakePTY. 48 tests pass, all three GOOS builds
succeed, vet clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(conpty): split IsAlive dead-vs-transient for reaper safety

clientIsAlive collapsed every probe failure (dial timeout, read-deadline
expiry, write error, connection-refused) to false, which the reaper turns
into ProbeDead and the LCM can promote to a permanent reap. A single
transient 2s loopback timeout would spuriously kill a live idle session.

Now clientIsAlive returns (alive bool, transientErr error): a refused dial
is definitively gone (false, nil); a timeout or any connected-then-failed
I/O error is transient (false, err) so the reaper records ProbeFailed and
retries. Wire IsAlive to propagate it. Add regression test covering both
the refused-is-gone and timeout-is-transient paths.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(sdd): B5 brief + ledger

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(terminal): stream-based Attach for tmux/zellij/conpty

Evolve the terminal layer from argv-based attach (PTYSource.AttachCommand
+ injected spawnFunc) to stream-based attach (Source embedding
ports.Attacher). tmux/zellij keep spawning their attach CLI on a local
PTY via the new shared ptyexec.Spawn; conpty attaches by dialing its
loopback pty-host directly with a loopbackStream over the B1 framing
protocol. Reattach/backoff/size/SIGWINCH/detach semantics are unchanged.

- ports: add Stream + Attacher.
- ptyexec: new shared package holding the creack/pty (unix) and ConPTY
  (windows) spawn, moved verbatim from terminal with its tests.
- terminal: PTYSource -> Source, drop spawnFunc/WithSpawn, run loop calls
  src.Attach and uses ports.Stream.
- tmux/zellij: add Attach (argv via ptyexec.Spawn); conpty: add Attach
  (loopbackStream); ports.Attacher assertions on all three.
- runtimeselect: union embeds ports.Attacher in place of AttachCommand.
- tests migrated; new conpty attach_test against in-process Serve+fakePTY.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* style(ptyexec): replace em dashes carried from moved pty files

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(sdd): B6 brief + B5 ledger

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(runtime): select conpty on Windows, register pty-host subcommand, delete zellij

- runtimeselect.New: Windows branch now returns conpty.New(conpty.Options{}) instead
  of zellij; compile-time assertion updated to conpty.Runtime.
- cli/ptyhost.go: new hidden "ao pty-host" subcommand (DisableFlagParsing so agent
  shell args with leading dashes survive); calls conpty.RunHost and exits with its code.
- cli/root.go: wires newPtyHostCommand alongside newLaunchCommand.
- cli/doctor.go: Windows terminal-runtime check replaced with a static ConPTY
  built-in pass; zellij import and checkZellij function removed.
- cli/spawn.go: Windows attach hint updated to dashboard message (ConPTY has no
  CLI attach); zellij import removed.
- daemon/lifecycle_wiring.go: stale zellij comment updated to tmux/conpty.
- daemon/wiring_test.go: zellij import and TestDaemonZellijSocketDir test removed;
  TestWiring_StartLifecycleThreadsMessengerIntoLCM now uses tmux.New.
- terminal/attachment_integration_test.go: re-pointed at real tmux
  (TestAttachmentStreamsRealTmuxPane + TestAttachmentReattachAdoptsNewSize);
  sessions cleaned up in t.Cleanup.
- internal/adapters/runtime/zellij: deleted entirely.

All three GOOS builds pass; go test -race ./... 1607 passed; go vet clean;
grep -rn "runtime/zellij" returns nothing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(daemon): correct terminal-runtime comment to conpty on Windows

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(ptyexec): drop stale zellij reference in Windows spawn comment

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(sdd): final phase B ledger

* build(desktop): support local keychain signing for macOS builds

Bridge forge.config.ts to accept the local keychain flow (APPLE_SIGNING_IDENTITY
identity + AO_NOTARY_PROFILE notarytool profile) in addition to the existing CI
secrets path (CSC_LINK + APPLE_ID/app-specific-password). Enables a signed +
notarized macOS build from a developer Mac without exporting a .p12 or the Apple
ID app-specific password.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(daemon): default TERM so Finder-launched tmux attach works

A Finder/Dock launch starts the supervisor under launchd with no
controlling tty, so TERM is unset. The daemon inherits that, and its
tmux attach client (spawned with env=nil, inheriting the daemon env)
dies immediately with "open terminal failed: terminal does not support
clear" — the orchestrator terminal pane never opens.

Seed TERM=xterm-256color (what the renderer's xterm.js emulates) as the
base of buildDaemonEnv, the same place PATH is reconstructed for the same
class of "Finder launch lacks a terminal's env" bug. A real TERM from the
shell/process env still wins.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(lifecycle): plan for save-on-close/restore-on-open sessions

Captures the intended daemon lifecycle: on shutdown save every running
session (worker and orchestrator) plus its gitignore-respecting uncommitted
work to refs/ao/preserved/<id>, then force-remove worktrees; on boot recreate
worktrees, replay the preserved work, and restore all sessions. Reuses
existing SQLite state, session_worktrees.preserved_ref, manager.Restore, and
the /shutdown endpoint (no new file, migration, or route).

Also gitignore the built daemon binary copied into frontend/daemon/.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(frontend): sync regenerated pnpm-lock and routeTree

Working-tree regeneration of the pnpm lockfile and TanStack Router generated
route tree. No hand edits; generated output only.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(workspace): add ForceDestroy for shutdown-path worktree removal

Adds ForceDestroy(ctx, info) to ports.Workspace and the gitworktree
adapter. It runs `git worktree remove --force`, then prune, then
os.RemoveAll as a backstop. A new worktreeForceRemoveArgs builder in
commands.go emits --force; the existing worktreeRemoveArgs is untouched
so Destroy still refuses dirty worktrees via ErrWorkspaceDirty.

TDD: test first creates a dirty worktree, confirms Destroy refuses with
ErrWorkspaceDirty, then confirms ForceDestroy succeeds and the path is
gone and deregistered. All 1609 backend tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(workspace): add StashUncommitted and ApplyPreserved for session lifecycle

Implements the correctness-critical save-on-close / restore-on-open pair
in the gitworktree adapter:

- StashUncommitted: captures uncommitted work (tracked edits and new
  non-ignored files) via a temp GIT_INDEX_FILE into a real commit stored
  at refs/ao/preserved/<session-id>. Never touches the real index or
  stash stack. Returns empty string for clean worktrees. Logs the count
  of .gitignore-skipped paths.
- ApplyPreserved: replays the preserve commit onto a freshly re-added
  worktree via "git checkout <SHA> -- .". Deletes the ref on clean
  success; keeps it and returns ErrPreservedConflict (wrapped) on
  content conflicts.
- Adds both methods to ports.Workspace interface and stubs them in
  integration and session_manager test doubles.

TDD: wrote two failing tests first (RED confirmed via build failure on
undefined methods), then implemented to GREEN. All 39 adapter tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(workspace): replace path-checkout with cherry-pick in ApplyPreserved

git checkout <sha> -- . is a path-checkout that always exits 0 for
content divergence, making ErrPreservedConflict unreachable. Replace
with git cherry-pick --no-commit which performs a true three-way merge,
leaves textual conflict markers on conflict, and exits non-zero so the
sentinel is correctly returned. Conflict detection now uses exit code
only (locale-independent). Add TestWorkspaceIntegrationApplyPreservedConflict
to assert: error is ErrPreservedConflict, preserve ref is kept, conflict
markers appear in the file. All 40 tests pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(session-manager): add SaveAndTeardownAll and RestoreAll for shutdown lifecycle

Implements Task 3: capture-then-destroy on shutdown and restore-all on startup.

- Adds ErrPreservedConflict to ports as a named sentinel; gitworktree aliases it
  (following the same pattern as ErrBranchCheckedOutElsewhere).
- Extends the Store interface with UpsertSessionWorktree and ListSessionWorktrees
  so the session manager can write the shutdown-saved marker and read it back.
- SaveAndTeardownAll: for every live session with a workspace path, stash
  uncommitted work, write the session_worktrees row (DB commit before worktree
  removal, crash-safety invariant), mark terminated, destroy runtime, force-remove
  the worktree. Best-effort per session; no kind filter.
- RestoreAll: for every terminated session that has a session_worktrees row (the
  marker written by SaveAndTeardownAll), re-create the worktree, apply any
  preserved ref (conflict logs and continues), then relaunch via the existing
  single-session Restore. Sessions killed by the user before shutdown (no row)
  are skipped. Best-effort per session; no kind filter.
- TDD: 9 new tests (RED confirmed via build failure, GREEN confirmed 63 pass).
  Full suite: 1621 tests across 77 packages.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(terminal): enable tmux mouse scroll and fix link clicking

On macOS the runtime is tmux, but two mouse interactions were broken in
the embedded terminal while copy/paste kept working:

- Scroll: the renderer drives scrolling by writing SGR mouse-wheel
  reports into the pane (the zellij `--mouse-mode true` model), but tmux
  ignores those reports unless mouse mode is on. Create only set `status
  off`, never `mouse on`, so wheel scrolling silently no-opped. Enable
  `set-option -t <id> mouse on`, mirroring the existing status-off step.

- Link clicking: the default WebLinksAddon handler calls window.open()
  with an empty URL and then assigns location.href. Electron's
  setWindowOpenHandler denies every window.open and only forwards the URL
  passed to it, so the empty open is dropped and clicks no-op. Pass the
  matched URL to window.open directly so the main process routes it to
  shell.openExternal (the OS browser).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(session-manager): assert UpsertSessionWorktree precedes ForceDestroy

Add a shared ordered call log (sharedLog *[]string) to both fakeStore
and fakeWorkspace. TestSaveAndTeardownAll_CaptureOrderAndMarker now
wires both fakes to the same slice and asserts upsertIdx < forceIdx,
enforcing the crash-safety invariant that the DB write is committed
before the worktree is force-destroyed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(daemon): wire RestoreAll/SaveAndTeardownAll into boot/shutdown sequence

Exposes session manager through a minimal sessionLifecycle interface
(RestoreAll, SaveAndTeardownAll) returned from startSession, then calls
RestoreAll (best-effort) before srv.Run and SaveAndTeardownAll with a
fresh 30s-bounded context after srv.Run returns. Both SIGTERM and POST
/shutdown funnel through srv.Run returning, so the single save call site
covers both paths.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(daemon): fix seam-test tautology and lifecycle variable shadow

Finding 1: dispatch both sessionLifecycle methods through an interface
variable (var sl sessionLifecycle = fake) so the runtime body exercises
interface dispatch, not just direct struct method calls.

Finding 2: rename local variable 'lifecycle' to 'lc' in
TestWiring_StartSessionBuildsSessionService to remove the shadow of the
imported lifecycle package.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(frontend): call POST /shutdown before killing daemon on quit

In before-quit, POST /shutdown (8s AbortSignal.timeout) so the daemon
saves sessions gracefully before the SIGTERM kill. Adds a re-entrancy
guard (quitting flag) so a concurrent app.quit() cannot double-preventDefault.
Falls back to killDaemon on fetch failure or timeout: quit is never blocked.
Keeps the process.on('exit') SIGTERM fallback intact.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(storage): guard session_worktrees.state against empty-string CHECK violation; add ponytail comments

The save path (saveAndTeardownOne) never sets domain.SessionWorktreeRecord.State,
so it arrives at UpsertSessionWorktree as "". The generated upsert includes state
in the INSERT column list, so the DB default ('active') is never applied and the
CHECK constraint (state IN ('active', ...)) would fire at the first real shutdown.

Fix: default to 'active' in the store adapter when row.State is "". No schema
change, no migration, no gen edit.

Also add ponytail: comments on the State field (domain type), the write path, and
the read path, documenting that state is unused multi-repo scaffolding and that the
upgrade path is to wire a real value when multi-repo worktree lifecycle states ship.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(storage): add real-SQLite test for empty-State guard in UpsertSessionWorktree

Adds TestUpsertSessionWorktreeEmptyStateDefaultsToActive to the store
test file. It inserts a SessionWorktreeRecord with State at zero value
"" via UpsertSessionWorktree against a real SQLite DB, then reads the
row back and asserts State == "active". This directly exercises the
guard added in the prior commit and would fail if the guard were
removed (the CHECK constraint rejects ""). Mirrors the helpers and
setup pattern of TestSessionWorktreesRoundTrip exactly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(comments): correct shutdown-mechanism and task-ref inaccuracies

Fix 1: daemon.go comment near SaveAndTeardownAll now correctly states
that POST /shutdown closes the shutdownRequested channel (not cancel ctx).
Also tighten the RestoreAll comment to remove the inaccurate claim.

Fix 2: remove "Task 2's" phrasing from ForceDestroy ponytail comment in
workspace.go; condition still references StashUncommitted by name.

Fix 3: add note in main.ts that the 8s fetch timeout is shorter than the
daemon's 30s save bound, so a SIGTERM after fetch abort does not cut the
in-flight save short.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore: remove .superpowers workflow scratch from repo

These SDD workflow artifacts (task briefs, agent reports, progress ledger,
review packages) were committed by accident in prior work, against the
.superpowers/sdd/.gitignore intent. Remove them from the repo; they remain
local-only scratch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(spec): graceful restore + post-failure orchestrator recreate

Fix the opaque 500 when restoring an un-resumable session (typed 409
SESSION_NOT_RESUMABLE), and add a post-failure popup that offers to recreate a
fresh orchestrator on the same branch (cleaning the worktree, preserving
committed history). Orchestrators only; recreate fires only after a restore
attempt confirms the session cannot be resumed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(plan): restore-recreate orchestrator; reuse existing /orchestrators clean=true

Planning discovery: the recreate capability already ships via POST /orchestrators
(clean=true), which kills the dead orchestrator and re-spawns on the canonical
branch (addWorktree reattaches an existing branch). So the feature collapses to a
typed-error fix plus a frontend popup. Spec updated to match.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(session): return typed SESSION_NOT_RESUMABLE instead of 500 on un-resumable restore

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(renderer): offer recreate-orchestrator popup when a session cannot be restored

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(spec): drop stale OpenAPI-regen note (feature adds no route)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(ci): gofmt/goimports, golangci-lint hygiene, and Windows-aware doctor tests

Formatting: ran gofmt and goimports (with local-prefixes) on the 8 listed
files plus ptyexec/spawn_unix.go which the linter also flagged.

Lint (25 issues fixed):
- gosec G115: EncodeMessage now returns ([]byte, error) with an explicit
  bounds check before the int->uint32 conversion; all callers updated.
- govet nilness: removed dead `if lastErr == nil` branch in clientIsAlive;
  lastErr is provably non-nil at that point (real bug).
- nilerr: extracted runAcceptLoop helper so Accept-error-on-close is not
  flagged; listener close is normal shutdown, not a caller error.
- staticcheck SA4010: removed dead `full = append(...)` loop in host_test.
- revive var-declaration: `var prev int = -1` -> `prev := -1`.
- revive redefines-builtin-id: deleted local `min` helper; builtin covers it.
- unparam (2): dropped always-nil env return from attachCommand; dropped
  unused shellPath param from buildLaunchCommand; updated callers.
- errcheck (8): deferred Close/Remove calls wrapped in func(){_ = ...}();
  type assertion in host_main.go uses ok-form; fmt.Fprintf to stdout uses
  _, _ = pattern; workspace.go tmpIdx.Close() uses _ =.
- gocritic nestingReduce: inverted if+continue in runtime.go resolve loop.

Windows E2E: skip TestDoctorChecksTmuxVersion,
TestDoctorChecksTmuxVersionFailsOnError, TestDoctorWarnsWhenTmuxMissing on
windows (ao doctor emits a conpty check there, not tmux).

Verified: gofmt -l . clean, golangci-lint 0 issues, go build ok,
go test -race 1624/1624 pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(ci): set git identity in worktree clone fixture; loosen tmux reattach timeouts

The preserve round-trip/conflict tests commit inside a worktree of the cloned
repo, which had no git identity; CI runners cannot auto-derive one, failing with
"empty ident name". Set user.email/user.name on the clone in setupOriginClone so
its worktrees inherit it.

The tmux reattach test drives a real shell and parses stty output, which is slow
under -race on CI; raise its echo-write and SIZE-output waits.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(terminal): resend size probe on tmux reattach until the shell answers

Bumping timeouts was the wrong fix: a 30s wait still failed, so the probe output
deterministically never appeared, not slowness. onOpen signals the stream accepts
input, not that the reattached sh -i is at a prompt, so the first echo keystroke
can be dropped. Resend the probe each poll until SIZE output lands, and on timeout
dump the captured pane buffer so a remaining failure is self-explaining.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(terminal): set TERM for real-tmux attach tests so they run in CI

Root cause (from the buffer dump the prior commit added): with TERM unset on CI
runners, tmux refuses to attach a client and prints "open terminal failed:
terminal does not support clear", so the pane never runs the size probe. The
daemon defaults TERM in production; the tests bypass it. Set TERM=xterm-256color
in both real-tmux tests. Reproduced locally with `env -u TERM` (fails the same
way) and verified the fix passes under it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(spec): crash-proof session reconcile design

Boot-time reconcile makes live tmux + worktree state match the DB on every
daemon start, so a SIGKILL/crash/force-quit that skips SaveAndTeardownAll no
longer leaks an orphaned daemon, tmux sessions, or worktrees. Adopt
crash-surviving tmux sessions, preserve-and-terminate dead ones, reap
in-namespace orphans, and add a frontend kill+replace branch for a wedged
orphan daemon.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(spec): simplify reconcile to per-session IsAlive, drop ListSessions

Every leak in the incident maps to a DB row, so orphan-reap is a per-session
IsAlive+Destroy over terminated rows; no runtime enumeration, no ports/conpty/
runtimeselect changes. Reaping a tmux session with no DB row is deferred (YAGNI).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(plan): crash-proof session reconcile implementation plan

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(session): reconcile live pass (adopt alive, stash+terminate dead)

* feat(session): reconcile reap pass and Reconcile entry point

* feat(daemon): run Reconcile on boot in place of bare RestoreAll

* test(integration): reconcile terminates dead-live sessions and reaps leaked tmux

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(integration): correct misleading CreateSession comment in reconcile test

* feat(frontend): kill+replace a wedged orphan daemon on launch

When both inspectExistingDaemon and resolveDaemonFromPort return null but
a process still holds the daemon port (a crashed/orphaned daemon), spawning
a new Go child would collide on the port and exit 1. Detect this case, SIGTERM
the holder (via the run-file PID, falling back to the probe PID), poll until the
port is free (up to 8s), clear the stale run-file, then proceed to spawn fresh.
The healthy-daemon reuse path is unchanged.

Pure helper: src/shared/daemon-takeover.ts (planDaemonTakeover)
Unit tests:  src/shared/daemon-takeover.test.ts (3 tests, TDD red-green)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(frontend): fire orphan-daemon takeover when a holder actually exists

Replace planDaemonTakeover (inverted logic: ran kill block only when probe
was null) with shouldReplacePortHolder(probe, holderPidAlive) which returns
true when a real holder exists: non-null probe (rejected responder) OR a
run-file PID that is still alive (hung holder). Update main.ts call site to
compute PID liveness before gating the kill block. Update tests to cover all
three distinct outcomes non-vacuously.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* docs+test: accurate takeover comments, reconcileLive probe-error test, Reconcile doc

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(session): restore promptless orchestrators and crash-orphaned sessions

The orchestrator was abandoned on every app open: a fresh orchestrator
spawned each launch and the prior conversation appeared lost (it was not;
the transcript stays in ~/.claude, resumable by the deterministic
--session-id AO pins). Two defects combined:

1. Restore's guard rejected any session with no agentSessionId AND no
   prompt as ErrNotResumable. But Claude resumes via a deterministic
   session id regardless of those fields, so promptless orchestrators
   were perfectly resumable yet always rejected. Workers slipped through
   only because they carry a prompt. Move the resumability decision to the
   adapter: restoreArgv returns ErrNotResumable only when GetRestoreCommand
   reports it cannot resume AND there is no prompt to fresh-launch from.

2. reconcileLive marked a crash-orphaned (dead-runtime) session terminated
   without a restore marker, so RestoreAll skipped it and it stayed dead.
   It now saves-and-tears-down to the same end state a graceful shutdown
   produces (capture work, write the session_worktrees marker, terminate,
   remove the worktree), so RestoreAll relaunches it on the same boot,
   resuming history. Crash recovery now matches graceful restart. If work
   capture fails it terminates without a marker rather than risk losing
   un-preserved work.

Tests: promptless orchestrator restores via adapter resume; promptless
session with a non-resuming adapter still returns ErrNotResumable;
reconcileLive writes the marker + tears down the worktree. Full backend
suite green (1632), gofmt/vet clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
(cherry picked from commit 9ae05735d6f06ac989857534bae2766392772c71)

* chore: format with prettier [skip ci]

* docs: scrub stale zellij references after tmux/conpty migration (#409)

PR #404 migrated the runtime adapter from Zellij to tmux (Darwin/Linux)
plus conpty (Windows), selected via runtimeselect, but ~30 stale zellij
references lingered in comments and docs describing zellij as the current
runtime. This is a comments/docs-only cleanup with no behavioral change:
comments now say tmux (or tmux/conpty when both platforms are relevant),
terminal/doc.go and docs/backend-code-structure.md are rewritten to
reflect the tmux + conpty + runtimeselect attach model, and the daemon
environment, STATUS, stack, architecture, and CLI docs are updated.

Also gitignore the local .codegraph/ and .cursor/ tooling dirs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore: format with prettier [skip ci]

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-06-25 15:05:41 +05:30
Harshit Singh Bhandari 47e3dddff5
fix(desktop): stop console-window flashing on Windows (#399)
On Windows the daemon shells out to console-subsystem processes without
suppressing the console window. The reaper polls session liveness on a
timer (and the attach loop retries), each call runs a zellij command, and
every invocation pops a console window that instantly closes, so the user
sees rapid blinking.

- Add a platform-split hideWindow(cmd) helper to the zellij package
  (CREATE_NO_WINDOW + HideWindow on Windows, no-op elsewhere) and call it
  in execRunner.Run so list-sessions and friends stop flashing.
- startBackgroundProcess now uses CREATE_NO_WINDOW instead of
  CREATE_NEW_CONSOLE (the latter creates a console then hides it, flashing).
- Pass windowsHide: true to the daemon spawn in main.ts so the daemon's own
  console stays hidden on a Windows GUI launch.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 17:49:04 +05:30
i-trytoohard 82d69ce120
feat(cli): add examples to ao preview --help (#388)
* feat(cli): add examples to ao preview --help (#387)

Update the preview command help to include a concise Examples section
covering the four common workflows:

  ao preview
  ao preview file:///home/aoagent/ReverbCode/index.html
  ao preview http://localhost:5173
  ao preview clear

Replace the workspace-relative path (./dist/index.html) in the Long
description with the absolute file:// URL pattern agents actually use.

Command syntax and behavior are unchanged.

Closes #387

* fix(cli): clarify no-arg preview fallback behavior in help text

Address review feedback on #388: the no-argument description now
reflects the daemon's actual behavior — autodetect the workspace
entry point, fall back to the session's existing preview target
when none exists.

---------

Co-authored-by: AO Bot <ao-bot@composio.dev>
2026-06-23 01:28:06 +05:30
Vaibhaav Tiwari 3e3d22c6fa
fix: make preview browser deterministic (#384)
* feat(frontend): add live browser panel

* 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

* test: allow project settings form more time

* fix: make preview browser deterministic

* chore: format with prettier [skip ci]

* fix: preserve cleared preview state

---------

Co-authored-by: Vaibhaav <user@example.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-06-23 00:33:25 +05:30
Harshit Singh Bhandari ea54f31b24
fix(daemon): fall back to ephemeral port on conflict + harden teardown (#386)
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>
2026-06-22 22:05:14 +05:30
Harshit Singh Bhandari 8fa403c480
fix(preview): add clear, reuse defaults, force refresh, local files (#379) (#380)
* 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>
2026-06-22 16:04:29 +05:30
neversettle f85b0d2ffe
Refactoring Reviews feature code and idempotency check (#377)
* fix: dedupe AO review change nudges

* fix: satisfy review delivery lint

* fix: renumber review delivery migration

* fix: gate review trigger idempotency on verdict

* fix: preserve running review trigger idempotency

* fix: fail stale running review runs

* Update review.go

* fix: avoid review context import ambiguity

* docs: clarify defensive review idempotency branch

* fix: keep review submit persistence single-sourced

* fix: remove unused review nudge send result

* fix: preserve existing pr review nudge copy

* fix: simplify merge conflict nudge return
2026-06-22 15:40:00 +05:30
Vaibhaav Tiwari c6d9692d37
feat(frontend): add live browser panel (#375)
* 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>
2026-06-22 04:14:18 +05:30
Laxman 7ba860741c
fix: add terminal controls and restore copy/scroll (#372)
* 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>
2026-06-22 02:26:54 +05:30
Adil Shaikh 8146629ad6
feat: add desktop notifications v1 (#262)
* feat: add desktop notifications v1

* fix: remove duplicate notification icon

* fix: humanize notification type labels

* Revert "fix: humanize notification type labels"

This reverts commit b6ebe6913753c863d6a1f246954f6c66e61f08b5.
2026-06-22 01:14:04 +05:30
Adil Shaikh 6f8112e7b9
feat: surface SCM summaries in desktop (#263)
* feat: surface scm summaries in desktop

* chore: format with prettier [skip ci]

* fix: hydrate PR views from session facts

* chore: format with prettier [skip ci]

* fix: document PR summary DTOs

* fix: clean pr summary attention details

* chore: format with prettier [skip ci]

* fix: default codex sessions to bypass approvals

* fix: refine pr inspector summary

* chore: format with prettier [skip ci]

* fix: color pr diff metadata

* chore: format with prettier [skip ci]

* fix: move diff icon to file count

* fix: keep shell topbar on board routes

* fix: render all session prs on board

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-06-22 00:35:00 +05:30
neversettle 0e5891df91
fix(review): notify worker on changes_requested instead of relying on SCM poll (#337) (#340)
* 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>
2026-06-21 19:52:46 +05:30
Laxman 0e1c5fe54b
Feat/posthog error tracking redaction (#329)
* feat(telemetry): send renderer exceptions to PostHog error tracking

* fix(telemetry): redact local paths and urls before capture

* feat(telemetry): enrich backend error metadata

* fix(telemetry): document telemetry helper exports

* fix(telemetry): satisfy gocritic on helper signature

* chore: format with prettier [skip ci]

* fix(telemetry): redact embedded local urls

* chore: format with prettier [skip ci]

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-06-21 18:50:47 +05:30
swyam sharma 34a9fdb736
fix: surface session branch in frontend (#358)
Co-authored-by: Swyam Sharma <cruzer@Swyams-MacBook-Pro.local>
2026-06-21 14:57:37 +05:30
i-trytoohard 78cdc6d5ca
fix(session-manager): terminate sessions even when handle is missing (#360)
* fix(session-manager): terminate sessions even when handle is missing

Kill hard-failed with ErrIncompleteHandle before recording terminal
intent, so a session that lost its runtime/workspace handle (crash,
partial spawn) was un-killable and stuck forever on the dashboard.

MarkTerminated now runs unconditionally; each destroy step is gated
individually on whether that handle exists. If nothing is present to
tear down, the session still terminates — freed=false just signals
nothing was freed. ErrIncompleteHandle is still returned by Restore
and runtimeMessenger.Send, where a missing handle is genuinely fatal.

Updates TestKill_RefusesIncompleteHandle → TestKill_TerminatesIncompleteHandle.

* style: gofmt blank line between test functions

---------

Co-authored-by: AO Bot <ao-bot@composio.dev>
2026-06-21 14:30:00 +05:30
Vaibhaav Tiwari 708ec5db56
fix: recover terminal reattach after daemon idle (#354)
* 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>
2026-06-21 13:59:15 +05:30
Adil Shaikh 72b757b203
feat: require explicit project agents (#355)
* feat: require explicit project agents

* chore: format with prettier [skip ci]

* test: configure integration project agents

* fix: center project agent selection dialog

* fix: keep agent selects neutral before validation

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-06-21 06:07:53 +05:30
Adil Shaikh 5e8c8defbd
fix: keep terminal mux persistent across navigation (#325)
* fix: keep terminal mux persistent across navigation

* chore: format with prettier [skip ci]

* fix: require configured agent defaults

* chore: format with prettier [skip ci]

* fix: recreate stale orchestrator worktrees

* fix: stop terminal reattach on socket close

* fix: stop terminal attach loop on close

* fix: keep agent default selects controlled

* revert: undo persistent mux branch changes

* fix: harden terminal mux attachment flow

* chore: format with prettier [skip ci]

* chore: remove terminal flow diagnostics

* chore: format with prettier [skip ci]

* fix: satisfy zellij command lint

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-06-21 06:07:32 +05:30
i-trytoohard f6ca7c2662
fix(session_manager): append confidentiality guard to agent system prompts (#341)
The orchestrator/worker system prompts (role, coordination, branch
conventions) contained no instruction telling the agent to treat them as
private, so a plain "give me your system prompt" made Claude Code dump
the role block verbatim.

Add a systemPromptGuard appended to every non-empty system prompt via
buildSystemPrompt, covering both spawn and restore paths. The guard
covers direct, indirect, and embedded reveal requests while leaving
general project/workflow questions answerable.

Adds TestSystemPrompt_AppendsConfidentialityGuard across orchestrator
and both worker variants.

Co-authored-by: i-trytoohard <i-trytoohard@users.noreply.github.com>
2026-06-20 17:44:59 +05:30
neversettle c53c4af8bd
test(storage): guard against duplicate goose migration version prefixes (#336)
* test(storage): guard against duplicate goose migration version prefixes

Statically scans embedded migration filenames for repeated numeric
version prefixes and fails with a clear message, catching the class
of bug from #333 (two PRs adding 0014_*.sql independently) before
goose.Up() would panic at runtime.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(storage): dedupe migration versions by goose's parsed int64, not raw string

versionPrefix compared raw filename prefixes, so 014_x.sql and 0014_y.sql
were treated as distinct even though goose.NumericComponent parses both
to version 14 and panics on the collision. Use goose.NumericComponent
directly so the test enforces goose's actual collision rule.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test(storage): trim PR-specific framing from migration version test comment

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-20 12:49:59 +05:30
Khushi Diwan f98c5e56cc
fix(storage): renumber telemetry migration to 0015 to resolve 0014 version collision (#334)
* 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>
2026-06-20 11:23:13 +05:30