Commit Graph

1219 Commits

Author SHA1 Message Date
Adil Shaikh 200bf4dfee
Potential fix for pull request finding 'CodeQL / Client-side cross-site scripting'
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2026-05-14 12:41:17 +05:30
whoisasx 84bae872fa Remove redundant dashboard notification actions 2026-05-14 11:57:41 +05:30
whoisasx 9d802b5555 Update AO notifier app icon 2026-05-14 11:42:00 +05:30
whoisasx bd1fa42c61 Improve desktop notification UX 2026-05-14 03:47:07 +05:30
whoisasx 66b89e2acf Improve notifier setup and rich notifications 2026-05-14 03:03:18 +05:30
whoisasx c665da9183 chore: use ao-agent as composio default user 2026-05-13 21:49:01 +05:30
whoisasx 2401abf6e9 Make notifier payloads semantic v3 2026-05-13 21:24:11 +05:30
whoisasx 360c302f0a feat: add dashboard notifier 2026-05-13 20:57:07 +05:30
whoisasx ed176861b3 Fix notifier setup flows 2026-05-13 17:35:39 +05:30
whoisasx 23014ae402 feat(notifier): add composio setup flows 2026-05-12 05:48:04 +05:30
whoisasx 8b0cd43e5f fix(notifier): use composio entity execution 2026-05-12 00:58:26 +05:30
whoisasx 3304c60416 fix(notifier): support composio actions api 2026-05-12 00:42:28 +05:30
whoisasx 0de5bb9114 fix(cli): handle denied desktop notification permission 2026-05-11 15:26:49 +05:30
whoisasx 54c61ca31a feat(cli): add native desktop notifier setup 2026-05-11 15:11:53 +05:30
whoisasx 23eb789f6b feat(cli): add manual notifier test harness 2026-05-10 21:59:01 +05:30
whoisasx 3bea2e9c9e fix(notifier-desktop): use terminal-notifier on macOS for click-to-open support
On macOS, when terminal-notifier is installed (brew install terminal-notifier),
desktop notifications now open the dashboard URL when clicked instead of
opening Script Editor. Falls back to osascript when terminal-notifier is
not available.

New config option `dashboardUrl` controls the click-through URL:
  notifiers.desktop.dashboardUrl: "http://localhost:8080"

Refs #1579
2026-05-08 19:16:35 +05:30
i-trytoohard 9bfd7656bb
fix(cli): refuse to spawn when daemon is not polling the project (#1460)
* fix(cli): refuse to spawn when daemon is not polling the project

`ao spawn` and `ao batch-spawn` used to print a stderr warning and then
create the session anyway when the running AO daemon did not include the
target project in its polling set (or when no daemon was running at all).
The resulting sessions got full worktrees and tmux panes but no
lifecycle reactions — CI-failure routing, review comments, revive
transitions, and the event log were silently dead.

Promote the warning to a hard error so sessions are never created in a
state where the lifecycle manager won't run for them. The error message
tells the user which `ao start` invocation will fix it.

Closes #1455

* test(cli): cover batch-spawn daemon-polling enforcement

`spawn` and `batch-spawn` share the `ensureAOPollingProject` helper, but
only `spawn` had tests for the new fail-fast behavior. Add matching
tests for `batch-spawn` so a future refactor that breaks its guard is
caught.

---------

Co-authored-by: Prateek <karnalprateek@gmail.com>
2026-05-08 18:13:42 +05:30
Adil Shaikh 1981d471ca
fix(core): deliver prompt inline via positional arg, remove post-launch polling (#1583)
Claude Code's positional [prompt] argument keeps it in interactive mode;
only -p/--print triggers headless one-shot exit. The entire post-launch
polling mechanism was built on the wrong assumption.

Changes:
- Claude Code plugin: pass prompt as positional arg in getLaunchCommand
- Core types: remove promptDelivery field from Agent interface
- Session manager: remove post-launch polling/retry block
- Prompt builder: clarify wording ("title, description, and labels"
  instead of "full issue details"), unify # format
- Tracker-github: match prompt wording update
- CLI spawn: remove dead-code promptDelivered warning

Closes #1582
2026-05-08 17:44:10 +05:30
i-trytoohard b9b20e19d1
chore: release 0.6.0 (#1723)
* chore: release 0.6.0

* test(agent-codex): bump version pin to 0.6.0

---------

Co-authored-by: Prateek <karnalprateek@gmail.com>
2026-05-08 02:37:44 +05:30
i-trytoohard a2afccc69a
fix(web): disable xterm scrollback to prevent terminal right-side clipping (#1678)
* fix(web): disable xterm scrollback to prevent right-side clipping

FitAddon.proposeDimensions() reserves 14px (DEFAULT_SCROLL_BAR_WIDTH)
for the scrollbar when scrollback > 0. The custom CSS sets the actual
scrollbar to 5px with overflow-y: overlay (deprecated in Chrome 114+).
This mismatch causes the terminal to calculate the wrong number of
columns — content gets clipped under the scrollbar on the right side.

tmux already provides scrollback and copy-mode, so xterm's scrollback
is redundant. Setting scrollback: 0 eliminates the scrollbar entirely
and makes FitAddon's width calculation match the rendered output.

Fixes #1677

* chore(web): refresh stale scrollback comments per greptile

* fix(web): reset letter-spacing on .xterm to fix right-side char clipping

The body has letter-spacing: -0.011em (~-0.176px at 16px) for typography
refinement. xterm's DOM renderer measures cell width with that spacing
inherited (~7.83px), then sets an inline letter-spacing override on
.xterm-rows that cancels the body inherit, leaving glyphs to render at
their natural ~8.00px width.

The mismatch — measured 7.83px cells vs rendered 8.00px glyphs —
accumulates as cols grow, eventually overflowing .xterm-rows > div and
getting clipped by its overflow: hidden. At 124 cols the drift was
~21px, chopping ~3 chars off the right edge.

Resetting letter-spacing on .xterm makes both phases agree and reduces
the drift to sub-pixel rounding (~2px worst case at any reasonable
cols).

---------

Co-authored-by: AO Bot <ao-bot@composio.dev>
Co-authored-by: Prateek <karnalprateek@gmail.com>
2026-05-08 02:15:57 +05:30
i-trytoohard d0dff3b93f
fix(web): drop = prefix from set-option in mux-websocket (closes #1714) (#1715)
* fix(web): drop = prefix from set-option in mux-websocket (closes #1714)

In tmux 3.4 the `=` exact-match prefix only works with `has-session` and
`attach-session`. For `set-option`, the prefix is silently ignored, so
`mouse on` and `status off` never get applied — breaking scroll wheel in
the dashboard terminal and leaving the tmux status bar visible.

Use the bare session id for the two `set-option` calls; keep `=` on
`attach-session` where it is correct.

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

* refactor(web): move exactTmuxTarget next to attach-session

Address review feedback: the `=`-prefixed target is only used by
attach-session, so declare it adjacent to that call. Comment now
sits above the set-option calls it actually explains.

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

---------

Co-authored-by: Prateek <karnalprateek@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 19:08:21 +05:30
Harshit Singh Bhandari 0f539a3d4e
fix(cli): reload dashboard config after adding project from already-running menu (#1706)
When `ao start` finds an existing daemon and the user picks "Add <dir>",
the project is written to the global config but the dashboard's cached
services (loaded once into globalThis) never see it, so visiting the new
project page renders notFound(). Call notifyProjectChange() after the
write — same pattern as attachAndSpawnOrchestrator — so the dashboard
invalidates its cache before the browser opens.
2026-05-07 18:43:19 +05:30
Harshit Singh Bhandari 5c1d56aea4
fix(web): bound PTY re-attach loop with grace-period counter reset (#1640)
* fix(web): bound PTY re-attach loop with grace-period counter reset (#1639)

When `ao stop` (or any external action) kills a tmux session out from
under a still-subscribed dashboard, the mux server's PTY exit handler
attempts to re-attach. The MAX_REATTACH_ATTEMPTS=3 cap was supposed to
prevent unbounded respawning, but it was never engaging because the
counter was reset to 0 immediately after each "successful" `open()` —
where success only meant the new PTY was *spawned*, not that it
*survived*. When the underlying tmux session is gone, attach-session
exits ~40 ms after spawn, the exit handler fires again with counter=0,
and the loop runs at ~80 spawns/sec.

Diagnostic data captured on the issue: a single 1.5-second burst
produced 119 spawn↔exit cycles, raising the process's PTY fd count
from ~15 to ~153. Sustained for a few seconds, this exhausts the
macOS system PTY pool (kern.tty.ptmx_max=511), after which nothing
on the system can spawn a new PTY (tmux, VS Code terminal, ao spawn,
etc.) until the leaking process is killed.

Fix:
- Remove the `terminal.reattachAttempts = 0` reset inside the exit
  handler.
- Schedule a delayed reset via setTimeout in `open()`, gated on the
  closure-captured `pty` reference still being terminal.pty after
  REATTACH_RESET_GRACE_MS (5 s).

Effect: tight crash loops cannot reset the counter (PTY exits before
grace expires) and hit MAX_REATTACH_ATTEMPTS within ~150 ms, after
which the server emits "exited" and stops respawning. A long-lived
PTY that crashes hours later still gets a fresh retry budget.

Adds an integration test that reproduces the runaway scenario by
killing the tmux session externally and asserting "exited" arrives
within 2 s. Without this fix the test hangs and times out — the
exact symptom of the bug.

Note: this addresses the dominant runaway behaviour. A separate
~1 fd/cycle leak in node-pty 1.1.0 itself (each spawn opens 3
PTY-class fds in the parent, each exit releases only 2) remains and
will be tracked separately — likely a node-pty upgrade.

Fixes: #1639

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

* fixup(web): track grace timer + add recovery test (#1639 PR review)

Address two PR review notes on #1640:

1. Greptile P2 — store the grace-period timer handle on ManagedTerminal
   and clearTimeout it in the unsubscribe cleanup path. The closure
   guard already prevented any incorrect counter reset, so this is
   tidiness rather than a bug fix: it eliminates the up-to-5 s window
   where the timer's closure kept the killed PTY and evicted terminal
   object reachable. Also clears any prior timer when scheduling a new
   one in open() so back-to-back re-attaches don't pile up dead closures.

2. Copilot — add an integration test for the recovery path. The
   existing runaway test exercises the case where the counter must NOT
   reset (PTY crashes inside grace); the new test exercises the case
   where the counter MUST reset (PTY survives grace, then crashes
   later). Without the grace timer firing correctly, a single transient
   blip during startup would permanently consume the retry budget.

Test takes ~5.5 s because it uses the production grace period; per-test
timeout raised to 15 s. Vitest runs tests in parallel so this doesn't
serialize the suite.

Refs: #1639, #1640

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

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-07 18:37:44 +05:30
i-trytoohard 496c3717ab
fix(runtime-tmux): disable tmux status bar + dead code cleanup (#1711)
* fix(runtime-tmux): disable tmux status bar at session creation

Closes #1709.

#1683 added `set-option ... status off` to `core/src/tmux.ts::newSession()`,
but no code in the workspace imports or calls that helper — worker sessions
are spawned by the `runtime-tmux` plugin, which was not modified. The green
status bar was therefore still visible at session creation, only being
suppressed once the web layer's WebSocket connection ran its own
`set-option` (with a flash window before that).

Add the same call to runtime-tmux immediately after `tmux new-session` so
the bar is hidden from the moment the session exists, regardless of
whether anyone ever attaches via the web terminal.

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

* chore(core): remove unused newSession helper

The `newSession` function in `core/src/tmux.ts` and its `newTmuxSession`
re-export in `core/src/index.ts` had zero callers anywhere in the
workspace (verified via grep across packages/, excluding dist and tests).
Worker sessions are spawned by the `runtime-tmux` plugin, which has its
own implementation. The status-bar fix from #1683 lived only in this
helper and was therefore never executed — see #1709 and the prior
commit which moves the fix to the actual spawn path.

Removes:
- `newSession` and `NewSessionOptions` from core/src/tmux.ts
- `newSession as newTmuxSession` re-export from core/src/index.ts
- The corresponding test block in core/src/__tests__/tmux.test.ts

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

* chore(core): demote unused GhTraceResult export to internal

`GhTraceResult` was exported from `core/src/gh-trace.ts` but never
re-exported from `core/src/index.ts` and never imported anywhere in
the workspace. Its only consumer is `writeTraceEntry()` inside the
same file, where it's used as a parameter type. Demoted to a private
interface.

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

* fix(runtime-tmux): kill session if set-option fails

Move the `set-option ... status off` call inside the existing try/catch
so a failure (e.g. the 5-second tmux command timeout firing on a slow
host) triggers `kill-session` cleanup instead of leaving an orphaned
tmux session behind. Renames the surfaced error to
"Failed to configure or launch session" since the try block now covers
both configuration and the launch send-keys.

Adds a regression test that asserts kill-session is called when
set-option throws.

Addresses review feedback on #1711.

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

---------

Co-authored-by: Prateek <karnalprateek@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 14:35:26 +05:30
i-trytoohard 40aeb78c09
feat(core): add per-project env block to ProjectConfig (#1679)
* feat(core): add per-project env block to ProjectConfig

Adds an optional `env: Record<string, string>` field to ProjectConfig
that forwards environment variables into worker session runtimes. Useful
for scoping per-project tokens like GH_TOKEN to pin gh auth per project.

The merge order in session-manager runtime.create environment is:
agent.getEnvironment → PATH/GH_PATH → AO_AGENT_GH_TRACE → project.env →
AO_* internals. AO-internal vars always win over user-supplied values.

Closes #169

* fix(core): protect PATH and GH_PATH from project.env override

Per greptile review on #1679: spreading `project.env` after PATH/GH_PATH
let a user-supplied PATH or GH_PATH silently clobber the carefully
constructed agent path. Apply the same "protected key" treatment as
AO_* internals — spread project.env BEFORE PATH/GH_PATH/AO_AGENT_GH_TRACE
at all three runtime.create call sites (worker spawn, orchestrator spawn,
restore). Extend the precedence test to assert PATH and GH_PATH still
win over a colliding project.env entry.

---------

Co-authored-by: Prateek <karnalprateek@gmail.com>
2026-05-07 11:29:19 +05:30
Dhruv Sharma fc7d76ad54
feat(core): wire activity events into lifecycle-manager failure paths (#1511) (#1620)
* rebase: forward branch onto main + resolve activity-events kind union conflict

* feat(core): wire scm/runtime/agent plugin-call failure events

Adds activity-event evidence for previously-silent failure paths in
lifecycle-manager.ts so the RCA agent can answer 'why did X happen?':

- scm.batch_enrich_failed (line 617 catch)
- scm.detect_pr_succeeded (line 658 success path)
- scm.detect_pr_failed (line 664 catch)
- scm.review_fetch_failed (line 1517 catch)
- scm.poll_pr_failed (line 1132 catch)
- runtime.probe_failed (line 938 catch)
- agent.process_probe_failed (lines 1054 + 1139 catches, with where field)
- agent.activity_probe_failed (line 1062 outer catch)

Plus 6 new tests covering the call shapes.

Invariants preserved (per CLAUDE.md):
- B1 state-mutate-before-emit: each emit follows existing observer call
- B2 never throws: recordActivityEvent best-effort by design
- B3 re-entrancy guard unchanged
- B4 Promise.allSettled semantics unchanged

* feat(core): wire reaction lifecycle activity events

Adds AE evidence around reaction triggers, escalations, and failures so
RCA can answer 'did AO try to auto-fix this? did it succeed?':

- reaction.action_succeeded (combined for send-to-agent / notify / auto-merge,
  with data.action variant) — fires after each successful reaction action
- reaction.send_to_agent_failed — fires in the previously-silent catch when
  sessionManager.send throws inside a send-to-agent reaction
- reaction.escalated — fires alongside the existing notifyHuman escalation
  with data.escalationCause = 'max_retries' | 'max_duration'

Plus 3 new tests covering the call shapes.

Invariants preserved: emits land after the existing notifyHuman/return
paths so state mutation order is unchanged.

* feat(core): wire auto-cleanup, poll-cycle, detecting escalation events

Adds AE evidence around session destruction, poll loop failures, and the
detecting→stuck transition so RCA can answer 'when did my session get
cleaned up?', 'did the polling loop crash?', and 'why did AO mark this
session stuck?':

- session.auto_cleanup_deferred — agent busy, cleanup deferred
- session.auto_cleanup_completed — kill succeeded, runtime + worktree gone
- session.auto_cleanup_failed (level=error) — kill threw, session stays merged
- lifecycle.poll_failed (level=error) — pollAll outer catch fired
- detecting.escalated — first cycle that promotes detecting→stuck, with
  cause = max_attempts | max_duration. Guarded by detectingEscalatedAt
  metadata so it fires once per escalation, not on every poll while stuck.

Plus 5 new tests covering the call shapes and the idempotency guard.

Invariants preserved:
- Auto-cleanup events fire AFTER existing observer.recordOperation (B1)
- detecting.escalated emits ONCE per escalation (invariant B9 in
  .context/lifecycle-manager-instrumentation.md)
- poll_failed emits inside the existing pollAll catch — flow unchanged

* feat(core): wire report_watcher.triggered activity event

Adds AE evidence when the report watcher fires (no_acknowledge / stale_report
/ agent_needs_input). RCA: 'AO thinks my agent is stuck — why?'

- report_watcher.triggered (level=warn) — emitted alongside the existing
  observer.recordOperation, only when a trigger is non-null (per invariant
  in .context/lifecycle-manager-instrumentation.md §B9)

Plus 1 test exercising the no_acknowledge trigger path.

* fix(core): one-shot guard on report_watcher.triggered AE emit

Live-observed regression: report_watcher.triggered fired 116 times in
production over a few hours because the emit was unguarded and re-fired
every 30s poll while a trigger stayed active. Symptom was massive event
flood for stuck/no-acknowledge/stale conditions.

Fix: gate the emit on the existing isNewTrigger variable (same one-shot
guard pattern used for detecting.escalated). The observer.recordOperation
above remains unguarded by design (it's a metric/heartbeat); the AE trail
is for actionable evidence only.

Adds a regression test that drives the same trigger across two polls and
asserts the AE event fires only on the first.

* fix(core): address Greptile feedback on PR #1620

Two findings from Greptile (issue same as Codex P2 #1):

1. scm.batch_enrich_failed omitted projectId/sessionId — when the
   lifecycle worker is project-scoped (deps.projectId set), this event
   is effectively project-scoped too. Without projectId, queries like
   `ao events list --project todo-app --type scm.batch_enrich_failed`
   return zero results, defeating the purpose of the instrumentation.
   Fix: pass scopedProjectId when set. Unscoped (multi-project) supervisors
   still leave projectId null because the batch crosses project boundaries.

2. Misleading field name pendingSinceMs in session.auto_cleanup_deferred
   data — the local variable of the same name is a Unix epoch timestamp,
   but the data field stored `Date.now() - pendingSinceMs` (an elapsed
   duration). RCA agents would mis-interpret it as a timestamp and compute
   a 1970-era "pending since" date. Renamed to pendingElapsedMs.

* fix(core): address Codex review on PR #1620

- lifecycle.poll_failed: keep summary generic, route raw error text
  through `data.errorMessage` only. sanitizeSummary just truncates;
  sanitizeData redacts credential URLs. Since FTS5 indexes summary,
  interpolating subprocess error output (which can include
  https://x-oauth-basic:TOKEN@github.com/... from git/gh) made
  credentials persistently searchable.

- reaction.escalated: expand escalationCause to
  "max_retries" | "max_attempts" | "max_duration" and mirror the
  trigger checks. Numeric escalateAfter is an attempt-count gate, not
  a duration; previously got misattributed to "max_duration" whenever
  retries was unset (built-in defaults use {escalateAfter: 2}).

Adds two regression tests as guards for both behaviors.

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

* fix(core): replace import() type annotation with import type to satisfy lint

CI's @typescript-eslint/consistent-type-imports rule rejects inline
`typeof import("../activity-events.js")` inside the vi.mock factory.
Hoist it to a top-level `import type * as ActivityEventsModule` so the
type lives in a proper import declaration; vi.mock factory resolution
is unaffected (type-only imports emit no runtime code).

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

* fix(core): keep report_watcher.triggered summary generic to plug FTS leak

auditResult.message for the agent_needs_input trigger embeds the
free-form report.note supplied via `ao report --note "..."`. Since
sanitizeSummary only truncates and FTS5 indexes the summary column,
a note containing a credential URL would be persistently searchable
from the events DB. Same class of bug as the prior poll_failed fix.

Summary becomes generic ("<trigger> triggered"); the full message
continues to flow through `data.message` where sanitizeData redacts
credential URLs.

Adds a regression test that seeds a needs_input report with a
credential-bearing note and asserts the summary stays clean.

Reported by @ashish921998 in PR #1620.

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

* fix(core): redact token-shaped secrets in activity-event data (P1)

Both `summary` and `data` columns are FTS5-indexed (events-db.ts:58-59).
Prior fixes moved raw error/report text from `summary` to `data.message` /
`data.errorMessage`, on the assumption that sanitizeData() would scrub it.
That assumption was incomplete: sanitizeData only redacted credential URLs
and entire values under sensitive *key* names. Token-shaped substrings
(`Bearer …`, `ghp_…`, `sk-…`, JWTs, `AKIA…`, ALL_CAPS_TOKEN=value) under
non-sensitive keys like `message`/`errorMessage` were stored as-is and
made searchable via FTS.

Adds a TOKEN_PATTERNS array applied to every string value during
sanitization, plus a 500-char per-string cap (matching sanitizeSummary's
existing precedent — limits blast radius if a new token format slips past
the patterns).

Patterns cover: Bearer headers, GitHub PATs (classic + fine-grained),
OpenAI/Anthropic sk- keys, Slack xox- tokens, AWS access key IDs, JWTs,
and ENV-style assignments scoped to ALL_CAPS keys ending in
TOKEN/PASSWORD/SECRET/etc.

Tests:
- 10 new sanitizeString unit tests (one per token shape + prose-preservation
  regression guard + 500-char cap + nested array/object recursion)
- 1 new FTS5 integration test that drives recordActivityEvent → real SQLite
  → both direct row read and FTS MATCH must return zero token leakage

Test fixtures use string concatenation across the prefix boundary so
literal token shapes don't appear in source (gitleaks pre-commit guard).

Reported by @ashish921998 in PR #1620.

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

* fix(core): bound credential-URL regex to prevent ReDoS (CodeQL alert)

CodeQL flagged CREDENTIAL_URL_RE as polynomial: input shaped like
`http://http://http://...` with no terminating `@` caused O(n²)
backtracking because the unbounded `[^@\s]+` greedily spanned multiple
`http://` prefixes before failing at end-of-string and walking back.

Two-part fix:
1. Exclude `/` from the userinfo character class — this is also semantically
   correct since RFC 3986 userinfo cannot contain unencoded `/`.
2. Add a hard length cap (200 chars) on the userinfo segment as a belt-and-
   braces guard against future pathological inputs.

The fix is observable: 14KB pathological input completes in single-digit
ms post-fix vs multiple seconds pre-fix. Adds a regression test that
runs the pathological input through the full sanitize pipeline and
asserts <100ms completion.

Reported by GitHub Advanced Security on PR #1620.

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

* fix(core): replace CREDENTIAL_URL_RE regex with linear scan

The bounded {1,200} quantifier in CREDENTIAL_URL_RE let credential URLs
with >200-char userinfo pass through unredacted. Since data is FTS5-indexed,
those credentials became searchable (P1 from PR #1620 review).

Replace the regex with a simple linear scan (redactCredentialUrls) that:
- Has no length limit — scans until @, space, or /
- Is O(n) with no regex backtracking (fixes CodeQL polynomial-regex alert)
- Matches http:// and https:// case-insensitively (preserves old /gi behavior)

Adds regression tests for:
- >200-char userinfo bypass
- URLs without userinfo (no false positives)
- Multiple credential URLs in one string
- Pathological ReDoS-shaped input still completes in <100ms

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: AO Bot <ao-bot@composio.dev>
2026-05-06 22:14:21 +05:30
i-trytoohard f617ae0746
fix(core): disable tmux status bar at session creation (#1683)
* fix(core): disable tmux status bar at session creation

The tmux green status bar was visible in web terminals because
newSession() never set status off. The global tmux config has
status on, and the web layer only disabled it on WebSocket connect.

Now we hide the status bar immediately after session creation so it's
never visible, regardless of when (or if) the web UI connects.

Fixes #1682

* test(core): update tmux newSession test for status off call

The new set-option status off call adds a 5th execFile invocation.
Update the test expectations to match the new call sequence.

---------

Co-authored-by: AO Bot <ao-bot@composio.dev>
2026-05-06 21:32:22 +05:30
i-trytoohard 8fee6c0e2d
chore: release 0.5.0 (#1676)
* chore(release): add changesets for 0.5.0 and bump codex version test

- Add changesets for #1643 (orchestrator worktree adoption), #1549
  (sidebar empty-state), and #1608 (terminal attach + mux routing).
- Update agent-codex package-version.test.ts expectation from 0.4.0
  to 0.5.0 so the test no longer fails after the upcoming version bump.

* chore: release 0.5.0

---------

Co-authored-by: i-trytoohard <193449657+i-trytoohard@users.noreply.github.com>
2026-05-06 16:44:45 +05:30
yyovil be061a3989
fix(core): adopt orphaned orchestrator worktrees (#1643)
* fix(core): adopt orphaned orchestrator worktrees (#1641)

* fix: address review feedback on worktree adoption

- Normalize CRLF line endings in parseWorktreeList for cross-platform support
- Collapse duplicate classifySpawnError payload blocks into single condition
- Filter prunable/deleted worktree entries in findManagedWorkspace
- Add GIT_TIMEOUT to git() helper for all execFileAsync calls
- Add tests for prunable entries and CRLF parsing

* fix: update test assertions for git() helper timeout

All git() calls now pass timeout: GIT_TIMEOUT to execFileAsync.
Update toHaveBeenCalledWith assertions to include the new option.
postCreate sh -c calls remain unchanged (direct execFileAsync).

* fix: mock existsSync in findManagedWorkspace tests

The existsSync(entry.path) filter added for prunable worktree detection
needs existsSync to return true for valid worktree paths in adoption tests.

* fix: use mockReturnValueOnce to prevent existsSync mock leaking

vi.clearAllMocks() does not reset mockReturnValue, only mock history.
Using mockReturnValueOnce ensures existsSync stubs don't leak to
subsequent tests and cause clearStaleWorktreePath to consume git mocks.

---------

Co-authored-by: AO Bot <ao-bot@composio.dev>
2026-05-05 20:34:00 +05:30
i-trytoohard ea320312db
refactor(core): extract hasRecentCommits helper into @aoagents/ao-core (#1437)
* refactor(core): extract hasRecentCommits helper into @aoagents/ao-core

Deduplicate the byte-identical hasRecentCommits(workspacePath) helper
that was copy-pasted between agent-aider and agent-cursor. Exposes the
helper from core with a parameterized window so future agent plugins
using git-commit-based activity detection can share it.

Closes #1423

* test(core): make hasRecentCommits custom-window test discriminate on the parameter

The previous assertion passed the default (60) to hasRecentCommits, so a
bug where windowSeconds was silently ignored would still have passed.
Use a commit backdated ~2 minutes and assert both a 30s window excludes
it and a 600s window includes it — proving the parameter is forwarded
to `git log --since=...`.

Addresses Greptile review on #1437.

---------

Co-authored-by: Prateek <karnalprateek@gmail.com>
2026-05-05 18:58:53 +05:30
i-trytoohard 3a69722940
chore(cli): remove deprecated 'ao init' command (#1438)
* chore(cli): remove deprecated 'ao init' command

Delete the `init` command and its deprecation shim. `ao start` already
auto-creates the config on first run in an unconfigured repo, so the
separate entry point is redundant.

- Remove `packages/cli/src/commands/init.ts` and its test.
- Remove `registerInit` call from the CLI program.
- Drop `createConfigOnly()` from start.ts (only the init shim used it);
  export `autoCreateConfig` so the existing default-config test can
  invoke it directly.
- Update user-facing "Run `ao init` first" messages in `verify`/`status`
  to point to `ao start`.
- Refresh stale `ao init` references in ao-doctor, onboarding test,
  openclaw setup doc, and the config/types doc comments.

Closes #1420

* docs: remove ao init website docs

Agent-Logs-Url: https://github.com/ComposioHQ/agent-orchestrator/sessions/41663963-ca49-4673-982f-ca10dee68d31

Co-authored-by: miniMaddy <77185670+miniMaddy@users.noreply.github.com>

---------

Co-authored-by: Prateek <karnalprateek@gmail.com>
Co-authored-by: iamasx <adilshaikh4064@gmail.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: miniMaddy <77185670+miniMaddy@users.noreply.github.com>
2026-05-05 18:43:55 +05:30
Harsh Batheja eb06a4d090
fix(web): render empty-state in sidebar when no projects configured (#1549)
* fix(web): render empty-state in sidebar when no projects configured

A fresh-install user with zero projects saw a blank sidebar and had no
way to open AddProjectModal from it. The early-return was originally
projects.length <= 1 (#381), softened to === 0 in #927, but no empty-
state UI was added at the same time.

Replace the null branch with a small ProjectSidebarEmpty sibling that
reuses the existing header (with the + button wired to AddProjectModal),
shows a one-line explainer, and renders only the ThemeToggle in the
footer (the show-killed/show-done/settings buttons are meaningless with
zero projects).

* fix(web): mark sidebar + button SVGs aria-hidden

The decorative SVG inside the labeled + buttons (empty-state and
populated sidebar) should not be announced — screen readers should rely
on the button's aria-label. Adds aria-hidden="true" to both for
consistency.

* fix(web): always mount sidebar so empty-state renders on fresh installs

Dashboard previously gated the sidebar on projects.length >= 1, leaving
ProjectSidebarEmpty unreachable. ProjectSidebar handles both cases now,
so drop the gate and add a dashboard-level test for the zero-project
path.

* fix(web): honor collapsed prop in empty sidebar branch

ProjectSidebarEmpty discarded the collapsed prop, so on a fresh install
the wrapper shrank to 44px while the inner sidebar stayed 224px and
overlapped the main content. Render a 44px-wide rail with just the +
button when collapsed, matching the populated sidebar's collapse path.
2026-05-04 20:13:11 +05:30
Ashish Huddar d0fde88f2a
Fix direct terminal attach and keep project-scoped mux routing (#1608)
* fix(qa): ISSUE-001 mobile kanban columns

* Fix terminal tmux targeting for session detail views

* fix(web): unblock event loop in tmux-name resolution and drop dup CSS

- mux-websocket: switch resolveExactTmuxName from execFileSync to
  promisified execFile so a slow tmux call no longer stalls the
  WebSocket message handler. Propagate async through TerminalManager.open
  / subscribe and the pty.onExit reattach path.
- globals.css: remove duplicate `.kanban-board { grid-template-columns:
  minmax(0, 1fr) }` rule. The 767px breakpoint already covers it.

Addresses Greptile feedback on PR #1608.

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

* refactor(web): drop defensive tmux-name precheck

The has-session precheck in resolveExactTmuxName was UX padding around
the actual fix (using tmux's `=` exact-match prefix). Without the
precheck:
- attach-session fails naturally on a stale tmux name
- the existing reattach + exit-notify path surfaces the failure
- open()/subscribe() can stay sync — no event-loop concern, no async
  cascade through the WS message handler

Net: -64 / +21 in mux-websocket.ts. Reverts the integration test that
relied on the precheck to its pre-PR id-based form.

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

---------

Co-authored-by: Prateek <karnalprateek@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 19:55:10 +05:30
Harshit Singh Bhandari cd6d0292b6
refactor(cli): collapse running/not-running fork via ensureDaemon (PR B.2) (#1626)
* refactor(cli): collapse running/not-running fork via ensureDaemon (PR B.2)

Replaces the three branches that handled "AO is already running" cases in
start.ts (§3.2 URL/path-while-running, §3.3 project-id-while-running,
§3.5 non-human info dump) with a single attach pipeline that runs after
resolveOrCreateProject. The fork between attach and spawn is now a single
post-resolve decision point.

New module packages/cli/src/lib/daemon.ts owns the daemon side of that
fork:

  - attachToDaemon(running) -> AttachedDaemon { port, pid,
    notifyProjectChange() } — pure handle plus a typed
    {ok}|{ok:false,reason} cache-invalidation result, replacing the
    open-coded fetch + try/catch that lived in two places.
  - killExistingDaemon(running) — SIGTERM -> waitForExit -> SIGKILL ->
    unregister, used by the "Restart everything" menu option. Replaces
    the inline restart code in §3.4.

resolve-project.ts gains an opt:

  - { targetGlobalRegistry?: boolean } — when true, fromUrl and fromPath
    register against the global config (the daemon's source of truth)
    rather than into a cwd-local one. fromUrl's "register globally"
    branch is a near-verbatim move of the §3.2 inline clone+register
    block, including the global-registry dedup and the flat-local-config
    write that §3.2 deliberately preferred over fromUrl's wrapped yaml.
    fromCwdOrId short-circuits straight to the global registry for
    project-id args while running.

The new dispatch in start.ts:

  - Running + non-human + (no arg | URL | path) -> info dump, exit 0
    (preserved §3.5 behavior; project-id args still fall through to
    attach+spawn so automation can `ao start <id>` against a live
    daemon).
  - Running + human + no arg -> menu (preserved §3.4: open / quit /
    add / new / restart). "restart" now routes through
    killExistingDaemon and falls through to the spawn path; "new" sets
    startNewOrchestrator and falls through to the attach path.
  - resolveOrCreateProject(arg, deps, { targetGlobalRegistry: !!running })
  - Running -> attachAndSpawnOrchestrator helper (§3.2 short-circuit
    preserved: URL/path arg whose project is already in
    running.projects skips the orchestrator-spawn and just opens the
    dashboard).
  - Not running -> existing runStartup + register + handlers.

attachAndSpawnOrchestrator unifies the §3.2/§3.3 messaging behind a
single justCreated discriminator:

  - justCreated=true (URL clone or path register): "Spawning
    orchestrator session..." -> "Project '...' registered in the global
    config." -> "Orchestrator session ready: ..."
  - justCreated=false (project id or already-registered path):
    "Attaching to running AO instance..." -> "Orchestrator session
    ready: ..." -> "Project '...' reattached to running daemon (PID
    ...)"

Both flows then notifyProjectChange (warns on failure, never throws —
the dashboard might be down), print the lifecycle-attach notice when
the project isn't yet supervised, and either openUrl (human) or print
the URL (non-human).

Subsumes the B.1 follow-up (migrate §3.2 inline clone+register to share
fromUrl): the inline block is deleted outright by the fork collapse and
fromUrl now owns both the not-running and the global-registry cases.

start.ts: -1126 / +131 lines. New daemon.ts: 105 lines. resolve-project.ts:
+167 lines (the global-registry branch + a moved-from-start.ts
detectClonedRepoDefaultBranch helper).

No behavior change. Test count and failure set are identical to
upstream/main; the 8 stop-command failures pre-exist on fad75b63.
8 new daemon.test.ts tests cover attachToDaemon (port/pid wiring,
notifyProjectChange success / non-2xx / fetch-throws) and
killExistingDaemon (SIGTERM happy path, SIGKILL escalation, throw on
both-fail, ESRCH-on-already-dead).

Step 2 of PR B in ao-118's start.ts refactor plan
(~/.ao/agent-orchestrator/ao-118/plan.md). startNewOrchestrator and the
§3.4 menu options remain intact — those land in PR B.3.

* fix(cli): canonicalize paths and guard reload in fromPath global branch

Two review fixes on resolve-project.ts:

1. Restore realpathSync canonicalization in fromPath's global-registry
   branch. The original §3.2 inline block in start.ts canonicalized both
   sides before comparing, so an `ao start /tmp/foo` against a daemon
   whose global config stored /private/tmp/foo (macOS symlink) would
   dedupe correctly. The B.2 collapse used plain resolve() and would
   miss the match, calling addProjectToConfig and double-registering
   the project. New canonicalize() helper mirrors the elsewhere-used
   try-realpathSync-fallback pattern.

2. Add the missing null guard on reloaded.projects[addedId] in the same
   branch, matching fromUrlIntoGlobal's existing guard. addProjectToConfig
   could persist nothing on a write-permission error that doesn't throw,
   in which case the undefined project would propagate into
   generateOrchestratorPrompt with a useless stack trace; an explicit
   "Failed to register" error is what the URL branch already raises.

* fix(cli): scope daemon test spy and correct add-menu comment

Two review fixes:

1. Move the process.kill spy in daemon.test.ts inside beforeEach +
   afterEach (vi.restoreAllMocks). The previous module-scope spy could
   leak into sibling test files when Vitest reuses worker threads,
   silently mocking process.kill in unrelated suites and producing
   confusing failures.

2. Rewrite the misleading comment on the "add" menu branch in start.ts.
   The previous wording claimed the path "intentionally does not register
   globally", but loadConfig() walks up from cwd and returns the global
   config as a canonical fallback — so addProjectToConfig may register
   globally in that common case. The new comment honestly describes the
   canonical-aware behavior and the intentional skip of orchestrator
   spawn (the "add" choice is distinct from "new").
2026-05-04 14:28:07 +05:30
Harshit Singh Bhandari caa7f60a4b
refactor(spawn): plugin-owned preflight + collapse project resolution (#1622)
* feat(core): add PreflightContext + optional preflight() to plugin interfaces

Foundation for PR 2 of the ao spawn refactor: lets plugins own their own
prerequisites instead of the CLI hardcoding 'if runtime === tmux check
tmux' / 'if tracker === github check gh auth' switches.

PreflightContext describes intent (willClaimExistingPR, role) rather
than CLI flag names, so plugins never learn about flags. New flags map
to new intent fields only when a plugin actually needs them.

Adds preflight?(ctx) as an optional method on Runtime, Agent, Workspace,
Tracker, SCM. Backwards-compatible: existing plugins keep working
unchanged. Subsequent commits move checkTmux into runtime-tmux and
checkGhAuth into the github plugins, then update spawn.ts to iterate
selected plugins instead of switching on plugin names.

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

* feat(plugins): implement preflight() in runtime-tmux + tracker-github + scm-github

Each plugin now owns its own prerequisite checks (tmux binary, gh auth)
behind the optional PluginModule preflight() contract added in the
previous commit. The CLI no longer needs to know which plugin needs
which tool — it just iterates the selected plugins.

- runtime-tmux: checks 'tmux -V' and throws with platform-appropriate
  install hint (brew / apt / dnf / WSL)
- tracker-github: checks 'gh --version' and 'gh auth status'
  unconditionally (tracker is exercised on every spawn that has an
  issueId AND on lifecycle polling for issue closure)
- scm-github: same gh auth checks but only when the spawn will exercise
  PR-write paths — gates on context.intent.willClaimExistingPR

Subsequent commit refactors the CLI to iterate plugins instead of
hardcoded 'if runtime === tmux' switches.

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

* refactor(cli): make ao spawn iterate plugin preflight, collapse project resolution

Three small changes bundled because they all touch spawn.ts:

1. Plugin-iterating preflight: replaces the hardcoded
   'if runtime === tmux check tmux' / 'if tracker === github check gh
   auth' switches in runSpawnPreflight with a 4-line loop that walks the
   selected plugins and calls each one's optional preflight(). Plugin
   internals are no longer leaked into the CLI; new plugins only need to
   declare their own preflight.

2. Project-resolution collapse: the prefix/no-prefix and issue/no-issue
   paths previously had three near-duplicate code blocks each with its
   own try/catch around autoDetectProject. Replaced by one
   resolveProjectAndIssue() helper that uses resolveSpawnTarget's
   fallback parameter — caller wraps in a single try/catch.

3. Micro-deletes: drop the unused 'return session.id' in spawnSession
   (callers already ignore it; the SESSION=<id> stdout line is the
   scriptable contract). Drop checkTmux/checkGhAuth from lib/preflight.ts
   (now in their respective plugins) along with their orphaned tests.

LOC: roughly net-zero. Wins are structural — adding runtime-podman /
tracker-jira / scm-bitbucket no longer requires editing spawn.ts.

Pre-existing start.test.ts 'stop command' failures are unrelated (verified
on upstream/main bare).

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

* perf(plugins): dedupe gh-auth check across tracker-github + scm-github

Address greptile P2 on PR #1622: when a project has both tracker:
github and scm: github with --claim-pr, both plugin preflights ran
'gh --version' + 'gh auth status' independently — 4 execs where 2
suffice, and two identical error messages on failure.

Add memoizeAsync(key, fn) to core (process-scoped Promise cache) and
have both github plugins share the key 'gh-cli-auth'. Second caller
hits the in-flight (or resolved) promise — zero extra subprocess
overhead, one error on failure.

Caches both successes and rejections: failed checks should never
re-run within a process (cache dies with the CLI, user fixes the
underlying issue and re-invokes).

5 unit tests for memoizeAsync covering: single-fire dedup, value
identity, distinct keys, rejection caching, concurrent in-flight dedup.

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

* refactor(spawn): collect-all preflight + per-plugin tests + key-namespacing docs

Address self-review feedback on PR #1622:

1. **Collect-all preflight** (spawn.ts): runSpawnPreflight previously
   aborted at the first plugin's failure, so a user with multiple broken
   prereqs (tmux missing AND gh logged out) had to fix-and-retry to
   discover the second one. Now collects every plugin's error and
   reports them together ("2 preflight checks failed:\n  1. ...\n
   2. ..."). Single-failure path is unchanged — that error throws as-is
   without the wrapper. Test added: 'collects every plugin's preflight
   failure into one combined error'.

2. **Drop redundant workspace literal fallback** (spawn.ts):
   DefaultPluginsSchema in core/config.ts applies .default("worktree")
   to workspace, same as runtime/agent. The literal '?? "worktree"'
   was asymmetric defensive theater — dropped to match the runtime/agent
   form.

3. **memoizeAsync key-namespacing convention** (process-cache.ts):
   Added a JSDoc section documenting that two callers using the same
   key get shared state (intentional for cross-cutting checks like
   gh-cli-auth, dangerous for plugin-internal caching). Recommends
   namespacing plugin-internal keys as 'plugin-name:thing'.

4. **Per-plugin preflight unit tests**:
   - runtime-tmux: tmux-present resolves; tmux-missing throws with
     platform-specific install hint (verified per-platform branch)
   - tracker-github: happy path, gh-not-installed, gh-not-authenticated
   - scm-github: no-op when willClaimExistingPR=false (zero gh calls),
     full check when true, plus install/auth failure branches

   Process cache cleared in beforeEach so each test starts fresh.
   Required exporting _clearProcessCacheForTests from core/index.ts
   (matches existing _testUtils pattern in gh-trace.ts).

Pre-existing start.test.ts 'stop command' failures unchanged
(verified on bare upstream/main).

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

* fix(test): collapse duplicate @aoagents/ao-core import in tracker-github test

eslint no-duplicate-imports caught it on CI — combined the value and
type-only imports into one statement.

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

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 14:03:26 +05:30
Harshit Singh Bhandari fad75b6323
refactor(cli): extract resolveOrCreateProject for the not-running path (#1621)
Replaces the per-arg-shape dispatch that lived inline in start.ts (URL,
path, project id, no arg) with a single resolveOrCreateProject(arg, deps)
call. The new module dispatches internally to fromUrl/fromPath/fromCwdOrId
helpers and returns a uniform { config, projectId, project, source,
justCreated, parsed? } shape.

Behavior preserved exactly — fromUrl is a near-verbatim move of
handleUrlStart's body (which is now deleted as it had no other callers),
fromPath mirrors the path branch's loadConfig/autoCreate/addProject
cascade, and fromCwdOrId carries over the registerFlatConfig recovery
path with the same semantics.

Dependencies that live in start.ts (addProjectToConfig, autoCreateConfig,
resolveProject, resolveProjectByRepo, registerFlatConfig, cloneRepo) are
passed in via a ResolveDeps object. This keeps the new module decoupled
from start.ts's other concerns (interactive prompts, agent detection,
project type detection) without creating a circular import.

Scope note: the "AO is already running + URL/path arg" branch in start.ts
still has its own inline clone+register block. That block deliberately
diverged from handleUrlStart (it writes a flat local config, not the
legacy wrapped one) — migrating it to share fromUrl is a small follow-up
before PR B.2 collapses the running-vs-not-running fork.

Step 1 of PR B in the start.ts refactor plan
(~/.ao/agent-orchestrator/ao-118/plan.md). Net diff: start.ts shrinks by
~146 lines, new file adds 294. Test count and failure set are identical
to upstream/main.
2026-05-04 09:44:07 +05:30
i-trytoohard ef8ac42dd4
chore: release 0.4.0 (#1625)
* chore: release 0.4.0

Consume 33 changesets across the linked package group. All public
packages bumped to 0.4.0 and published to npm.

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

* test(agent-codex): bump package-version assertion to 0.4.0

Release gate test was still asserting 0.3.0 after the 0.4.0 bump.

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

* chore: align CHANGELOG headers with @aoagents npm scope

The H1 of every package CHANGELOG.md still read @composio/* from
before the npm scope rename. Body entries that historically reference
@composio/* are left intact — they document what was true at the time
of those releases.

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

---------

Co-authored-by: Prateek <karnalprateek@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 06:57:24 +05:30
Harshit Singh Bhandari 7c7ffb5624
fix(web): source sidebar orchestrator from API field, not session list (#1623)
* fix(web): source sidebar orchestrator from API field, not session list

PR #1615 merged the initial implementation but missed the follow-up
fix. The merged sidebar code looks up the orchestrator inside the
`sessions` prop, but /api/sessions/route.ts strips ALL orchestrators
from that array before returning (they're exposed via a separate
`orchestrators` field on the same response). Result: the new menu
entry — and the existing icon button next to the dashboard icon —
never render for any project.

Replace the broken in-sidebar derivation with a new `orchestrators`
prop on ProjectSidebar. Each parent passes the data it already has:

  - Dashboard.tsx: passes `activeOrchestrators` (already in scope)
  - PullRequestsPage.tsx: passes `orchestratorLinks` (already in scope)
  - sessions/[id]/page.tsx: stores the `orchestrators` field already
    returned by /api/sessions and threads it through SessionPageShell
    and SessionDetail as `sidebarOrchestrators`

Tests refocused: the sidebar now just renders what the prop says, so
the live-vs-terminal selection lives in the API
(selectPreferredOrchestratorId) and is no longer the sidebar's
responsibility.

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

* style: fix indentation on sidebarOrchestrators prop in SessionPage

Addresses Greptile review comment on PR #1623.

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

* refactor(web): use DashboardOrchestratorLink for API response type

Addresses non-blocking review feedback on PR #1623. The /api/sessions
response actually returns DashboardOrchestratorLink shape (which
includes projectName), so use that as the response type instead of
the narrower ProjectSidebarOrchestrator. The sidebar prop type stays
narrow on purpose — it's the minimum the sidebar needs to render.

Also document the "one orchestrator per project" Map invariant.

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

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 00:09:43 +05:30
Harshit Singh Bhandari b98d8eca8e
refactor(cli): extract preflight + shutdown from start.ts (PR A) (#1617)
* refactor(cli): extract runtime preflight and install helpers from start.ts

Pulls scattered preflight logic out of start.ts into two focused modules:

- lib/install-helpers.ts: shared install primitives (askYesNo,
  runInteractiveCommand, tryInstallWithAttempts, genericInstallHints,
  canPromptForInstall, InstallAttempt) — used by ensureGit/ensureTmux,
  the agent runtime installer, and the optional gh install path.

- lib/startup-preflight.ts: the runtime checks themselves (ensureGit,
  ensureTmux, warnAboutLegacyStorage, warnAboutOpenClawStatus) plus a
  top-level runtimePreflight(config) that orchestrates tools + state
  warnings + idle-sleep + OpenClaw credentials. Distinct from the
  existing lib/preflight.ts, which validates dashboard build artifacts.

start.ts now calls runtimePreflight(config) once at the top of
runStartup instead of inlining 32 lines of orchestration; ensureGit is
imported for the three callsites that still need it directly (URL
clone, addProjectToConfig, URL-while-running branch).

No behavior change. Test count and failure set are identical to
upstream/main (8 pre-existing failures in the stop command tests).

Step 1 of PR A in the start.ts refactor plan
(~/.ao/agent-orchestrator/ao-118/plan.md). start.ts shrinks by 301
lines; net +55 LOC across the touched files (the abstractions cost
some interface overhead, as expected).

* refactor(cli): extract shutdown handler from start.ts

Moves the SIGINT/SIGTERM handler out of runStartup's closure into
lib/shutdown.ts as installShutdownHandlers({ configPath, projectId }).
The handler logic is identical: stop lifecycle workers, kill all
active sessions, record last-stop state for restore on next ao start,
unregister from running.json, await the bun-tmp janitor's final
sweep, then exit with the right code (130 for SIGINT, 0 for SIGTERM).

Also drops three now-unused start.ts imports (stopProjectSupervisor,
stopAllLifecycleWorkers, stopBunTmpJanitor) — they're consumed inside
the new shutdown module.

Note: the equivalent kill-and-record loop in `ao stop` is left
untouched. It has different verbosity (spinner, warnings, per-project
output) and different options (--purge-session) than the signal
handler. Unifying them is a behavior-shaping change that belongs with
the daemon/stop restructure in PR B, not this mechanical extraction.

Step 2 of PR A in the start.ts refactor plan
(~/.ao/agent-orchestrator/ao-118/plan.md). Test count unchanged (8
pre-existing stop-command failures from upstream/main, same set).

* refactor(cli): address PR review — idempotent shutdown + correct legacy count

Two P2 review comments from greptile-apps[bot]:

1. shutdown.ts: the 'idempotent' JSDoc claim was unbacked — shuttingDown
   was a per-invocation closure, so calling installShutdownHandlers
   twice would register duplicate listeners. Add a module-level
   handlersInstalled guard and hoist shuttingDown to module scope so
   the abstraction matches its docstring.

2. startup-preflight.ts: warnAboutLegacyStorage gates on the count of
   non-empty hash dirs but printed hashDirs.length (total). Pre-existing
   bug in the original start.ts code — a user with mostly-empty hash
   dirs would see an inflated migration count. Renamed sessionCount →
   nonEmptyDirCount (the variable always counted dirs, not sessions)
   and used it in the message.
2026-05-03 22:43:26 +05:30
Harshit Singh Bhandari 71253105cd
refactor(core): replace spawn rollback ladder with CleanupStack (#1616)
* refactor(core): replace spawn rollback ladder with CleanupStack

Replace the four nested try/catch + cleanupSpawnWorkspaceAndMetadata
helper in _spawnInner with a single LIFO CleanupStack. Each side
effect (reserved metadata, workspace, prompt files, runtime handle)
pushes its undo as soon as the resource exists; on success we
dismiss(), on failure we runAll().

Why: adding a new spawn step previously required extending every
prior cleanup block. Easy to forget; no compiler check. The stack
makes rollback structural — a new step pushes one cleanup, no risk
of leaving prior resources behind. runAll() is fault-tolerant by
design: a throwing cleanup never short-circuits the rest.

Behavior is preserved. Adds characterization tests for the worker
spawn rollback paths (none existed before — only spawnOrchestrator
was covered):
  - workspace.create failure cleans reserved metadata
  - runtime.create failure destroys worktree + cleans metadata
  - postLaunchSetup failure destroys runtime + worktree + metadata
  - one cleanup throwing does not skip subsequent cleanups

Refs #1603 (PR 1 of the ao spawn refactor plan).

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

* refactor(core): make CleanupStack runAll() terminal, symmetric with dismiss()

Address review feedback on PR #1616: previously `dismiss()` → `push()` was
a documented no-op but `runAll()` → `push()` would silently queue cleanups
that fired on a subsequent `runAll()`. Asymmetric and surprising.

Set `this.dismissed = true` at the top of `runAll()` so both terminal
states (success via dismiss, failure via runAll) reject further pushes
identically. Add a regression test pinning the new symmetric behavior.

The "idempotent runAll" test continues to pass (early-return path now
fires via the dismissed flag instead of the empty-stack short-circuit).

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

* refactor(core): surface CleanupStack errors and cover postCreate rollback

Address review feedback on PR #1616:

- Pass an onError callback to cleanupStack.runAll() in _spawnInner that
  logs cleanup failures via console.error. The previous /* best effort */
  pattern silently swallowed errors during rollback; now the same errors
  are surfaced for debugging without changing behavior (cleanup errors
  still don't propagate, subsequent cleanups still run).
- Add a characterization test for the workspace.postCreate failure path.
  This was the only rollback path without a test — the stack handled it
  correctly already, but pinning it down prevents regression.

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

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-03 22:43:21 +05:30
Harshit Singh Bhandari e465a4702d
fix(agent-claude-code): fold underscores in Claude project slug (#1611) (#1612)
`toClaudeProjectPath` only normalized `/`, `.`, and `:` — but Claude Code's
on-disk slug also folds underscores (and other non-alphanumerics) to `-`.
AO project data dirs are named `<sanitized>_<hash>` (e.g.
`graph-isomorphism_d185b44d56`), so the slug AO computed pointed at a
directory that never existed. Cascading failures:

- `getSessionInfo` couldn't read the JSONL → `claudeSessionUuid` never
  got persisted to session metadata.
- On restore, `getRestoreCommand`'s metadata lookup found nothing AND its
  workspace-scan fallback also missed (same bad slug), returning `null`.
- Session-manager's native-restore guard then threw
  `SessionNotRestorableError` → API returned 409.

Verified on-disk: every session under projects without underscores has
`claudeSessionUuid` persisted; every session under projects with
underscores does not. The orchestrator angle in #1611 is the loudest
symptom — orchestrators die early so they have nothing else to fall back
on — but the same bug silently broke worker restore in any multi-project
setup.

Fix: replace `[/.]` with `[^a-zA-Z0-9-]` in the slug regex, matching
Claude Code's actual encoding. Adds direct unit tests for
`toClaudeProjectPath` covering the underscore case plus existing paths,
and a regression test in the `getSessionInfo` path-conversion suite.

Fixes #1611.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-03 20:40:18 +05:30
Harshit Singh Bhandari 3b9ba3122e
feat(web): add 'Open orchestrator' to sidebar 3-dot menu (#1615)
* feat(web): add 'Open orchestrator' to sidebar 3-dot menu

Adds a labeled menu entry above 'Project settings' that navigates to
the project's orchestrator session. The orchestrator is the most-used
session in any project, but today the only path to it is the unlabeled
icon button next to the dashboard icon - easy to miss for new users.

The entry is hidden when no live orchestrator exists (matching the
existing icon-button pattern), so the menu shrinks gracefully on
projects where 'ao start' has never run or has stopped.

Closes #1613

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

* refactor(web): drop redundant guard in orchestrator menu render

Hold the validated session in `liveOrchestrator` instead of a separate
boolean flag. TypeScript narrows automatically from the assignment, so
the render condition no longer needs `&& orchestratorSession` to satisfy
the type checker.

Addresses review feedback on #1613.

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

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-03 20:39:57 +05:30
Adil Shaikh 703d5844f0
fix(core): deliver enriched review content on changes_requested transition (#1578)
* fix(core): deliver enriched review content on changes_requested transition

The transition reaction for changes_requested sent a generic message
("Details will follow shortly") but the backlog dispatch that carries
the actual review comment bodies was blocked by its own deduplication
logic — the transition handler recorded the fingerprint hash as
"dispatched" without ever sending the enriched content.

Remove the premature hash recording and the transition guard so the
backlog dispatch fires in the same poll cycle, delivering actual review
comment details (file paths, line numbers, authors, bodies) to the
agent immediately.

Closes #1558

* fix(core): update stale comments from review feedback

- Update throttle-bypass comment to reflect removed Branch B
- Fix test comment: second check is throttled, not just fingerprint match

* fix(core): prevent double-billing reaction attempts on changes_requested transition

When a changes_requested transition fires, the transition handler calls
executeReaction (attempt 1) and then maybeDispatchReviewBacklog calls it
again for the enriched message (attempt 2). With retries:1, this caused
premature escalation on the very first transition poll.

Fix: when the transition handler already fired executeReaction for the
same reaction key, send the enriched payload directly via
sessionManager.send — bypassing the reaction tracker entirely.

Also moves lastReviewBacklogCheckAt after the SCM fetch so a failed
getReviewThreads call doesn't block retries for 2 minutes.

Fixes #1578

* fix(core): gate review bypass on send-to-agent action type

The direct sessionManager.send bypass (introduced to prevent double-billing
reaction attempts) fired unconditionally, ignoring reactionConfig.action.
With action: "notify", the enriched review content was pushed to the agent's
stdin instead of routing through notifyHuman.

Gate the bypass on action === "send-to-agent" so notify configs fall through
to executeReaction which routes correctly. Applied to both human and
automated review comment paths.

Adds test verifying action: "notify" does not call sessionManager.send
and does fire the notifier.

Fixes #1578
2026-05-03 18:30:47 +05:30
Adil Shaikh cf5a418a48
fix(scm-github): silence HTTP 304 warnings in ETag guards (#1581)
* fix(scm-github): silence HTTP 304 warnings in ETag guards and use observer logging

ETag guard functions and the GraphQL batch handler used raw console.warn()/
console.error() that fired on every poll cycle, including expected HTTP 304
(Not Modified) responses. This flooded the orchestrator terminal with noise.

- Add 304 fallback check in error message for cases where gh CLI doesn't
  populate stdout/stderr on non-zero exit
- Migrate all 4 raw console.warn()/console.error() calls to observer?.log()
  for consistency with the rest of the file
- Thread observer parameter through shouldRefreshPREnrichment and the three
  ETag guard functions (checkPRListETag, checkCommitStatusETag,
  checkReviewCommentsETag)
- Update test to verify observer-based logging instead of console spy

Closes #1580

* fix(scm-github): use word boundary in 304 regex to prevent false positives

Use \b304\b instead of /304/ to avoid matching substrings like "3040"
or "30400" in error messages.

Closes #1580

* fix(scm-github): use is304() for error message fallback and thread observer into getReviewThreads

1. Replace \b304\b regex with is304() (anchored to HTTP status line) in all
   three ETag guard fallback paths. Prevents false positives from URL paths
   like "pulls/304/comments" appearing in Node's execFile error messages.

2. Capture instance-level observer in createGitHubSCM() and pass it to
   checkReviewCommentsETag and getReviewThreads error logging. Non-304
   errors on the review polling path are now visible via observer instead
   of being silently swallowed by lifecycle's catch.

3. Restore "error" severity for partial batch failures in
   enrichSessionsPRBatchImpl (was incorrectly downgraded to "warn").

4. Add tests for Guard 1 URL false-positive, Guard 2 error logging,
   and Guard 2 HTTP 304 fallback paths.

Closes #1580

* test(scm-github): add Guard 3 (checkReviewCommentsETag) tests

Add 5 tests for the review comments ETag guard covering:
- 200 response (changed) and 304 response (unchanged)
- Error with observer logging
- HTTP 304 in error message treated as cache hit
- URL containing "304" NOT treated as cache hit (false-positive prevention)

This completes the test coverage for all three ETag guard functions'
error and 304-fallback paths, as requested in review.

Closes #1580
2026-05-03 18:30:27 +05:30
Ashish Huddar ab65d12356
Fix native restore fallback for Claude and Codex sessions (#1602)
* Fix native session restore fallback for Claude and Codex

* Address restore metadata review comments

* Fix metadata normalization lint

* Address PR metadata review feedback

* Prevent fresh restore fallback for native agents
2026-05-01 21:27:17 +05:30
Ashish Huddar 2306078761
feat: add SQLite-backed activity event logging layer (#1528)
* feat: add SQLite-backed activity event logging layer

Implements a structured diagnostic event trail for the orchestrator.
When unexpected behavior occurs (stuck sessions, silent CI failures,
missed PR transitions), operators can now reconstruct timelines with
`ao events` rather than guessing from logs.

Key design decisions driven by Codex review:
- FTS5 external-content table uses INSERT/DELETE triggers so search
  actually works without manual rebuild
- ts_epoch (epoch ms) used for all time comparisons to avoid text vs
  SQLite datetime() ambiguity near cutoff
- PRAGMA busy_timeout=3000 handles WAL lock contention across
  CLI/lifecycle/web processes
- user_version schema versioning for future migrations
- EventType renamed to ActivityEventKind to avoid collision with
  existing types.ts export
- Event names match existing vocabulary (ci.failing, review.pending)
- ActivityStateCache (Map) tracks previous activity state so
  lifecycle-manager can emit activity.transition diffs
- session.spawn_failed captures failed spawns via wrapper try/catch
- better-sqlite3 in optionalDependencies: AO keeps working if native
  build fails; getDb() returns null and writes become no-ops

New files:
- packages/core/src/events-db.ts — lazy DB init, WAL, schema+triggers
- packages/core/src/activity-events.ts — write API, sanitizer
- packages/core/src/query-activity-events.ts — query + FTS + stats
- packages/cli/src/commands/events.ts — `ao events list/search/stats`

Wired into:
- lifecycle-manager.ts: lifecycle.transition + activity.transition
- session-manager.ts: session.spawned, session.spawn_failed, session.killed

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

* fix(activity-events): address PR review comments

- redactValue: remove value-string regex check that silently erased
  error messages mentioning auth terms (key-based redaction is sufficient)
- sanitizeData: reject payloads >16KB instead of slicing (sliced JSON
  is malformed and corrupts JSON output)
- lifecycle-manager: prune activityStateCache alongside states in the
  per-poll stale-entry cleanup loop (prevents unbounded growth)
- events CLI: warn on unrecognised --since duration format instead of
  silently applying no time filter
- searchActivityEvents: accept optional projectId and push it into SQL
  WHERE clause instead of post-LIMIT in-memory filter

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

* fix(activity-events): review pass fixes

- formatRow: pad level string before chalk-wrapping so ANSI codes
  don't corrupt column alignment in ao events output
- events-db: add PRAGMA synchronous=NORMAL (WAL+NORMAL is standard
  recommendation; avoids per-write fsync in the poll hot path)
- events-db: emit console.warn when _dbFailed is set so operators know
  events are being dropped instead of failing silently forever
- activity-events: remove dead redactValue wrapper (was a no-op after
  the previous fix; call site now directly assigns the value)
- activity-events: remove unused session.cleanup from ActivityEventKind
  (no call site emitted it; dead API surface)
- query-activity-events: add optional limit param to searchActivityEvents
  (default 100, max 1000, parameterized); add --limit flag to ao events search

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

* fix(activity-events): widen source/kind to allow plugin-defined values

ActivityEventInput.source and .kind accept ActivityEventSource|string
and ActivityEventKind|string respectively, so new event sources (e.g.
scm-github, notifier-slack) don't require editing core types.

The named union members are preserved for IDE autocomplete on known values.

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

* fix(events): level column alignment, filter kind widening, negative limit guard

- events.ts: padEnd(5) → padEnd(9) so level column aligns with the 9-char LEVEL header
- query-activity-events.ts: ActivityEventFilter.kind widened to ActivityEventKind|string
  for consistency with ActivityEventInput.kind (plugin-defined kinds can now be queried)
- query-activity-events.ts: negative/NaN limit values sanitized with Number.isFinite +
  Math.max(1,...) to prevent SQLite LIMIT -N returning the full table

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

* fix(events): FTS alias, credential URL sanitization, periodic retention

- query-activity-events.ts: use full table name in MATCH (activity_events_fts
  MATCH ?) instead of alias to avoid 'no such column: fts' in some FTS5 builds
- activity-events.ts: redact https://token@host URL credentials in string values;
  add hourly retention sweep so long-lived processes don't grow DB indefinitely
- events-fts-integration.test.ts: real SQLite integration test for FTS5 search,
  projectId filter, and epoch-based time filter

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

* fix(lint): remove inline import() type annotations in integration test

ESLint @typescript-eslint/consistent-type-imports forbids import() in type
positions — replaced with any to keep the test working without the type imports.

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

* fix(events): parse data to structured JSON in --json output; add test proving value sanitizer correctness

- ao events list/search --json now parses ActivityEvent.data from JSON
  string back to a structured object, making it jq-friendly without
  requiring double-parsing by callers (P1 finding from PR review)
- Add test "preserves error messages that mention sensitive words in
  values" to refute Greptile's false-positive P1 finding: SENSITIVE_KEY_RE
  only matches key names, not string values; "token expired" and
  "authorization header missing" values are preserved correctly

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

* fix(events): BM25 relevance ordering for FTS search; add versioned JSON envelope

Search now orders by FTS5 rank (BM25) instead of ts_epoch, returning most relevant
events first. --json output now wraps events in { version, query, meta, events }
for stable CLI contracts.

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

* fix(events): quote FTS tokens to prevent false negatives on operator-like terms

Searching for words like "OR", "AND", or "NOT" would produce empty results
because SQLite FTS5 treated them as operators after token joining. Wrapping
each token in double quotes forces literal phrase matching.

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

* fix(tests): update FTS assertion for quoted tokens; raise timeout on slow audit test

query-activity-events test expected the old unquoted join format; update to
match the quoted form added in the previous commit. The agent-report audit
trail test occasionally exceeds the 5 s default on CI runners; raise to 15 s.

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

* fix(qa): ISSUE-001 - type events stats entries

* test(events): cover session and lifecycle event call sites

* test(core): wait for lifecycle branch adoption

* chore: add activity events changeset

* fix activity event review feedback

* batch prune old activity events

* record activity event when spawn starts

* Fix activity event FTS rebuild and sanitization guard

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-01 21:13:20 +05:30
Ashish Huddar 9ca3c1fcd7
fix: force launcher relink during update (#1594)
* fix: force launcher relink during update (#1591)

* fix: address launcher refresh review feedback (#1591)

* fix: improve launcher refresh diagnostics (#1591)
2026-05-01 17:09:58 +05:30
Chirag Arora b2cdf7adab
fix(web): scope terminal tmux resolution by project (#1551)
* fix(web): scope terminal resolution by project

* fix(web): avoid suffix false-positives in project-scoped tmux key match

Made-with: Cursor

* fix(web): scope fullscreen terminal resize by project

Made-with: Cursor
2026-05-01 16:15:43 +05:30
Ashish Huddar 0a9ba4cd7f
fix: protect live dashboard artifacts (#1598)
* fix: protect live dashboard artifacts (#1589)

* fix: address dashboard artifact review (#1589)

* fix: handle dashboard rebuild port reassignment (#1589)
2026-05-01 16:09:57 +05:30
Ashish Huddar e94ff28106
fix(cli): supervise lifecycle workers for active projects (#1600)
* Refine issue checklist for dynamic lifecycle supervisor

* Harden project supervisor reconcile handling

* fix(cli): surface supervisor startup failures

* fix(cli): allow missing global config on startup
2026-05-01 15:45:06 +05:30