Commit Graph

1197 Commits

Author SHA1 Message Date
Priyanshu Choudhary 24eefda23b fix(core): restore must rewrite statePayload.runtime.handle, not just top-level
Restore was writing the freshly-spawned runtime handle to the top-level
metadata `runtimeHandle` key, but the canonical lifecycle parser prefers
`statePayload.runtime.handle` and falls back to the top-level only when
statePayload is missing. The next lifecycle tick read the stale handle
from statePayload and rewrote both keys from it, silently undoing the
restore's update.

Symptom: a session restored after AO restart kept the old PID in
metadata. Lifecycle probe found that PID dead (it was from a previous
boot) and the dashboard rendered the orchestrator as exited/killed even
though a new process was actually running.

Fix: rebuild the canonical lifecycle with the new handle via
buildUpdatedLifecycle() and persist via lifecycleMetadataUpdates() so
statePayload and runtimeHandle stay in sync. Mirrors the pattern used
in kill/spawn paths.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 11:35:25 +05:30
Priyanshu Choudhary d16d3c3f4d test(windows): isolate USERPROFILE per test, normalize path assertions
Before this, vitest worker isolation only overrode HOME, but on Windows
os.homedir() reads USERPROFILE. Tests that wrote to ~/.agent-orchestrator
ended up sharing the real user's data directory across workers, leading
to flaky cross-test pollution.

- test-utils: createTestEnvironment / setupTestContext now override both
  HOME and USERPROFILE to the per-test fake home, restore both on
  teardown. rmSync uses maxRetries:5/retryDelay:50 to ride out the same
  Windows file-lock window that atomic-write retries cover.
- core test files (global-config, plugin-integration, portfolio-*,
  project-resolver, recovery-actions, orchestrator-prompt*): set fake
  USERPROFILE alongside HOME and use the retry-aware rmSync.
- update-check.test: normalize path separators in assertions
  (path.replace(/\/g, "/")) so script-path matching works on Windows
  without forcing the production code to emit posix paths.
- orchestrator-prompt.dist.test: pass shell:true on Windows to execFileSync
  for .cmd targets, working around Node CVE-2024-27980's hardening.

Removed lifecycle-service.test.ts — it covered stopLifecycleWorker, which
was deleted when lifecycle was moved in-process during the merge with
main. The remaining lifecycle paths are exercised by lifecycle-manager
tests in core.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 11:25:34 +05:30
Priyanshu Choudhary 0dcbdb2673 fix(windows): plugin runtime gaps + ConPTY graceful shutdown
runtime-process:
- Reserve the per-instance processes-map slot before the platform split
  so the Windows ConPTY branch participates in duplicate-create detection
  and getMetrics/getAttachInfo bookkeeping. Previously, Windows returned
  a handle without storing it, so duplicate session IDs were silently
  accepted and getMetrics always reported 0 uptime.
- Add 500ms graceful-exit poll before SIGKILLing the pty-host on destroy
  so node-pty can dispose its ConPTY handle. Skipping this orphaned the
  conpty_console_list_agent helper and triggered Windows Error Reporting
  dialogs (0x800700e8) on real runs, not just tests.
- pty-host: install SIGTERM/SIGINT/SIGHUP/SIGBREAK/beforeExit handlers
  that drive the same shutdown sequence (kill pty, drop clients, close
  pipe, exit after 50ms grace), and route MSG_KILL_REQ through the same
  path. Previously MSG_KILL_REQ only called pty.kill() and left the host
  process lingering.
- Add windowsHide:true to the pty-host child spawn so node-pty's helper
  console window stays hidden on errors.

workspace-worktree: normalize paths to a comparable POSIX form
(backslash→slash, lowercase drive letter) when matching git worktree
list --porcelain output against project directories. git emits
forward-slash paths on Windows; path.join produces native backslashes
— the comparison failed and list() returned empty.

agent-opencode: guard tmux/ps usage with isWindows() in isProcessRunning
so process-runtime sessions on Windows take the PID-signal path instead
of attempting Unix-only commands.

cli/start: detect Windows local paths in isLocalPath (drive letter
prefix, UNC path, .\, ..\) so spawn arguments like C:\... aren't
mistaken for project names.

integration test: replace cat + /tmp with platform-native echo (findstr
"x*" on Windows, cat on Unix) and os.tmpdir(); the original used
Unix-only tooling and would never run on Windows.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 11:25:19 +05:30
Priyanshu Choudhary ad52b0a699 fix(core): Windows-stable storage hash and atomic write retry
storage-key: normalize to POSIX form (strip drive letter, replace backslash
with forward slash) before hashing so identical repos produce identical
hashes across Windows and Unix, and across different Windows working
directories of the same checkout.

atomic-write: retry renameSync up to 10x with 50ms backoff when Windows
returns EPERM/EACCES/EBUSY — antivirus, file indexer, or backup software
briefly hold handles to recently-written files. Cleans up the temp file
on final failure so subsequent retries don't trip "file exists".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 11:24:57 +05:30
Priyanshu Choudhary 2e9eaa4f8f Merge remote-tracking branch 'origin/main' into feat/windows-platform-adapter 2026-04-24 02:45:44 +05:30
Priyanshu Choudhary c76ea4389d Merge branch 'main' into feat/windows-platform-adapter 2026-04-24 02:45:37 +05:30
fastestdevalive 488b311048
fix(web): move orchestrator info to top bar + fix scroll-to-bottom button (#1348)
* fix(web): fix scroll-to-bottom button in terminal

Two bugs with the scroll-to-bottom button:

1. Button disappeared as soon as the user started swiping down, instead
   of staying visible until the live tail was actually reached.
   Root cause: the touch-scroll onScrollTowardLatest callback immediately
   set followOutput=true on the first downward swipe. Removed that
   callback — in normal buffer, terminal.onScroll decides based on real
   viewport position; in alternate buffer (tmux), the button stays
   visible until the user explicitly clicks it.

2. Clicking the button did nothing when running inside tmux.
   Root cause: the terminal runs in xterm's alternate buffer when tmux
   is active. terminal.scrollToBottom() has no effect in alt buffer
   (xterm has no scrollback there — the user is viewing tmux copy-mode
   scrollback). The button click now detects the buffer type: normal
   buffer uses terminal.scrollToBottom(); alternate buffer sends "q" to
   exit tmux copy-mode and return to the live tail.

   Additionally replaced the previous DOM viewport.scrollTop approach
   and DOM scroll event listener with terminal.scrollToBottom() and
   terminal.onScroll respectively — xterm v6 may update scrollTop via
   requestAnimationFrame, making raw DOM scroll events unreliable.

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

* fix(web): move orchestrator session info to top bar; show orchestrator button on mobile

Two session-detail page fixes for orchestrator sessions:

1. Orchestrator session info (status, branch, PR pills) now lives in the
   same top bar used by regular sessions, instead of a large stacked
   strip above the terminal. Adds an inline "orchestrator" badge and a
   compact row of per-zone agent-count pills (merge / respond / review /
   working / pending / done) next to the existing status + branch pills.
   Removes the now-unused OrchestratorTopStrip and _OrchestratorStatusStrip
   helpers (~270 lines removed).

2. The orchestrator navigation link button on worker session pages was
   hidden on mobile (≤640px) due to a topbar-desktop-only class. Removed
   the class so it appears on mobile as an icon-only button (the text
   label is already suppressed on mobile via topbar-btn-label CSS).

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

* fix(web): remove unused crumbHref/crumbLabel vars to fix lint

* fix(web): allow topbar zone pills to wrap; hide labels on mobile

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-23 17:06:53 +05:30
Ashish Huddar aa3342bb24
perf(core): cache sessionManager.list() to fix slow dashboard after prolonged running (#1113)
* perf(core): cache sessionManager.list() to prevent redundant I/O on every poll

sessionManager.list() performs O(n) synchronous disk reads and async subprocess
calls (runtime liveness checks, activity detection) per session on every
invocation. Multiple concurrent callers — SSE poll (5s), lifecycle manager (30s),
API refreshes, backlog poller — all trigger independent full-I/O list() calls.
As sessions accumulate, each call gets proportionally slower, causing the
dashboard to take increasingly long to load.

Add an in-memory cache with 2-second TTL and request coalescing:
- Cached results are returned within the TTL window (no I/O)
- Concurrent calls to list() share a single in-flight request
- Cache is invalidated on any mutation (spawn, kill, cleanup, restore, etc.)

This collapses redundant I/O when multiple callers poll within seconds of each
other, directly fixing the slow dashboard load after prolonged running.

Closes #1049

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(core): prevent stale in-flight list() results from caching after invalidation

Address review feedback: invalidateListCache() now clears the in-flight
promise map and increments a generation counter. When a list() call
completes, it only writes to cache if no invalidation happened during
the fetch. This prevents a race where kill() invalidates the cache
mid-flight but the stale promise's result overwrites it with a fresh
timestamp.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(core): add post-mutation cache invalidation and inflight promise ownership check

Address two review findings:

1. Post-mutation invalidation: invalidateListCache() was only called at
   the start of mutations. A concurrent list() running mid-mutation would
   read pre-mutation disk state and cache it with a fresh timestamp. Now
   invalidateListCache() is also called after mutations complete, clearing
   any stale cache entries populated during the mutation window.

2. Inflight promise ownership: the finally block unconditionally deleted
   the inflight cache entry by key. If invalidation replaced it with a
   new promise, the old finally would delete the replacement. Now it
   checks that the stored promise is still its own before deleting.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(web): skip PR enrichment for terminal sessions in /api/sessions

Prevent unnecessary GitHub API calls for dead sessions by filtering out
sessions in terminal states (killed, done, merged, terminated, cleanup)
from PR enrichment. This reduces API calls from ~108 to ~48 on the test
machine's configuration, directly addressing the slow dashboard load issue.

Motivation: PR enrichment was calling enrichSessionPR() for all sessions
with a PR field, including killed sessions. With 18 sessions having PRs
and 10 of them killed, this resulted in 60 wasted API calls per dashboard
load. The metadata enrichment already correctly skips terminal sessions —
this aligns the PR enrichment behavior to match.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(web): keep only terminal PR enrichment optimization

* fix(web): preserve terminal PR cache for #1049

* fix(web): refresh open PRs for #1049

Only treat merged and closed PRs as terminal for dashboard PR enrichment so killed runtimes with still-open PRs keep receiving live SCM updates. Fall back to a blocking refresh on cold-cache terminal PRs so merged and closed sessions can self-heal after cache expiry.

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-23 16:50:26 +05:30
i-trytoohard b208617e6e
chore: remove orphan root files and stale .gitignore entries (#1434)
- Delete test-ao-config.yaml and test-ao-config2.yaml (zero refs)
- Delete .gitignore-template (zero refs, purpose unclear)
- Remove .sisyphus and .gstack/ from .gitignore (tools no longer used)

Closes #1421

Co-authored-by: Prateek <karnalprateek@gmail.com>
2026-04-23 16:48:55 +05:30
suraj_markup a41d12b1f9
feat(skills): add ao-weekly-release skill for automated release notes (#1239)
* feat(skills): add ao-weekly-release skill for automated release notes (#1231)

Adds the in-repo skill files the bot cron invokes every Thursday 10:00 IST
to generate and post weekly release notes to Discord. Keeping the skill
under version control means merged PRs to main are picked up on the next
run with no manual redeployment.

- SKILL.md: output contract, style constraints, failure-mode table, and
  the update workflow so future editors know what the runner must guarantee
- run.py: deterministic stdlib-only runner that queries gh for the latest
  release, merged PRs, commits, contributors, and stars in a 7-day window
  and renders a publishable markdown post in the established house style

The runner never fabricates data — every rendered value traces back to a
gh or git command, and missing data surfaces as "(unavailable)" or a
non-zero exit code so the cron can post a "quiet week" message instead.

Refs #1231

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(skills): address review findings in release notes runner (#1231)

1. Install command: read version from npm registry instead of deriving
   from the GitHub release tag, which uses scoped npm tags like
   @composio/ao-cli@0.2.2 and produced malformed specifiers.

2. gh release create: same root cause — use npm version, not tag.

3. Full changelog link: resolve window dates to commit SHAs via
   git rev-list / GitHub API instead of main@{date} reflog syntax,
   which only works locally and 404s on GitHub.

4. Truncation count: track total omitted PRs across the initial
   MAX_HIGHLIGHTS cap and the body-size truncation pass, and render
   one accurate overflow line at the end.

5. Highlight bullets: skip the theme verb prefix when the cleaned
   title already starts with a verb (avoids "Added add …" stutter),
   and strip trailing (#NNN) refs from titles before appending the
   canonical (#{pr.number}) to avoid doubled refs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-23 01:11:31 +05:30
Harsh Batheja 616c56cea2
fix(cli): derive projectId from prefixed issue id on spawn (#1330)
* fix(cli): derive projectId from prefixed issue id on spawn

When `ao spawn <projectId>/<issue>` is used in a multi-project config,
route the spawn to the prefixed project and strip the prefix from the
issue id. Previously the projectId fell back to whichever project
`ao start` was running for, tagging cross-project sessions with the
wrong project (and session prefix).

Applies to both `ao spawn` and `ao batch-spawn`; batch-spawn errors
out if issues mix multiple project prefixes.

Fixes #1329

* refactor(core,cli): lift spawn target resolution into core

Move the issue-prefix → project routing from the CLI into a reusable
core utility, `resolveSpawnTarget(projects, issueRef, fallback?)`.

- Exposes routing to any spawn entry point (CLI, web API, programmatic).
- Accepts either a project id or sessionPrefix as the prefix; project id
  wins on collision.
- `ao batch-spawn` now groups issues by resolved project and preflights
  once per group instead of erroring on mixed prefixes.

Tests:
- 8 unit tests for `resolveSpawnTarget` in core.
- CLI spawn.test.ts: adds a sessionPrefix routing test (23 tests total).

Refs #1329

* test(cli): cover batch-spawn grouping; tighten resolveSpawnTarget API

Self-review found three gaps:

- `resolveSpawnTarget` accepted `undefined` issueRef and returned
  `{ issueId: "" }` with a fallback project. Dead path — both callers
  guard against undefined. Drop it and make issueRef required.
- No tests for `batch-spawn`. Added two:
  - routes cross-project prefixed issues to the correct project and
    lists each project's sessions separately
  - skips a prefixed issue when the target project already has an
    active session for it
- `spawn` and `batch-spawn` help text didn't document the
  `<projectId>/<issue>` or `<sessionPrefix>/<issue>` forms.

* fix(core): guard resolveSpawnTarget against prototype-key matches

Second review pass found a latent bug:

Plain JS objects inherit `__proto__`, `constructor`, `toString`,
`hasOwnProperty`, etc. from Object.prototype. The previous
`if (projects[prefix])` check entered the routing branch for any of
these keys, mis-routing a user typing `ao spawn __proto__/42` with
`projectId: "__proto__"`. Downstream session-manager would then receive
a junk project object (Object.prototype itself) and fail with a
non-obvious error.

Fix: use `Object.prototype.hasOwnProperty.call(projects, prefix)` so
only actual configured project ids match.

Also:
- Document the case-sensitive matching semantic explicitly.
- Lock in the batch-spawn grouping contract by asserting exact
  `list()` and `spawn()` call counts in the cross-project test.
2026-04-22 22:18:39 +05:30
Harsh Batheja 331f1ce568
fix(core): dispatch detailed bugbot review comments to agents (#1334)
* fix(core): dispatch detailed bugbot review comments to agents

The bugbot-comments reaction previously sent a generic "fix the issues"
message, leaving the agent to discover comments on its own. The typical
agent flow — fetch `GET /pulls/{pr}/comments` (first page only) and
filter for a bugbot marker — misses new comments when the response is
paginated, and trusts a stale `gh pr checks` status.

Now we format the already-fetched `AutomatedComment[]` into a detailed
message that lists each comment (severity, path:line, bot name, excerpt,
URL) and embeds explicit API-level guidance so the agent can verify via
`/reviews` + `/reviews/{id}/comments` + paginated `/pulls/{pr}/comments`
(checking `in_reply_to_id` for replies) instead of the naive first-page
scan.

Closes #895

* refactor(core): extract bugbot comment formatter, address review

Follow-up to #895 review:

- Extract formatAutomatedCommentsMessage to a module-level helper
  (packages/core/src/format-automated-comments.ts) so it is directly
  unit-testable rather than closure-scoped inside createLifecycleManager.
- Accept optional PRInfo and interpolate owner/repo/number into the
  verification guidance when dispatched from the lifecycle; fall back
  to OWNER/REPO/PR placeholders otherwise.
- Drop the dead `if (c.url)` guard (url is required on AutomatedComment).
- Append `…` when the first-line excerpt is truncated at 160 chars.
- Simplify the config.ts default message to a short fallback and note
  inline that the lifecycle dispatcher injects the rich listing.
- Add 11 focused unit tests for the formatter (interpolation, ellipsis,
  first-line extraction, missing path/line, severity rendering, ordering).

* fix(core): address review feedback on bugbot detail dispatch

Resolves the 6 comments from @illegalcall on #1334:

- H: Sentinel-gate the default-message override — export
  DEFAULT_BUGBOT_COMMENTS_MESSAGE from config.ts and only replace the
  reaction message when it matches the sentinel. User-customized
  messages in project YAML are left untouched.
- H: Add --paginate to step 2 of the verification guidance. Step 2 was
  previously missing --paginate, reintroducing the exact pagination
  failure mode #895 is meant to fix.
- H: Prompt-injection mitigation — strip backticks from comment body
  excerpts, wrap each excerpt in a code span so the content cannot
  break out, and add an "untrusted third-party data" preamble telling
  the agent not to treat excerpts as instructions.
- M: Preserve line number when c.line === 0 (file-level or 0-indexed
  tools). Was previously treated as falsy and dropped.
- M: Skip leading blank lines in excerpt extraction; strip leading
  markdown heading markers (### Title) and whole-line bold/italic
  wrappers before truncating.
- L: Correct the "reply resolves the thread" wording — replying alone
  does not resolve a review thread on GitHub; resolution is a separate
  "Resolve conversation" action.

Also adds a patch-level changeset for @aoagents/ao-core and expands
the test suite to 811 tests (was 803) covering sentinel override,
custom-message passthrough, line===0, backtick escaping, heading
stripping, and step-2 pagination regression.
2026-04-22 22:18:13 +05:30
i-trytoohard 84cd105c61
feat(web): enable tmux status bar in web terminal (#1470)
The tmux status bar was explicitly turned off when attaching a PTY
to a tmux session via the web terminal. This hid useful session
context (session name, window list, datetime).

Change status from 'off' to 'on' so users and agents can see the
tmux status bar in both CLI and web terminal views.

Closes #1469

Co-authored-by: AO Bot <ao-bot@composio.dev>
2026-04-22 21:58:52 +05:30
Ashish Huddar 6e59fddc71
feat(sessions): derive display names from task context (#1220) (#1224)
* feat(sessions): derive display names from task context (#1220)

Orchestrator and ad-hoc sessions used to render as "Orchestrator/Ao
Orchestrator 8" or "Ao 42" — humanized branch names that revealed
nothing about the work. getSessionTitle()'s fallback chain landed on
humanizeBranch() whenever no PR or enriched issue title was available,
and humanizeBranch() didn't know to strip the "orchestrator/" prefix
or recognise that "ao-orchestrator-8" is just the session ID.

Fix it at two levels:

Level 1 — humanizeBranch() cleanup
- Add "orchestrator" to the prefix-strip regex.
- Accept an optional sessionId argument; when the stripped branch is
  just the session ID (e.g. "orchestrator/ao-orchestrator-8" -> "ao-
  orchestrator-8"), return an empty string so getSessionTitle() can
  fall through to the next meaningful fallback instead of rendering
  noise.

Level 2 — persisted displayName derived at spawn time
- New optional SessionMetadata.displayName field, captured from the
  best available context when the session is spawned: issue title
  for tracker-backed sessions, the first line of a user prompt for
  prompt-only sessions, and the first line of the system prompt for
  orchestrators. Values are collapsed to a single line and truncated
  to 80 chars with an ellipsis.
- metadata.ts serialises and reads the new field.
- DashboardSession gains a displayName field; sessionToDashboard()
  pulls it from session.metadata.
- getSessionTitle() gains a new fallback ordered above the humanized
  branch, so sessions remain identifiable even when the tracker API
  is unavailable or an orchestrator has no attached issue.

Tests:
- metadata.test.ts: displayName round-trip.
- spawn.test.ts: displayName derivation from issue title, user prompt
  (first line), system prompt; truncation with ellipsis; omission
  when there is no context.
- format.test.ts: orchestrator/ prefix handling, session-ID collapse,
  displayName fallback, orchestrator bug repro ("Audit test coverage
  for session-manager" instead of "Orchestrator/Ao Orchestrator 8"),
  fall-through to summary/status when displayName is absent.

Refs #1220

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(format): prefer cleaned displayName over raw userPrompt (#1220)

Cursor bugbot review on #1224 caught that for prompt-only sessions,
`userPrompt` (step 3) always shadowed `displayName` (step 4) in
`getSessionTitle`. Both are populated from the same `spawnConfig.prompt`
at spawn time: `userPrompt` stores the raw multi-line original, and
`displayName` is the single-line 80-char-truncated version produced by
`deriveDisplayName`. Because `userPrompt` was checked first, the careful
cleanup never reached the dashboard — kanban cards and session tabs
rendered the full raw prompt instead of the clean version the PR was
supposed to deliver.

Swap the order so `displayName` wins when present. `userPrompt` remains
as a safety-net fallback for sessions spawned before `displayName`
existed or where derivation failed.

Add regression tests:
- prefers cleaned displayName over raw userPrompt for prompt-only sessions
- falls through to raw userPrompt when displayName is absent

Refs #1220

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(sessions): address review comments on displayName PR

- ProjectSidebar: drop session.branch short-circuit so displayName/cleaned
  branch fallback is reached (was rendering raw branch for any session
  with a branch, defeating the new fallback chain).
- session-manager restore: preserve displayName when rewriting metadata
  from archive so restored sessions keep their derived title. Add
  regression test.
- deriveDisplayName: truncate on code-point boundaries via Array.from so
  emoji at the boundary aren't cleaved into lone UTF-16 surrogates. Same
  fix applied to the tab-title truncate in web session page.
- Add regression test covering surrogate-boundary truncation.

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-22 12:24:03 +05:30
Chirag Arora 59f66f4de3
fix(web): gate dashboard debug bundle and move header placement (#1400)
Follow up #1320 by hiding the debug bundle control by default and only showing it in development or with . Move the button to the left header cluster and add regression tests for default-hidden and debug-enabled visibility.

Made-with: Cursor
2026-04-21 19:07:08 +05:30
Ashish Huddar f3ce113c4c
Add multi-project storage, resolution, and project settings support (#1343)
* feat: add content-addressed project storage keys

* Add per-project resolution and hardened project routing

* Fix storage-key test isolation

* feat(web): redesign Add Project modal with Finder-native layout

* feat: multi-project support with project sidebar, settings, and improved routing

Add per-project configuration in global-config, project-aware CLI commands
(start/spawn/open/session), workspace-worktree project resolution, redesigned
ProjectSidebar with settings modal, repair flow for degraded projects, project
detail page with loading state, reload API endpoint, and comprehensive tests.

* Fix legacy config storage keys and duplicate project flow

* Fix multi-project storage migration and collision handling

* Fix merge regressions in startup and config handling

* Externalize yaml and zod from the web server bundle

* Fix session prefix matching for hashed tmux names

* Fix multi-project migration regressions

* Ignore generated worktree files in ESLint

* Fallback reload config for local-only projects

* Speed up session refresh and redirect after kill

* Use fresh session lists for dashboard polling

* fix(web): remove unused direct terminal child state

* test(web): mock router in merge conflict actions coverage

* docs: call out filesystem browse rollout requirement

* Add portfolio tests and remove unused decomposer export
2026-04-21 17:45:55 +05:30
Chirag Arora f86d7f856f
fix(web): resolve dashboard issue URLs from tracker (#1353)
* fix(web): resolve dashboard issue URLs from tracker

Build issue URLs from tracker.issueUrl when sessions store identifier-only issue IDs so dashboard links, labels, and title enrichment stay valid for GitHub-style numeric issues.

Made-with: Cursor

* fix(web): harden issue URL enrichment and add regressions

Guard issue enrichment against URL double-wrapping, free-text synthetic links, and invalid tracker URL output while logging failures. Add regression coverage for URL-shaped issue ids, free-text issue ids, tracker issueUrl failures, and failed issue-title lookup throttling.

Made-with: Cursor

* fix(web): remove unreachable issue-url branch for lint

Drop the duplicate else-if path in enrichSessionIssue that was already covered by the absolute-url guard, resolving no-dupe-else-if and unblocking lint/typecheck/web onboarding checks.

Made-with: Cursor
2026-04-21 16:01:12 +05:30
Chirag Arora 380b5d5a7c
fix(web): surface dashboard SSR load failures (#1316)
* fix(web): surface dashboard SSR load failures

When getServices or session listing throws during SSR, show an error banner
and skip the empty-state CTA instead of implying there are zero sessions.
Disables realtime hooks while the banner is shown to avoid noisy reconnects.

Made-with: Cursor

* fix(web): make dashboard SSR errors recoverable

Narrow dashboard fatal errors to service/bootstrap failures and fail-soft on enrichment so transient metadata issues do not blank the page. Keep realtime updates active after SSR load errors, sanitize multiline error messages, and add regression tests.

Made-with: Cursor

* fix(web): address dashboard SSR recovery and attention-zone SSR

- Gate hiding the SSR load-error banner on liveSessionsResolved from
  useSessionEvents (successful /api/sessions refresh, SSE snapshot, or mux),
  not session count; use cache: no-store on session refresh fetches.
- Apply attentionZones from config immediately after getServices(); wrap
  sessionManager.list() in its own try/catch so list failures keep zone config.
- Treat session.status merged like merged PR for Done cards (hide Restore).
- Align ProjectSidebar orchestrator fixture id with isOrchestratorSession pattern.

Made-with: Cursor
2026-04-21 11:38:02 +05:30
Chirag Arora f09cc72dc8
feat(cli): hide terminal sessions in `ao session ls` by default (#1319)
* feat(cli): hide terminal session rows by default in session ls

- Filter with TERMINAL_STATUSES unless --include-terminated
- Hint when completed sessions are hidden; docs/CLI.md updated

Made-with: Cursor

* fix(cli): keep session ls JSON inventory stable

Address review concerns by preserving terminal sessions in `ao session ls --json` output while keeping terminal rows hidden by default in text output. Use the core terminal-session predicate, improve hidden-row messaging, and add regression tests for mixed/hinted and JSON behavior.

Made-with: Cursor

* fix(cli): resolve rebase conflicts with latest session ls behavior

Align session command and CLI docs with the current mainline JSON contract (`data` + `meta.hiddenTerminatedCount`) while preserving terminal-filter defaults and updated regression coverage.

Made-with: Cursor
2026-04-21 11:35:54 +05:30
Chirag Arora e518562a62
feat(web): copy observability debug bundle from dashboard (#1320)
* feat(web): copy observability debug bundle from dashboard

- Add CopyDebugBundleButton (desktop hero) calling GET /api/observability
- Document in docs/observability.md

Made-with: Cursor

* fix(web): harden debug bundle copy safety

Scope copied observability payloads to the selected project, scrub obvious token patterns from copied strings, and avoid copying on failed/non-JSON observability responses. Also avoid leaking query/hash URL data and add tests for failure branches.

Made-with: Cursor
2026-04-21 11:35:43 +05:30
Chirag Arora fed25d5d29
feat(web): merge conflict compare link and copy branch on session PR card (#1318)
* feat(web): merge conflict compare link and branch copy on PR card

- Add buildGitHubCompareUrl helper with unit tests
- Show compare + copy head branch when open PR has merge conflicts
- Add SessionDetail regression test for conflict affordances

Made-with: Cursor

* fix(web): harden merge-conflict PR actions

Address review feedback by correcting the changeset package name, encoding all compare URL path segments, and making conflict actions resilient to unenriched/rate-limited PR data and clipboard/timer edge cases. Add regression tests for URL encoding and conflict-action gating.

Made-with: Cursor

* fix(web): normalize browser timer handles in session detail

Use browser timer handle types consistently in the session detail PR card copy-feedback flow so Next.js typechecking does not mix Node Timeout and DOM number timer signatures.

Made-with: Cursor
2026-04-21 11:35:29 +05:30
Harsh Batheja 64badbd388
fix: run deploy as aoagent instead of root (#1378)
The VPS deploy workflow SSHes in as root but operates on files owned by
aoagent via the /root/agent-orchestrator → /home/aoagent/agent-orchestrator
symlink. Each deploy leaves new pnpm/bun/cache entries owned by root inside
an otherwise aoagent-owned tree. Switching the SSH user to aoagent
eliminates this ownership drift.
2026-04-21 01:45:10 +05:30
Harshit Singh Bhandari 99f15aeb2a
docs: add copilot instructions guidance (#1331)
Add the repository Copilot instructions file based on the AO draft and clean up wording, grammar, and markdown formatting while preserving the original guidance.
2026-04-21 01:08:56 +05:30
Harshit Singh Bhandari e45f34e1dd
Merge pull request #1308 from harshitsinghbhandari/fix/ao-start-orchestrator-reuse-race
fix: restore dead orchestrators on ao start
2026-04-20 14:53:20 +05:30
Dhruv Sharma f0d4faf2a4
fix: fail ao send when killed session delivery is not confirmed (#1236)
* fix: fail send when killed session delivery is not confirmed

* fix(core): drop requireConfirmation throw that re-introduced duplicate-message bug

The PR's sendWithConfirmation added a throw when confirmation heuristics
did not flip within SEND_CONFIRMATION_ATTEMPTS on a restored session.
But runtimePlugin.sendMessage had already fired, so the throw bubbled up
to the lifecycle manager's catch-all, leaving lastCIFailureDispatchHash
(and its merge-conflict twin) unset. Next poll re-dispatched the same
message — exactly the duplicate-message bug that commit 77685a5 removed.

Real fix for #1074 is preserved: restoreForDelivery still throws when
waitForRestoredSession returns false, which happens before sendMessage
fires. Killed sessions that cannot be revived are still reported as
failures, with no duplicate-send risk.

- sendWithConfirmation no longer takes requireConfirmation; unconfirmed
  delivery always returns (soft success).
- prepareSession returns Session (the tuple only existed to drive the
  removed throw).
- send's retry predicate reverts to prepared.restoredAt === undefined
  && isRestorable(prepared).
- Adds regression test: restored session + sendMessage fires +
  confirmation never flips → send() resolves.

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

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-20 13:56:31 +05:30
harshitsinghbhandari b2abcb5c3c test: fix restore mock setup in start tests 2026-04-20 13:33:34 +05:30
harshitsinghbhandari 55ebb6395a fix: tighten startup lock cleanup 2026-04-20 13:30:07 +05:30
harshitsinghbhandari dcd003fbd6 fix: restore dead orchestrators from session detail 2026-04-20 13:18:49 +05:30
Ashish Huddar f330a1ea69
feat(cli): filter terminated sessions from ao session ls / ao status by default (#1340)
* feat(cli): filter terminated sessions from ao session ls / ao status by default

Closes #1310. Terminated sessions (killed/terminated/done/merged/errored/cleanup,
plus lifecycle-driven terminal states) are now hidden from `ao session ls` and
`ao status` by default. A dim footer reports how many were hidden and how to
surface them. Pass `--include-terminated` to restore the full list.

JSON output wraps into `{ data: [...], meta: { hiddenTerminatedCount } }` on
both commands so text and machine-readable views tell the same story. This is a
breaking change for script consumers of `--json`; `--include-terminated` is the
escape hatch.

Orthogonal to `-a, --all` (orchestrator visibility, unchanged). Restore of
terminated sessions by id is unaffected — that path goes through `sm.get`, not
`sm.list`.

Docs (`SETUP.md`, `docs/CLI.md`) updated to match. Tests cover both the legacy
status branch and the canonical lifecycle branch of `isTerminalSession`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(cli): drop unused `lc` param in lifecycle-alive test case

ESLint's no-unused-vars rejects unprefixed unused args. The "alive — should
remain visible" branch of the new lifecycle-driven filter test in
`session.test.ts` took `lc` but never touched it. Switch to `()`. Matches the
equivalent case in `status.test.ts`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(core): preserve pr.state=merged when legacy metadata lacks pr= URL

Review blocker on PR #1340: a metadata file with `status=merged` but no `pr=`
URL was still showing as active in `ao session ls` / `ao status` by default.

Root cause: `synthesizePRState()` in lifecycle-state.ts short-circuited to
`{ state: "none" }` whenever no PR URL was present, ignoring the fact that the
legacy `status` column already encodes terminal truth. Once lifecycle was
synthesized as `session.state="idle"` + `pr.state="none"`, `deriveLegacyStatus`
returned `"idle"` and `isTerminalSession()` (lifecycle branch) returned false.
The new CLI filter then let the session through.

Fix: when legacy `status === "merged"` and no URL is available, synthesize
`pr.state="merged", reason="merged"` with `number: null, url: null`. The
terminal signal survives the flat-metadata → canonical-lifecycle round trip.

Also:
- Export `sessionFromMetadata` from the core barrel. CLI tests and external
  consumers need it to round-trip metadata through the canonical lifecycle.
- Update CLI `buildSessionsFromDir` helpers to route through `sessionFromMetadata`
  so mocked `sm.list()` reflects production reconstruction (the old shortcut
  bypassed synthesis entirely and was the reason the bug slipped past the
  original test suite).
- Add regression tests: one at the core level (`parseCanonicalLifecycle` for
  merged-without-URL) and one integration-style test per CLI command asserting
  the reviewer's exact repro produces the expected filtered output.
- One pre-existing test expectation updated: when metadata has `status=working`
  and `pr=<url>`, the reconstructed status is `pr_open`, not `working`. That's
  what production `sm.list()` has always returned; the test was previously
  hiding behind the reconstruction shortcut.

Changeset bumped to include ao-core (patch).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(cli): route review-check helper through sessionFromMetadata

Last remaining test-fidelity shortcut flagged by codex on PR #1340. The
`buildSessionsFromDir` helper in review-check.test.ts fabricated Session
objects by hand, bypassing the canonical lifecycle reconstruction that
production `sm.list()` runs. Doesn't affect review-check's actual behavior
(which reads `session.metadata["pr"]` directly), but aligns this test with
the equivalent helpers in session.test.ts and status.test.ts so future
lifecycle changes don't silently skip this surface.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 13:05:34 +05:30
Ashish Huddar faaddb15df
feat(core): auto-terminate sessions on PR merge (#1309) (#1311)
* feat(core): auto-terminate sessions on PR merge (#1309)

When a session's PR was detected as merged, the session transitioned
to status "merged" but its tmux runtime, worktree, and metadata were
never cleaned up — leaving zombie tmux sessions and stale entries in
`ao status` / `ao session ls`. Users worked around this with an
external watchdog. Close the loop in AO itself.

Changes:
- `kill()` gains an optional `reason` and returns `KillResult`
  (`cleaned` / `alreadyTerminated`), with short-circuit paths so
  repeated calls on archived sessions are safe no-ops instead of
  throwing `SessionNotFoundError`.
- New `LifecycleConfig` (`autoCleanupOnMerge: true` default,
  `mergeCleanupIdleGraceMs: 5 min`) so operators can opt out when
  they need merged worktrees preserved for inspection.
- `lifecycle-manager` runs `maybeAutoCleanupOnMerge` at the end of
  each `checkSession`. Reactions and notifications observe the live
  session first; cleanup runs last. If the agent is still `active` /
  `waiting_input` / `blocked`, cleanup is deferred and retried on
  the next poll until the agent idles or the grace window elapses
  (prevents killing an agent mid-task).
- New `CanonicalSessionReason` / `CanonicalRuntimeReason` variants
  (`pr_merged`, `auto_cleanup`) so observability distinguishes
  automated teardown from manual kills.

Scope is deliberately narrow to `merged`: `done` / `errored` often
need the worktree preserved for debugging; `killed` would self-recurse.

Follows Codex review feedback (conditional pass): scope narrowed,
reactions-before-cleanup ordering, idleness safety gate, real
idempotency guards, config opt-in.

6 new unit tests cover: idle agent cleanup, active agent deferral,
grace-window force-cleanup, config opt-out, terminated/killed no
self-recursion, kill() failure retry.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(core): clean up lifecycle config access per review

Address review comment on PR #1311. The `config.lifecycle` field is
typed optional but always populated by Zod — the old guard chain
(`if (lifecycleConfig && lifecycleConfig.autoCleanupOnMerge === false)`)
obscured that duality. Destructure with defaults at the call site so
the contract is visible in one place, and document why the field stays
optional (hand-constructed test configs) on the interface.

Matches the existing `power?: PowerConfig` pattern — keeps churn to
zero across 60 test config literals while removing the ambiguous guard.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs(core): surface auto-cleanup-on-merge in config, docs, and UI

Followup to DX audit on #1311. The lifecycle cleanup behavior was
operational but invisible — config key only in TS types, no changeset
for downstream consumers, missing observability spec, and a dashboard
summary that actively contradicted the new default-on behavior.

- agent-orchestrator.yaml.example: add commented `lifecycle:` block
  with both keys so operators discover the knob in the primary
  config reference.
- .changeset/auto-cleanup-on-merge.md: minor bump for @aoagents/ao-core
  with migration note (default-on, opt-out via config).
- docs/observability.md: document the three new lifecycle_poll
  operations (merge_cleanup.completed / deferred / failed) so
  dashboard/alert authors have a spec.
- packages/web/src/lib/serialize.ts: replace stale summary
  "PR merged; worker is still available for a keep-or-kill decision"
  with "PR merged; worker session will be cleaned up automatically".
  The old copy is wrong under default-on auto-cleanup.

Deferred to follow-up issues:
- Health surface degradation on repeated cleanup failure (the
  operator-facing gap Codex flagged — failures emit a metric but
  don't downgrade /api/observability health).
- Dashboard "cleaning up in Nm" indicator for the deferred state.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(core,web): address PR review feedback for auto-cleanup on merge

- serialize.ts: only claim "will be cleaned up automatically" when
  mergedPendingCleanupSince marker is present; otherwise show neutral
  "PR merged". Avoids lying when autoCleanupOnMerge is opted out.
- lifecycle-manager.ts: use ACTIVITY_STATE constants instead of
  hardcoded strings, matching the existing SESSION_STATUS.MERGED usage.
- config.ts: keep mergeCleanupIdleGraceMs=0 as a valid escape hatch
  (immediate cleanup), but reject 1..9999 with a units-mistake error
  so users typing `5` (intending seconds) get a clear message.

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 12:56:07 +05:30
Harshit Singh Bhandari 9e6f63d366
Merge pull request #1352 from harshitsinghbhandari/fix/terminal-cell-metrics-v6 2026-04-20 09:28:43 +05:30
harshitsinghbhandari 3be86e7dd1 fix(web): address pr-review findings on #1352
Follow-up to the first round of review comments:

- Guard `document.fonts` add/removeEventListener with feature detection.
  Without the guard, environments where FontFaceSet doesn't expose
  EventTarget (jsdom test mocks, older browsers) threw a TypeError
  during xterm init and the terminal silently failed to attach.
- Update DirectTerminal.render.test.tsx mock so `document.fonts`
  exposes addEventListener/removeEventListener stubs, restoring
  happy-path coverage of the init code path.
- Export `resolveMonoFontFamily` and add unit tests covering:
  CSS var present (prepended to fallback), CSS var absent
  (fallback only), and the invariant that no `var(...)` token ever
  leaks into the output.
- Clarify in the docblock why we read `--font-jetbrains-mono` and
  deliberately avoid `--font-mono` (the latter re-wraps in `var(...)`
  and would re-introduce the bug).

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
2026-04-20 04:57:28 +05:30
harshitsinghbhandari 76d30e386b fix(web): address PR #1352 review comments
- Resolve --font-jetbrains-mono via getComputedStyle at runtime so
  xterm honours the app's configured mono font token instead of a
  hard-coded "JetBrains Mono" fallback. next/font generates a unique
  family name (stored in the CSS custom property) that now gets fed
  to xterm alongside the fallback stack.
- Re-resolve the font-family on `document.fonts` `loadingdone` so
  xterm picks up the generated name once it registers.
- Change SessionDetail's DirectTerminal loading skeleton from a fixed
  h-[440px] to h-full, so the terminal area stays viewport-sized
  during the lazy-load window instead of locking to 440px.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
2026-04-20 04:49:19 +05:30
harshitsinghbhandari ca85eb5e61 fix(web): restore terminal cell metrics after xterm v6 upgrade
The v5 -> v6 xterm.js upgrade regressed the in-browser terminal: each
cell rendered visibly wider and shorter than the glyph inside it,
making horizontal spacing too wide and vertical spacing too tight.

Root causes:

1. `fontFamily` contained `var(--font-jetbrains-mono)`. xterm's char
   measurement ultimately hits canvas `ctx.font`, which cannot resolve
   CSS custom properties. The var token poisoned the font string so
   measurement fell back to a default font while DOM rows still rendered
   in JetBrains Mono — cell width vs glyph width drifted apart.

2. xterm v6 defaults `lineHeight` to 1.0. Combined with JetBrains Mono's
   tall x-height, rows visually collided.

3. `document.fonts.ready` can resolve before next/font's
   `font-display: swap` actually paints JetBrains Mono, so the initial
   `fit()` measures against the fallback font. xterm does not re-measure
   when the swap later lands.

4. The fit-target div had `p-1.5` padding, skewing FitAddon's cols/rows
   computation.

Fixes applied to `DirectTerminal.tsx`:

- Drop `var(...)` from `fontFamily`; use a plain font stack.
- Set `lineHeight: 1.2` to restore vertical breathing room.
- Add a `document.fonts` `loadingdone` listener that clears the
  texture atlas and re-fits when the webfont swap completes. Cleaned
  up in the effect teardown.
- Remove `p-1.5` from the terminal ref div.

Co-Authored-By: Claude Opus 4 <noreply@anthropic.com>
2026-04-20 04:33:03 +05:30
Harshit Singh Bhandari 7b13d2bead
fix(web): remove session detail lifecycle and audit panels (#1324) 2026-04-19 20:41:00 +05:30
harshitsinghbhandari eee8e66ffc fix: address pr-review regressions (#1306) 2026-04-19 19:59:16 +05:30
harshitsinghbhandari 6b15e68fc3 fix: address follow-up review regressions 2026-04-19 19:59:16 +05:30
harshitsinghbhandari 330fe7e63c docs: align changeset with codex restore behavior 2026-04-19 19:59:16 +05:30
harshitsinghbhandari 350c5c08e8 fix: restore permissionless codex workers with bypass 2026-04-19 19:59:16 +05:30
harshitsinghbhandari bb5dcf0af7 fix: address #1306 review follow-ups 2026-04-19 19:59:16 +05:30
harshitsinghbhandari aede68e8da test: remove duplicate restore mock 2026-04-19 19:59:16 +05:30
harshitsinghbhandari 27135eab0e fix: close failed startup lock writes 2026-04-19 19:59:16 +05:30
harshitsinghbhandari e1bb51f42a chore: replace changelog edits with changeset 2026-04-19 19:59:16 +05:30
harshitsinghbhandari 3ba526d282 fix: relax codex restore approval mode 2026-04-19 19:59:15 +05:30
harshitsinghbhandari 611ded42ca fix: prefer live orchestrators in worker navigation 2026-04-19 19:59:15 +05:30
harshitsinghbhandari 0481dbf158 fix: harden startup lock handling (#1306) 2026-04-19 19:59:15 +05:30
harshitsinghbhandari c6129de1c9 fix: restore dead orchestrators on start (#1306) 2026-04-19 19:59:15 +05:30
harshitsinghbhandari c00ccc773e fix: serialize ao start and stop numbered orchestrators (#1306) 2026-04-19 19:59:15 +05:30
Dhruv Sharma cb10f8c240
Merge pull request #1328 from yyovil/yyovil/fix-issue-1327 2026-04-19 19:27:32 +05:30
yyovil f4916bea63 fix: select matching gitleaks binary for arm64 runners 2026-04-19 18:49:04 +05:30