* feat(core): add CI failure summary SCM contract
Add an optional SCM getCIFailureSummary hook and shared CIFailureSummary shape so providers can return failed jobs, failed steps, run URLs, and bounded log tails without changing existing SCM implementations.
* feat(scm-github): summarize failed CI logs
Use failed GitHub check URLs to locate Actions run/job IDs, fetch gh run view --log-failed output, parse the step column from the gh log format, and cap each failed-job log tail at 120 lines. Return null when no failed run logs can be fetched so lifecycle can fall back cleanly.
* feat(lifecycle): send actionable CI failure details
Prefer SCM CI failure summaries when composing ci-failed agent messages, including failed job/step, run URL, and log tail. Fall back to check names/statuses for SCM plugins without getCIFailureSummary, and remove the generic default ci-failed text so default handling relies on the lifecycle-composed payload.
State invariants preserved: this only changes ci-failed reaction payload composition and dispatch hashing. It does not change lifecycle status decisions, state-machine transitions, ci-failed persistence, retry/escalation thresholds, or stable-passing tracker reset semantics.
* fix(lifecycle): harden CI failure summaries
Address review feedback: escape log-tail lines that could close markdown fences, report the last failed-step column from gh failed logs, and pass already-known failed checks into getCIFailureSummary to avoid duplicate CI check fetches.
State invariants preserved: lifecycle transition decisions, retry/escalation budgets, persistent ci-failed tracker behavior, and stable CI passing reset semantics remain unchanged.
* refactor(lifecycle): centralize CI failure check lookup
Deduplicate CI failure check lookup, filtering, and fingerprint generation across transition-time enrichment and follow-up CI-failure dispatch while preserving the existing fetch/no-fetch split.
State invariants preserved: lifecycle transition decisions, reaction retry/escalation behavior, ci-failed tracker persistence, and stable-passing reset semantics are unchanged.
* fix(scm-github): fetch in-progress failed job logs
* fix(lifecycle): label CI failure URLs explicitly
---------
Co-authored-by: i-trytoohard <193449657+i-trytoohard@users.noreply.github.com>
* fix: project builds on Windows
Replace Unix cp -r with Node.js fs.cpSync in CLI build script.
Add webpack snapshot config to prevent Next.js scanning Windows junction points.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(core): add cross-platform adapter (platform.ts)
Centralizes all platform-branching logic: shell resolution (pwsh > powershell > cmd),
process tree kill (taskkill on Windows), port-based PID discovery (netstat on Windows),
runtime defaults, and env defaults.
Addresses blockers B05, B06, B07, B08 from Windows compatibility proposal.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(core): platform-aware runtime default (tmux on Unix, process on Windows)
B04: Config and docs now reflect platform-specific defaults.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(cli): use platform-aware runtime fallback in start command
B01/B02: ensureTmux() is only called when runtime resolves to 'tmux'.
On Windows, runtime defaults to 'process', skipping tmux entirely.
* fix(core): tighten Windows PID port matching
* test(core): add mocked tests for platform.ts to fix diff coverage
Adds platform.mock.test.ts with 25 tests covering Windows-specific
branches (resolveWindowsShell fallbacks, killProcessTree, findPidByPort)
and Unix error-handling/env-var fallback chains that require mocking
node:child_process. Pushes platform.ts diff coverage from 45.9% to ≥80%.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(core): fix TypeScript errors in platform.mock.test.ts
Return ChildProcess from execFile mock implementations and use "" instead
of undefined for execFileSync mock return value to satisfy strict types.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(windows): cross-platform process management (PR 2/6) (#1028)
Fixes B05 and B06: replaces all Unix-only process management with the
platform adapter so AO works on Windows with runtime: process.
- dashboard stop/rebuild uses findPidByPort() (netstat on Windows, lsof on Unix)
- runtime-process destroy uses killProcessTree() (taskkill /T /F on Windows,
negative-PID SIGKILL on Unix) with conditional detached flag
- start.ts restart, ao stop --all, and stop-dashboard paths all switched
from process.kill() to killProcessTree() so child processes are reaped
- lifecycle-service stopLifecycleWorker() replaced with killProcessTree()
- Fixed destroy() hang: exit listener now registered before awaiting kill
so fast-exiting processes don't fire before the listener attaches
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(cli): spawn dashboard with detached:true on Unix for process group kill
Dashboard was spawned with detached:false, so killProcessTree's
process.kill(-pid) failed with ESRCH (not the group leader) and fell
back to killing only the listening process, orphaning Next.js workers.
Matches the runtime-process pattern: detached:!isWindows() makes the
dashboard the process group leader on Unix so the negative-PID group
kill in killProcessTree correctly reaps all children. On Windows,
detached:false is preserved — taskkill /T /F handles the tree kill
by PID regardless of process group membership.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(cli): forward SIGINT/SIGTERM to dashboard group; restore lifecycle stop semantics
Two Bugbot issues from the detached-dashboard and killProcessTree changes:
1. Dashboard Ctrl+C orphan: with detached:true on Unix, the dashboard is in
its own process group and does not receive SIGINT from the terminal. Add
process.once(SIGINT/SIGTERM) handlers that forward the signal via
killProcessTree so the entire dashboard group is reaped on exit. Handlers
are cleaned up when the dashboard exits to avoid leaks.
2. stopLifecycleWorker wrong return value: killProcessTree swallows ESRCH,
so a stale PID entry (process already dead) now returned true ("stopped")
instead of false ("not running"). Add an isProcessRunning guard before
the kill to restore the original semantics.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(cli): prevent double SIGTERM dispatch when both SIGINT and SIGTERM arrive
The forward handler now self-removes both listeners before calling
killProcessTree, so a second signal cannot invoke it again on an
already-dead PID.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(core): respect signal parameter in killProcessTree on Windows
SIGTERM now uses taskkill /T /PID (WM_CLOSE, graceful) instead of /F,
preserving the SIGTERM→wait→SIGKILL escalation used by lifecycle-service,
start.ts, and runtime-process. SIGKILL keeps /T /F /PID (force).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(core): update killProcessTree Windows tests for signal-aware taskkill
Split the single taskkill test into two: SIGTERM uses /T /PID (graceful)
and SIGKILL uses /T /F /PID (force), matching the updated implementation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(core,cli): add missing coverage for platform.ts and lifecycle-service
- platform.mock.test.ts: cover Windows getEnvDefaults PATH fallback (line 148)
- lifecycle-service.test.ts: cover stopLifecycleWorker stale-PID path,
normal SIGTERM kill, and SIGKILL escalation after timeout
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(cli): suppress consistent-type-imports lint error in test mock factory
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(agents): add process-runtime PID check to isProcessRunning (#1031)
B13 (P0): All 4 agent plugins now check PID directly when runtime is
'process' instead of only scanning ps -eo for tmux TTYs. Enables correct
dashboard activity state on Windows.
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(windows): platform-aware shell for postCreate, script-runner, symlinks (PR 4/6) (#1032)
fix(windows): platform-aware shell for workspaces and script-runner (B07, B08, B19)
B07: workspace-worktree and workspace-clone use getShell() instead of sh -c.
B08: script-runner spawns scripts in file-mode on Unix (so $1/$2/$3 reach
positional args) and uses getShell() on Windows; AO_BASH_PATH override
uses || so empty string is treated as unset.
B19: workspace-worktree symlink falls back to cpSync on Windows.
Also fixes path separator check in workspace-worktree to use path.sep
instead of hardcoded "/" for correct Windows behaviour.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(test): use mockReturnValueOnce for pwsh shell mock to prevent test pollution
vi.clearAllMocks() clears call history but not mockReturnValue implementations.
The Windows pwsh shell tests were polluting subsequent tests that expected the
default sh mock. Changed to mockReturnValueOnce so the override is consumed
by the single call and subsequent tests get the default sh implementation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(cli): script-runner always uses bash on Unix, getShell() only on Windows
getShell() on Unix returns process.env.SHELL || /bin/sh, which may be zsh,
fish, or plain sh. When a script file is passed as an argument to these
shells (file mode), the #!/bin/bash shebang is ignored and bash-specific
syntax in ao-doctor.sh / ao-update.sh / setup.sh breaks.
Fix: hardcode bash on Unix (AO_BASH_PATH still overrides it), use getShell()
only on Windows where bash is unavailable.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(core): use lazy factory for Zod runtime default
z.string().default(getDefaultRuntime()) evaluates getDefaultRuntime() once
at module load time, creating hidden coupling between import order and
platform detection. Using a factory function ensures the default is resolved
lazily each time it is needed, making the intent explicit.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(cli): add detached: !isWindows() to dashboard spawn for correct process cleanup
Without detached:true on Unix, killProcessTree(pid) calls process.kill(-pid)
which targets a process group the child never owns — ESRCH causes fallback to
direct kill, leaving grandchild processes alive. Matches the pattern already
used in start.ts lines 782 and 795.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(cli): forward SIGINT/SIGTERM to detached dashboard child on Unix
Detached children run in their own process group, so Ctrl+C does not reach
them. Without forwarding, the dashboard holds the port after the parent exits.
Matches the identical pattern in start.ts lines 1154-1165.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(core): guard killProcessTree against pid <= 0
On Unix, -0 === 0 in JS, so killProcessTree(0) would call process.kill(0)
which sends the signal to every process in the calling process's group,
killing AO itself. findPidByPort can return "0" since it passes the
truthiness check and the /^\d+$/ regex. Guard pid <= 0 at the top of
killProcessTree and return early.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(windows): Node.js metadata wrappers + Claude Code hook + pwsh shell (squash merge PR5)
Squash merges feat/windows-hooks-and-launch (PR #1033) into PR1.
Blockers addressed:
- B16: ~/.ao/bin/gh and git wrappers are now Node.js scripts + .cmd shims on
Windows (bash on Unix unchanged). WRAPPER_VERSION bumped to 0.3.0 to force
reinstall.
- B17: Claude Code PostToolUse hook uses JSON.parse/fs built-ins on Windows
instead of bash+jq+grep+sed. Atomic writes via temp file + renameSync.
chmod skipped on Windows.
- B18: runtime-process uses getShell().cmd + shellInfo.args() instead of
shell:true (which resolves to cmd.exe on Windows). getShell() returns
pwsh > powershell.exe > cmd.exe on Windows, bash on Unix.
Conflict resolution:
- runtime-process spawn: kept PR5's getShell() approach (B18 fix), removed
shell:true which PR1 had as a placeholder.
- runtime-process destroy: kept PR1's cleaner killProcessTree delegation
(PR5 had inline platform-branching written before killProcessTree was
integrated into this worktree).
- Tests: merged PR5's new Windows compatibility tests, adjusted assertions to
match PR1's killProcessTree(pid, signal) API. Fixed Windows compat tests
that expected old spawn(launchCommand, opts) signature.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(core): deduplicate Node.js wrapper updateAoMetadata and add AO_DATA_DIR path validation
- Extract shared NODE_UPDATE_AO_METADATA constant used by both gh/git wrappers
- Add AO_DATA_DIR validation matching bash ao-metadata-helper.sh (must be under ~/.ao/, ~/.agent-orchestrator/, or tmpdir)
- Bump WRAPPER_VERSION to 0.5.0 to force reinstall with new security check
- Update version in agent-codex and core tests to match 0.5.0
- Fix CI test failures from WRAPPER_VERSION bump (0.2.0 → 0.4.0 → 0.5.0)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(core): add IPv6 netstat test for findPidByPort + mark _resetShellCache as @internal
- Add test case for IPv6 LISTENING entries ([::]:3000) in Windows netstat output
- Add @internal JSDoc to _resetShellCache to clarify it is test-only
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(cli): use -File mode on Windows for script-runner so positional args are forwarded
PowerShell -Command does not forward argv elements after the command string to the
script — they are treated as top-level PowerShell args and silently dropped. Using
-File passes remaining args as positional parameters ($1, $2, …) to the script, so
e.g. `ao doctor --fix` correctly reaches the script on Windows.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(cli,core): remove unused getEnvDefaults export, add SIGKILL fallback in signal forward handler
- Remove getEnvDefaults from @composio/ao-core public API — no production callers;
the function remains in platform.ts for future use (B10/B11)
- Add 5 s SIGKILL fallback in dashboard.ts and start.ts forward() handlers so the
parent process cannot hang indefinitely if the child ignores SIGTERM
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(cli): extract forwardSignalsToChild utility, fix ESM build script, reject Windows absolute symlinks
- Extract forwardSignalsToChild(pid, child) into shell.ts — eliminates verbatim
duplication of the SIGTERM/SIGKILL forwarding logic between dashboard.ts and start.ts
- Fix build script: replace node -e "require(...)" with node --input-type=commonjs -e
"require(...)" — required because package is "type":"module" and require is not
available in node -e by default in ESM context
- Fix duplicate import in shell.ts (no-duplicate-imports lint error)
- Reject Windows drive-letter (C:\) and UNC (\server\share) paths in symlink
validation — previously only Unix absolute paths starting with "/" were blocked
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(cli): clear SIGKILL fallback timer when child exits cleanly
If the child exits before the 5-second escalation window, the fallback
setTimeout would still fire and call process.exit(1). Track the timer
in the outer scope so the child.once("exit") handler can cancel it.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix: resolve two open bugbot issues on windows-platform-adapter PR
- fix(platform): always use /bin/sh on Unix instead of \$SHELL so
postCreate commands and runtime launches work correctly when the
user's login shell is non-POSIX (fish, nushell, etc.)
- fix(script-runner): detect cmd.exe fallback on Windows and throw a
clear, actionable error pointing to AO_BASH_PATH rather than passing
the PowerShell-specific -File flag to cmd.exe (which produces a
cryptic error and still can't run bash scripts)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(cli): gate lifecycle worker detached flag behind !isWindows()
Spawning with detached: true unconditionally creates a new console
window on Windows. Use the same !isWindows() pattern established
elsewhere in this PR so the process group behaviour is correct on
both platforms. killProcessTree already uses taskkill /T on Windows
so cleanup is unaffected.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(cli): require AO_BASH_PATH for all Windows shells, not just cmd.exe
pwsh and powershell.exe cannot run bash scripts any more than cmd.exe
can — shebangs are ignored and bash-specific syntax fails. The previous
guard only matched cmd.exe, allowing pwsh (the most common Windows
fallback) to silently invoke -File on a bash script and produce a
confusing PowerShell syntax error.
Simplify to: throw on any Windows shell without AO_BASH_PATH. Also
removes the now-dead getShell() call and -File code path from
script-runner.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(core): always force-kill on Windows; fix stale .cjs JSDoc
killProcessTree: drop the SIGTERM-without-/F branch on Windows.
taskkill without /F sends WM_CLOSE which is unreliable for headless
Node.js console processes — they may silently survive, leaving orphaned
processes. Always pass /F so termination is guaranteed. Callers that
do SIGTERM→wait→SIGKILL escalation are unaffected: SIGKILL simply
finds the process already dead.
agent-workspace-hooks: correct JSDoc that still said <name>.js after
the extension was changed to .cjs (forced CJS mode).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(cli): remove trivial findRunningDashboardPid wrapper
The function was a one-line pass-through to findPidByPort with no added
logic. Callers now import findPidByPort from @composio/ao-core directly.
waitForPortFree calls findPidByPort inline. Deleted the test file that
only tested the pass-through.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(cli): make forwardSignalsToChild idempotent via WeakSet guard
Prevents duplicate SIGINT/SIGTERM handlers if called more than once for
the same ChildProcess — avoids double killProcessTree and racing
process.exit(1) from stacked SIGKILL fallback timers.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(agent-claude-code): three Windows correctness fixes
C-1: Add AO_DATA_DIR allowlist validation to METADATA_UPDATER_SCRIPT_NODE.
The Node.js PostToolUse hook now validates AO_DATA_DIR against
~/.ao/, ~/.agent-orchestrator/, and os.tmpdir() before writing —
matching the protection already in ao-metadata-helper.sh and the
Node.js wrappers in agent-workspace-hooks.ts.
I-1: Guard getCachedProcessList() against Windows. ps -eo pid,tty,args
is Unix-only; the guard makes the intent explicit and avoids a
spurious execFile call when a stale tmux handle is encountered on
Windows.
I-2: Read systemPromptFile content synchronously on Windows instead of
using $(cat ...) bash command substitution, which is not understood
by PowerShell or cmd.exe.
Tests added for all three fixes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(agents): apply Windows correctness fixes to aider, codex, opencode
Mirrors the fixes already applied to agent-claude-code:
I-1: Guard ps -eo pid,tty,args behind isWindows() in isProcessRunning for
all three agents. ps is Unix-only; a stale tmux handle on Windows
would silently return false via exception catch. The explicit guard
makes intent clear and avoids the unnecessary execFile call.
I-2: Inline systemPromptFile content on Windows instead of $(cat ...)
bash command substitution (aider: --system-prompt; opencode: prompt
value). codex is unaffected — it passes the path via -c flag and
reads the file itself.
Tests: added isWindows mock (default false) to all three test suites to
prevent real isWindows()=true on Windows from triggering the new guard
in existing Unix-path tests. Added Windows-specific tests for each fix.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(core): use F_OK and skip bare extension in Windows gh/git wrappers
On Windows, fs.constants.X_OK is equivalent to F_OK (execute bit does
not exist), so any existing file passes the check. The empty extension
was also tried first, meaning a bare file named "gh" would be selected
over gh.exe. Switch to F_OK and only check .exe/.cmd extensions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(windows): QA fixes — dashboard spawn, shellEscape, tmux hint, PID fallback
T04: resolveNextBin() skips POSIX .bin/next shim on Windows; invokes
next/dist/bin/next via process.execPath instead (ENOENT fix).
T18: shellEscape() branches on isWindows() — PowerShell uses '' doubling,
Unix uses POSIX '\'' escaping. All 4 agent plugins benefit automatically.
T06 secondary: tmux attach hint in `ao spawn` output gated behind
runtimeName === "tmux" (process runtime has no tmux session).
T06/T11: runtime-process destroy() and isAlive() fall back to
handle.data.pid when the in-memory processes Map is empty — fixes
cross-process CLI calls (ao session ls / ao session kill).
* fix(web): prevent nft EPERM on Windows home directory junction points
Next.js nft (Node File Tracer) scans homedir() at build time and hits
EPERM on Windows junction points (e.g. Application Data). Adds homedir()/**
to TraceEntryPointsPlugin.traceIgnores on Windows server builds.
* Revert "Merge branch 'main' into feat/windows-platform-adapter"
This reverts commit 5ba7644548, reversing
changes made to 5da9bedf5c.
* Reapply "Merge branch 'main' into feat/windows-platform-adapter"
This reverts commit 6a326a07e3.
* fix(windows): update @composio imports to @aoagents scope
Our Windows-specific files were written before the @composio → @aoagents
rename landed. Update all affected imports across runtime-process,
workspace-clone, workspace-worktree, cli commands, and test files.
* feat(windows): add PTY host for ConPTY terminal sessions
Windows equivalent of the tmux daemon. Per-session detached pty-host.js
process owns a ConPTY (via node-pty), listens on a named pipe
(\.\pipe\ao-pty-{hash}-{sessionId}), and relays terminal I/O to any
connected client.
- runtime-process: spawn pty-host on Windows, route sendMessage/getOutput/
isAlive/destroy through named pipe protocol
- mux-websocket: named pipe relay for dashboard terminal (skip TerminalManager
on Windows), resolvePipePath via generateConfigHash (instant, no pipe scan)
- direct-terminal-ws: use real mux server on Windows instead of placeholder
- tmux-utils: findTmux returns null on Windows, resolvePipePath added
- orchestrator-prompt: runtime-agnostic language
- opencode: fix isProcessRunning tmux-before-guard bug (W29)
Addresses blockers W01-W12, W23, W24, W28, W29, W33-W35.
Unix behavior completely unchanged.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(windows): QA fixes — session attach, ao stop, activity detection
- cli/session: add Windows ao session attach via named pipe relay with
raw stdin mode and Ctrl+\ to detach; skip getTmuxActivity on Windows
- cli/start: fix ao stop looking for "tr-orchestrator" instead of the
actual numbered session (e.g. tr-orchestrator-5) — also fixes Linux
- agent-claude-code: fix toClaudeProjectPath dropping Windows drive colon
(C:\→C- not C) breaking JSONL lookup; ignore stale JSONL entries from
previous sessions in reused worktrees
- pty-client: use \r (carriage return) instead of \n for PTY Enter key
Addresses blockers W13 (partial), W14.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: resolve merge conflicts from main — rename isOrchestratorSession, update stop tests
- session.ts: use isOrchestratorSessionName (renamed in main) for JSON output
- start.test.ts: update stop command tests to use sm.list() instead of sm.get()
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address CI failures and review comments
- session.ts: restore isOrchestratorSession from core (checks both name
and metadata role) instead of name-only isOrchestratorSessionName
- session.ts: remove unused allSessionPrefixes after merge conflict
- start.test.ts: update stop command tests for sm.list() flow
- toClaudeProjectPath: remove speculative space replacement, keep only
verified chars (/ : .) — addresses review comment about Unix breakage
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(windows): address review feedback, fix tests, and improve Windows coverage
- Fix CI diff coverage and review feedback
- Fix test reliability for session attach, stop, and Windows attach
- Add coverage for session attach binary protocol and edge cases
- Clean up stdin listener + add resolvePipePath tests
- Address review comments + coverage for pipe relay
- Make 80 failing tests pass on Windows (cross-platform mocks, path assertions)
- Fix production code: execFile shell option for .cmd, isPathInside separator,
openclaw binary detection via `where` on Windows
- Add platform-aware test assertions for shell escaping, PATH handling, hooks
- Add signal forwarding comment for Windows dashboard process
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(windows): QA fixes — activity timestamps, prompt delivery, pty-host keep-alive
- session.ts/status.ts: use session.lastActivityAt on Windows (no tmux)
- session-manager.ts: stabilize ConPTY output before sending post-launch prompt
to prevent prompts being swallowed during agent startup splash screen
- pty-client.ts: split message + Enter into two writes (300ms gap) to match
tmux send-keys behavior; fix isAlive to return true while pipe is connectable
regardless of whether the agent process inside has exited
- pty-host.ts: keep named pipe server alive after agent exits (mirrors tmux
session persistence) so clients can still attach and view scrollback
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* 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>
* 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>
* 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>
* 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>
* test(cli): mock exec for ps verification in stop test
killDashboardOnPort runs a unix-only `ps` cmdline check before killing.
Without mocking exec, the call rejects and the catch returns false, so
killProcessTree is never called and the assertion fails on Linux CI.
* fix(windows): suppress console flashes, register process runtime, auto-detect Git Bash, chunk PTY input
- platform.ts: add windowsHide:true to pwsh/powershell/taskkill/netstat
spawns so AO no longer flashes a console window for each subprocess.
- script-runner.ts: auto-detect Git Bash at the common install paths on
Windows when AO_BASH_PATH is unset; tighter error if neither auto-detect
nor override succeeds. WSL bash intentionally excluded — invoking it
from Windows-native Node mixes Linux paths with Windows cwd and
silently breaks repo scripts. Also adds windowsHide:true to the spawn.
- web/services.ts: register @aoagents/ao-plugin-runtime-process so the
dashboard can spawn sessions on projects using runtime: process
(the Windows default per getDefaultRuntime).
- pty-client.ts: chunk ptyHostSendMessage into 512-char frames with a
15ms gap so large prompts (~3-4KB+) are no longer truncated by
ConPTY's input buffer. Cross-platform safe; Unix PTYs absorb chunks
at full speed. Trailing Enter still sent as a separate frame after
the existing 300ms pause.
- Mock test helpers updated to handle the (cmd, args, options, callback)
arity introduced by passing windowsHide; new test covers Git Bash
auto-detection.
* fix(windows): add windowsHide to remaining subprocess spawns
Sweeps the spawn sites missed by the first pass — `ao stop`, session
list, opencode introspection, tmux helpers, and worktree git/postCreate
all bypassed the previous fix and still flashed conhost on Windows.
- cli/lib/shell.ts: exec helper (used by git/gh/tmux wrappers)
- core/session-manager.ts: EXEC_SHELL_OPTION + standalone tmux call
- core/tmux.ts: tmux execFile helper
- plugins/workspace-worktree: git wrapper + rev-parse + postCreate shell
- workspace-worktree tests: assertions updated for the new options shape
* fix(codex): make agent-codex work on Windows
Three Windows-specific gaps that combined to make every Codex spawn fail
on PowerShell with "Unexpected token '-c' in expression or statement":
- formatLaunchCommand(): prepend `& ` to the joined launch string when
running on Windows. shellEscape quotes the resolved binary path
('C:\Users\...\codex.cmd'), and PowerShell parses a leading quoted
string as an expression — without the call operator the next flag
triggers a parser error before codex is ever invoked. bash treats
the same string as a normal command, so the prefix is Windows-only.
Applied at both getLaunchCommand and getRestoreCommand exits.
- resolveCodexBinary(): add a Windows branch using `where.exe` instead
of `which`. Prefers codex.cmd (npm shim) over codex.exe (Cargo build),
then falls back to %APPDATA%\npm\codex.{cmd,exe} and ~\.cargo\bin
for users whose PATH doesn't yet include the install dir. Lookup runs
with windowsHide:true so the search itself doesn't flash a console.
- sessionFileMatchesCwd(): compare paths via a canonical form
(forward slashes, lowercased drive letter) so Codex JSONL rollout
files can still be located when payload.cwd uses a different slash
direction or drive-letter case than the workspace path AO computes
via path.join. Without this, dashboard activity/cost stay empty
for Codex sessions on Windows.
* fix(windows): resolve gh.exe via PATHEXT and fix path-shape test regexes
resolveGhBinary() searched PATH for a literal "gh" file and threw on
Windows where the binary is gh.exe (or gh.cmd for npm shims). All gh
calls in tracker-github and scm-github failed before reaching execFile,
which made spawn() fall back from tracker-derived branch names and made
cleanup() skip the gh-driven kill paths entirely.
Honor PATHEXT on win32 so the resolver matches gh.exe/.cmd/.bat. Update
the four affected integration assertions to accept Windows path shapes.
Also bump the runtime-process sendMessage sleep on Windows — ConPTY
pipe round-trip needs more headroom than the Unix direct-stdin path.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(windows): junctions/hardlinks for symlinks, WinRT toast notifier, DPI re-fit
workspace-worktree: when symlinkSync EPERMs on Windows (no admin /
Developer Mode), try a junction for directories and a hardlink for
files before falling back to recursive cpSync. The previous fallback
copied node_modules into every worktree — slow and bloated.
notifier-desktop: add a win32 branch using PowerShell + WinRT toast XML
(no third-party deps). The script is base64-encoded as -EncodedCommand
to sidestep PowerShell argument tokenization. Toast failures log a
warning instead of rejecting so a stripped-down SKU or disabled
notifications can't crash the lifecycle.
DirectTerminal: re-fit on devicePixelRatio changes via matchMedia.
ResizeObserver doesn't fire when only DPR changes (e.g. dragging the
window between monitors at different scales on Windows), leaving an
unrendered stripe to the right of the last column until manual resize.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(test): update notifier-desktop integration test for Windows toast support
I missed this duplicate test in the integration-tests package when I
added the Windows branch to notifier-desktop. The mock callback used the
3-arg execFile signature (cmd, args, cb) but the new win32 path calls
execFile with 4 args (cmd, args, opts, cb), so the callback landed in
the opts slot and "cb is not a function" broke CI on Linux.
Make the mock signature-agnostic and replace the win32 "no execFile,
warns" assertion with one that verifies the EncodedCommand toast script.
Add a separate freebsd case for the actual unsupported-platform path.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs: consolidate six Windows port plans into one closeout
All 11 punch-list tasks shipped (junctions, WinRT toast, DPR re-fit
landed last) plus the foundational PTY-host / runtime-process work.
Stop fix is the only deferred item, waiting on upstream PR #1496.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(agents): use shell:true on Windows in detect() to honor PATHEXT
execFileSync with a bare command name on Windows does not consult
PATHEXT — it only finds literal .exe files. CLIs installed via
npm install -g land at %APPDATA%\npm\<name>.cmd, which detect() can't
see, so AO reports the agent as not installed.
Add shell: isWindows() so cmd.exe handles PATHEXT and finds .cmd shims.
Adds windowsHide: true while we're there to suppress conhost flashes.
Affects all 5 agent plugins: aider, claude-code, codex, cursor, opencode.
Reproduced with codex installed via npm on a Windows EC2 box where
where.exe codex resolved to codex.cmd but detect() returned false.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(windows): probe absolute powershell path so PATH-degraded children don't fall through to cmd.exe
The dashboard (Next.js) sometimes spawns the runtime-process pty-host with a PATH that
lacks C:\Windows\System32. Both `pwsh` and `powershell.exe` probes in
resolveWindowsShell() then fail and we drop to cmd.exe — which can't execute the
PowerShell-syntax launch commands agents emit (e.g. Codex's `& 'codex' ...`),
producing `'&' was unexpected at this time.` and an immediately-exited orchestrator.
Probe %SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe directly via
existsSync — this path is guaranteed on Windows 10+ and doesn't depend on PATH.
Also add an AO_SHELL env override as an explicit escape hatch.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix: restore orchestrator session with its systemPromptFile
When restoring an orchestrator session whose agent has no resumable thread for
the worktree (e.g. Codex when the rollout file's cwd doesn't match), restore()
falls back to getLaunchCommand(agentLaunchConfig). The fallback's
agentLaunchConfig was missing systemPromptFile, so Codex booted as a bare TUI
with no orchestrator instructions — the dashboard terminal showed the default
"Write tests for @filename" prompt instead of the orchestrator running.
spawnOrchestrator writes the prompt to {baseDir}/orchestrator-prompt-{sessionId}.md
and threads it through agentLaunchConfig.systemPromptFile (session-manager.ts:1687).
Re-attach the same file on restore when the role is orchestrator and the file
still exists on disk.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(test): assert negative pid in start full-stop test
killProcessTree on Unix targets the process group first via
process.kill(-pid, signal); only falls back to positive pid if that
throws. The test mock returns true, so only the negative-pid call
ever fires. Update assertion to match the actual call.
Caught by CI on Linux (test was Windows-skipped locally).
* fix(test): assert on killProcessTree mock, not process.kill
killProcessTree is module-mocked at the top of start.test.ts, so
process.kill is never invoked by the stop command — the spy
assertion would always see 0 calls on Linux CI. Assert on the
mock directly. Mock is platform-agnostic, so the skipIf is gone.
* fix(windows): node wrapper updateAoMetadata supports V2 .json metadata format
The Windows Node.js gh/git wrappers in NODE_UPDATE_AO_METADATA only tried
the bare session path (e.g. ao-154), but V2 storage uses ao-154.json files.
This caused silent metadata update failures on Windows — PR URLs written by
agents via `gh pr create` were never recorded in session metadata.
Fix mirrors bash ao-metadata-helper.sh: try .json first (V2), fall back to
bare name (V1/legacy). Also adds JSON.parse/stringify handling for V2 JSON
format instead of the key=value line-splitting that only worked for V1.
Bump WRAPPER_VERSION 0.6.0 → 0.7.0 to force reinstall on existing setups.
* chore: remove accidentally committed package-lock.json files
* feat(windows): pty-host registry + sweep on stop and project delete
Windows pty-hosts spawn detached so they survive parent exit (mirroring tmux
on Unix). That same detachment means taskkill /T cannot reach them on
graceful shutdown — they live in their own console group, outside the
parent's process tree. Per-session metadata can't be the source of truth
either: rm -rf'd worktrees, mid-write crashes, or manual recovery sever
AO's only handle to the host PIDs and orphan them silently.
This adds a sideband registry at ~/.agent-orchestrator/windows-pty-hosts.json
that AO writes on spawn (runtime-process) and reads on shutdown (cli/start.ts
sweepWindowsPtyHosts) and project delete (web/.../route.ts via
stopStaleWindowsPtyHosts). Reads auto-prune entries whose PID is gone, so
the registry is self-healing across crashes.
Sweep is graceful-first: each entry gets ptyHostKill via its named pipe,
500 ms grace probe, then killProcessTree as the hard fallback. The result
("swept N pty-host(s): G graceful, F force-killed") goes to the ao stop
log so users can see cleanup happened.
Verified live: spawn registers, destroy unregisters, ao stop --all sweeps,
PID 0 entries auto-prune on next read.
* fix(windows): retry worktree rmSync on file-handle drain race
After ao kills a runtime, the just-exited pty-host's child processes
(conpty_console_list_agent.exe, the agent's spawned shell, .git/index.lock)
still hold open handles inside the worktree for ~30 s–2 min while Windows
drains them. rmSync(force: true) deletes individual files but the parent
rmdir blocks with EBUSY/ENOTEMPTY/EPERM, leaving an empty orphan directory
under ~/.agent-orchestrator/projects/*/worktrees/.
destroy()'s catch-block fallback now calls removeDirWithRetry, which on
Windows retries with backoff [0, 100, 250, 500, 1000, 2000] ms checking
existsSync between attempts, and throws a descriptive error if the
directory survives all six. Non-Windows behaviour is unchanged (single
rmSync).
The thrown error escapes to session-manager.ts:kill which already swallows
it, so callers see no behaviour change today — but observability layers
can hook in later to surface real failures instead of silent orphans.
Addresses the Windows subset of #1562 (the cross-platform stale
.git/worktrees/<id>/ registration is still tracked there separately).
* fix(windows): code-review hardening — shell args, runtime default, sessionId, V2 pipe path
Four small fixes flagged in review of the Windows port:
- core/platform.ts: AO_SHELL override now infers args flag from the shell
basename (cmd → /c, bash/sh/zsh → -c, anything else → -Command). Previously
every override got PowerShell args, so AO_SHELL=cmd or AO_SHELL=bash
silently broke run-command flows.
- core/global-config.ts: defaults.runtime now resolves to getDefaultRuntime()
(process on Windows, tmux elsewhere) instead of the hardcoded "tmux".
First-run on Windows no longer writes a config that immediately fails
runtime resolution.
- web/server/mux-websocket.ts: validateSessionId now runs on the Windows
named-pipe relay path. The Unix branch validates inside TerminalManager;
the Windows path bypassed it entirely, so an unsanitised id became both
a map key and was interpolated into a pipe path downstream.
- web/server/tmux-utils.ts: resolvePipePath now reads the V2 JSON layout
(~/.agent-orchestrator/projects/{projectId}/sessions/{id}.json) first,
then falls back to V1 line-delimited metadata for users who haven't run
ao migrate-storage. The single-source-of-truth note is still accurate;
the search just covers both layouts during the migration window.
Each change has a paired unit test.
* chore: drop superseded windows-port-closeout plan
* fix(core): lazy-resolve homedir() in windows-pty-registry
REGISTRY_FILE was computed at module load via homedir(), which fired
before vitest mock factories for `node:os` could install. Tests that
mock node:os (notifier-desktop, terminal-iterm2, agent-claude-code
activity-detection) hit either a TDZ error or "homedir not defined on
mock" because the mock isn't bound at evaluation time.
Resolve the path lazily inside readRaw/writeRaw so each call honours
the current mock. Also rename the test helper export from a const to
a function (__getWindowsPtyRegistryFile) so tests can read the
post-mock value.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(cli): exit 0 when SIGKILL fallback fires on slow Ctrl+C
forwardSignalsToChild's 5 s fallback called process.exit(1) after
SIGKILL, which marks user-initiated Ctrl+C as an error whenever the
child is merely slow to drain (Next.js connection draining is the
common case). Shell scripts and CI pipelines that check the AO exit
code break.
Use exit 0 — graceful user shutdown is not a failure even if the
child needed force-killing. Reported by greptile review on PR #1025.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(core): drop synchronous shell probes that block event loop
resolveWindowsShell ran execFileSync("pwsh", ["-Version"], { timeout: 5000 })
on every cold start. On the common case (Windows 10/11 with no pwsh
installed) the call blocks the Node event loop for the full 5 s timeout,
stalling AO startup, runtime spawns, and postCreate hooks.
Walk PATH ourselves via existsSync — the lookup is microseconds and
needs no subprocess. Cascade unchanged: AO_SHELL → pwsh on PATH →
absolute powershell.exe → powershell on PATH → cmd.exe.
Reported by greptile review on PR #1025.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(runtime-process): treat EPERM as alive in pty-host destroy probe
destroy()'s 500 ms graceful-shutdown loop probes the pty-host with
process.kill(pid, 0) and treats any throw as "process gone, clean
exit". On Windows, cross-context processes can return EPERM — the
process is alive but we lack permission to signal it. Returning
early in that case orphans the pty-host and skips killProcessTree.
Detect EPERM and break out of the wait loop so the orphan falls
through to killProcessTree. Other error codes (ESRCH etc.) still
mean the process is gone.
Reported by Copilot review on PR #1025.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(cli): discover Git Bash via PATH walk for non-default installs
WINDOWS_BASH_CANDIDATES only checks C:\Program Files{,(x86)}\Git, so
users who installed Git for Windows on a different drive (e.g.
D:\Program Files\Git\) hit "Cannot run repo scripts on Windows
without bash" even though Git Bash is available. AO_BASH_PATH is the
documented escape hatch but should not be required.
Add a PATH-walk fallback that finds bash.exe wherever Git's bin dir
sits — Git for Windows adds itself to PATH at install time, so this
covers the typical non-default-drive case without a subprocess or
registry lookup.
Reported by greptile review on PR #1025.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(core): hard-code Windows path separators in findOnPath
Using path.delimiter / path.join in the PATH walker meant Linux CI
ran the test with `:` as the splitter and `/` as the joiner. The unit
test simulates Windows by setting PATH="C:\fake\bin" — on Linux
that splits to ["C", "\fake\bin"] and produces "C/powershell.EXE",
neither of which match the mocked existsSync.
findOnPath is only ever called from resolveWindowsShell, so use `;`
and `\` unconditionally. The runtime data — process.env values,
mocked existsSync — is what's being tested, not host-OS path logic.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(windows): PowerShell repo-script runner + ao-doctor/ao-update.ps1
runRepoScript on Windows now prefers a .ps1 sibling of the requested .sh
script and runs it via pwsh.exe (or bundled powershell.exe as fallback).
Adds ao-doctor.ps1 and ao-update.ps1 as Windows equivalents of the
existing bash scripts.
* test(cli): add missing mockExecSilent hoist in dashboard.test.ts
The findRunningDashboardPidsForWebDir tests reference mockExecSilent
but it was never declared in vi.hoisted, so they crashed with
ReferenceError before any assertion ran. Add the missing hoist and
wire execSilent into the shell.js mock.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(windows): echo projectId in pipe relay messages so MuxProvider routes correctly
MuxProvider keys subscribers under `${projectId}:${id}` when projectId is
provided. The Windows pipe relay was dropping projectId from outbound
messages, so the client routed by id alone and the subscriber bucket
mismatched — leaving the xterm pane blank on
/projects/[id]/sessions/[id].
Echo projectId on every outbound terminal frame (opened/data/exited/error)
so the Windows path matches the Unix tmux relay's behavior.
* test(core): respect TMPDIR in platform defaults test
* fix(runtime): harden dashboard launch shutdown
* fix(windows): scope pipe maps and resolvePipePath by projectId
The Windows pipe relay was project-scoped only on outbound WS frames.
Server-side storage and pipe-path resolution still keyed by bare session
id, so two projects sharing a session id on the same mux connection
would collide on the same socket/buffer entry, and resolvePipePath
returned the first matching project's metadata regardless of caller
intent. Brings the Windows path in line with the Unix subscriptionKey
contract.
- resolvePipePath(sessionId, projectId?, fs?) reads only the caller's
project metadata when projectId is provided; legacy callers keep the
walk-all-projects fallback.
- winPipes / winPipeBuffers keyed by \${projectId}:\${id}.
- projectId threaded through handleWindowsPipeMessage data/resize/close
cast sites.
- Tests cover the project-collision case in both mux-websocket and
tmux-utils.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(windows): clear 10 Windows test failures
Real fix:
- events-db: add closeDb() to release the better-sqlite3 file lock on
activity-events.db. Without it, Windows callers cannot rmSync the AO
base dir while the connection is open. Test teardowns in
test-utils.ts and plugin-integration.test.ts now call closeDb()
before rm to fix 4 EBUSY failures.
Test-only:
- tmux-utils.test.ts: normalize backslashes to forward slashes in two
resolveTmuxSession 'hash-prefix' tests; matches the pattern already
used by sibling tests in the same file.
- dashboard.test.ts, script-runner.test.ts, update-script.test.ts:
add it.skipIf(process.platform === 'win32') to four tests that
assert Unix-specific behavior (lsof cwd matching, posix script
paths, ao-update.sh smoke). Each file already uses the same skip
pattern for sibling tests.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(cli): port-scan fallback + Windows PowerShell branch coverage
start.test.ts: re-add the orphaned-dashboard port-scan test that was
lost during the merge from main (commit 4958512d). When the dashboard
auto-reassigns to port+N because the configured port was busy, ao stop
must walk port+1..port+MAX_PORT_SCAN to find it. Skipped on Windows
because killDashboardOnPort skips the ps cmdline verification there.
script-runner.test.ts: add coverage for the Windows PowerShell branch
in runRepoScript. Two Windows-only tests assert (1) ao-doctor.sh is
rewritten to ao-doctor.ps1 and dispatched via pwsh.exe / powershell.exe
with -NoProfile -NonInteractive -ExecutionPolicy Bypass -File and
forwarded user args, and (2) the rewrite is .sh-suffix-driven, not
blind, so a non-.sh script does not get a .ps1 lookup.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(agent-kimicode): make plugin Windows-compatible
Seven blocking issues prevented kimicode from working on Windows. None
were guarded by isWindows checks because the plugin was authored
without importing it. Symptoms ranged from silent agent launch
failures to misclassified process state to total session-discovery
breakage.
1. getLaunchCommand emitted bare command strings ("kimi --work-dir
...") which PowerShell parses as a quoted expression rather than
executing. Wrap with formatLaunchCommand() so Windows gets the
"& " call operator, matching agent-codex.
2. isProcessRunning called ps -eo on the tmux branch with no platform
guard. ps does not exist on Windows, so a stale tmux handle would
throw and misclassify a live agent as exited. Added the same
isWindows() return-false guard agent-codex uses.
3. resolveWorkspacePath called realpath() unconditionally. On Windows,
Node's realpath silently canonicalizes non-existent paths instead
of throwing ENOENT — turning "/workspace/test" into
"D:\workspace\test" and diverging the session-discovery hash from
any caller that hashed the raw input. Stat first so the catch path
is reached uniformly across platforms.
4. isInsideKimiSessions hardcoded "/" as the path separator in the
sandbox check. realpath returns native paths (backslashes on
Windows) so candReal.startsWith(rootReal + "/") never matched —
every candidate was rejected and findKimiSessionMatch returned
null forever. Use path.sep.
5. getEnvironment set PATH and GH_PATH locally with hardcoded POSIX
values. session-manager already injects both for every agent
plugin, so the local writes were dead code that masked the
Windows-aware central logic. Drop them; mirror agent-codex.
6. getLaunchCommand passed config.systemPromptFile via --agent-file,
but kimi expects --agent-file to be a YAML agent spec, not arbitrary
markdown. AO writes the orchestrator prompt as a plain .md file, so
kimi exited with 'Invalid YAML in agent spec file: expected
<document start>, but found <block sequence start>' on the first
bullet. Read the file synchronously and inline its contents into
--prompt instead, concatenating with any existing config.prompt.
7. session-manager listed kimicode in requiresNativeRestore, so when
getRestoreCommand returned null (because the previous launch failed
before kimi wrote any session data), AO threw
SessionNotRestorableError instead of falling back to a fresh
getLaunchCommand. Removed kimicode from the allowlist; falling back
is the only sensible behavior when there is no session on disk to
resume.
Tests: switched the per-suite workspace constant to a per-test
mkdtemp-scoped path so a coincidental directory at /workspace/test
on the host doesn't make Windows realpath canonicalize it. Mocked
isWindows so platform-aware production code can be exercised
deterministically. Made the shell-escape prompt assertion
platform-aware (POSIX 'backslash-quote' vs PowerShell double-quote).
Replaced the --agent-file tests with system-prompt-content-into-prompt
assertions backed by a real temp file under fakeHome.
Result: all 103 tests pass on Windows (was 30 failures pre-fix).
* fix(agent-claude-code): preserve Windows drive-letter slug encoding
The merge of origin/main #1611 ("fold underscores in Claude project
slug") inadvertently regressed Windows behavior. #1611 kept the
pre-existing `.replace(/:/g, "")` so `C:\Users\dev\foo` slug-encoded
to `C-Users-dev-foo` (single dash), but Windows-side QA had already
established (commit 582c5373) that real Claude Code on Windows
produces `C--Users-dev-foo` — the colon position becomes a dash,
not stripped. Stripping the colon broke JSONL lookup on Windows so
session info / restore / metadata persistence all silently failed.
Two test files disagreed after the merge: activity-detection.test.ts
expected the Windows-correct double-dash form (kept by my merge),
while index.test.ts expected origin's single-dash form (added by
#1611). Linux CI ran activity-detection's case against the
single-dash impl and failed loudly.
Fix: drop the redundant `.replace(/:/g, "")`. The broader
`[^a-zA-Z0-9-]` regex already handles the colon as a dash, which
matches Claude's actual on-disk encoding on Windows. Updated
index.test.ts to expect `C--Users-dev-foo` and fixed an unrelated
local-Windows test bug where a hardcoded POSIX path string was
compared against a `pathJoin` result (passes on Linux CI but fails
locally on Windows).
Underscore folding from #1611 is preserved.
* fix(cli): Windows platform adapter follow-ups
Three independent Windows correctness fixes bundled with their tests:
* daemon.ts: killExistingDaemon now uses killProcessTree (taskkill /T /F)
instead of raw process.kill so detached grandchildren of the daemon
(pty-host, dashboard subprocess) are reached on Windows. POSIX behavior
is preserved via killProcessTree's process-group fallback.
* startup-preflight.ts: on Windows, when the project config selects
runtime: tmux, offer to rewrite the line to runtime: process in the
project YAML instead of prompting "install tmux?". The rewrite is a
targeted line-replace (not yaml round-trip) so comments and quoting
are preserved. Decline -> hard exit with manual-fix guidance.
* path-equality: new pathsEqual / canonicalCompareKey helpers used by
start.ts and resolve-project.ts for "same filesystem entry" checks.
realpathSync on Windows can return canonical paths whose drive-letter
case or 8.3-vs-long-name expansion differs from the input even when
both resolve to the same on-disk entry, which made naive === comparisons
miss and surface as phantom "register this project?" prompts on
re-runs of `ao start <path>`. Lowercases on Windows; POSIX is
unchanged.
Also fixes resolve-project.ts's isLocalPath to recognize Windows path
patterns (drive-letter, UNC, .\, ..\) so `ao start C:\path\to\repo`
takes the path branch instead of being mis-classified as a project id.
Test changes: makeConfig now defaults to runtime: process so tests run
on every platform without tripping the Windows-tmux exit; the one tmux
preflight test pins process.platform = 'linux'. The "kills existing
process" test asserts on killProcessTree instead of process.kill.
On-disk yaml fixtures in start.test.ts switch from runtime: tmux to
runtime: process for the same reason.
New tests: 7 in startup-preflight.test.ts (Windows rewrite, decline
exit, missing configPath exit, comment+quoting preservation, Linux
pass-through), 8 in path-equality.test.ts (drive-letter case, segment
case, POSIX case-sensitivity, realpathSync fallback, ~ expansion),
killProcessTree assertions added to daemon.test.ts.
Verified non-issues during the audit (no code change): ao stop graceful
shutdown gap (the work was already moved into ao stop itself in a prior
refactor; running.json/last-stop/sessions are persisted before the
parent kill, and stale state is self-healing on next read);
better-sqlite3 cross-platform binary (optionalDependencies +
files: ['dist'], no prebuilt .node bundled in any release artifact);
ao-doctor / ao-update PowerShell rewrite (script-runner already
rewrites .sh -> .ps1 on Windows, .ps1 siblings ship in assets/scripts/,
covered by an existing Windows-only test); bun-tmp-janitor leak
(janitor is a no-op on Windows because opencode ships no win32 binary
and Windows refuses to unlink mapped files).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(cursor): silence stderr bleed-through in detect() on Windows
execFileSync("agent", ["--help"]) with encoding but no explicit stdio
inherits stderr from the parent process. On Windows with shell:true,
cmd.exe prints "'agent' is not recognized as an internal or external
command" to the terminal even though the exception is caught.
Fix: add stdio:["ignore","pipe","ignore"] to capture stdout (needed for
Cursor marker checks) and discard stderr. Mirrors the pattern used by
the kimicode plugin's detect(). Also adds a 5s timeout as a safety net.
Zero behavior change on macOS/Linux: shell:false means Node throws ENOENT
directly with no subprocess output, so the try/catch already handles it.
Fixes the spurious error printed during ao start first-run setup on Windows.
Co-authored-by: Priyanchew <57816400+Priyanchew@users.noreply.github.com>
* fix(runtime-process): preserve EPERM in Windows pty-host sweep exit-poll
The catch in sweepWindowsPtyHosts treated every error as "process exited",
including EPERM. On Windows EPERM means the pty-host exists but the caller
lacks permission to signal it (cross-context), so the orphan was skipping
the killProcessTree force-kill step and leaking. Mirror the destroy() logic
at line 290: only flag exited on non-EPERM (typically ESRCH).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(runtime-process): poll for payload instead of fixed sleep
The Windows ConPTY round-trip (named pipe -> pty-host -> pwsh -> findstr
-> rolling buffer) varies from hundreds of ms to seconds depending on
runner load, AV scanners, and cold caches. The previous 1500 ms fixed
sleep flaked on slow Windows runners (observed empty getOutput buffer at
sample time). Replace it with a 10 s deadline poll that checks for the
actual payload substring, robust to both timing variance and incidental
shell banners arriving first.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* docs: document cross-platform abstractions and reflect Windows support
Adds docs/CROSS_PLATFORM.md as the canonical reference for cross-platform
development: the "Golden Rule" (no raw process.platform === "win32" — use
isWindows() and the helpers in platform.ts), a full inventory of every
platform helper (platform.ts, path-equality, windows-pty-registry,
pty-client, sweepWindowsPtyHosts, validateSessionId, resolvePipePath,
setupPathWrapperWorkspace, activity-state helpers, AO_SHELL/AO_BASH_PATH),
the EPERM-vs-ESRCH gotcha when probing processes, PowerShell-vs-bash
differences, IPv6 localhost stalls, agent-plugin specifics, and a 10-point
pre-merge checklist.
Updates internal docs (CLAUDE.md, AGENTS.md, docs/ARCHITECTURE.md,
docs/DEVELOPMENT.md, CONTRIBUTING.md, .github/copilot-instructions.md,
.cursor/BUGBOT.md, packages/core/README.md, packages/plugins/runtime-tmux/
README.md, packages/core/src/prompts/orchestrator.md, ARCHITECTURE.md) to
remove tmux-only / POSIX-only claims, point at the new doc, and (in
docs/ARCHITECTURE.md) describe the Windows runtime architecture: pty-host
helper, named-pipe protocol, registry, sweep, mux WS Windows branch.
Updates user-facing docs (README.md, SETUP.md, docs/CLI.md) to split
prerequisites by OS (no tmux on Windows), reflect that ao doctor and
ao update work on Windows, and note that power.preventIdleSleep is a
no-op on Linux and Windows.
Updates the agent-orchestrator skill (skills/agent-orchestrator/SKILL.md
and references/config.md) so it advertises Windows support, drops tmux
from the required-bins list, and gives the right Windows guidance for the
"spawn tmux ENOENT" error.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(core): update orchestrator-prompt test for cross-platform runtime warning
The orchestrator system prompt was rewritten in 1d8c8f75 to call out both
tmux send-keys (Unix) and the Windows named-pipe write path so the
orchestrator agent doesn't try either. The test still asserted the old
literal "never use raw \`tmux send-keys\`" string. Update it to assert the
new platform-neutral phrasing plus the presence of both runtime mentions.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* ci(windows): matrix Linux+Windows + close coverage gaps
Adds windows-latest to typecheck/test/test-web matrices (lint stays
Linux-only; nothing ESLint catches differs by OS). fail-fast: false
so one OS's failure never masks the other's. tmux install steps gate
on runner.os == 'Linux' since Windows uses runtime-process. test job
adds a node-pty prebuild smoke step on Windows so a future ABI break
fails fast with a clear message. test-web is broadened from
server/__tests__/ to the full vitest suite — closes a pre-existing
Linux-too gap and ensures component/hook/lib tests run on Windows.
Closes three completeness gaps where Windows code paths existed but
no test exercised them:
1. session.test.ts (5 tests): "tests Windows behavior, skips on
Windows" defensive pattern. Tests fully mock isWindows + net.connect
+ child_process — flipping skipIf(win32) to plain it() runs them on
both OSes. All 45 tests pass on Windows.
2. dashboard.test.ts (+2 tests): findRunningDashboardPidsForWebDir
has parallel POSIX (lsof + cwd verification) and Windows
(findPidByPort, no cwd check) implementations. Existing tests
asserted lsof; new runIf(win32) tests assert findPidByPort path
plus dedup across multiple ports.
3. start.test.ts (+1 test): port-scan fallback for orphaned
dashboards skips ps cmdline verification on Windows by design.
Existing test asserted ps was called; new runIf(win32) parallel
asserts ps was NOT called and the kill still fires.
Adds first PS1 script test coverage (previously zero):
4. update-ps1.test.ts (4 tests): argparse — --help/-h, unknown flag,
conflicting --skip-smoke + --smoke-only.
5. doctor-ps1.test.ts (4 tests): argparse + full check pipeline
smoke. The pipeline test runs every Check-* function against an
empty repo and asserts the script exits cleanly with a "Results: N
PASS, N WARN, N FAIL, N FIXED" summary line — catches PS1 syntax
errors and crashes mid-pipeline.
Net effect: CLI suite went from 622 -> 630 passing tests on Windows
(5 unskipped + 8 new); skipped count dropped from 25 -> 20. All other
suites unchanged.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* ci(windows): add minimal permissions block to CI workflow
CodeQL flagged the workflow as missing an explicit permissions
declaration (security/code-scanning/61). All jobs are read-only
(checkout, install, build, test) — contents: read is sufficient.
Matches the workflow-level pattern already used in coverage.yml.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore: add changeset for native Windows support
Minor bump across the linked package group. The next release PR will
consume this and bump from 0.4.0 to 0.5.0.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(cli): make ao open work cross-platform
Mac-only assumptions broke `ao open` on Windows and Linux:
- source of truth was `tmux list-sessions`, which is empty without tmux
- the open action shelled out to `open-iterm-tab`, a macOS helper
Switch the source of truth to `sm.list()` (works on every platform — also
handles `runtime-process` sessions on Windows) and branch the open action:
- macOS: `open-iterm-tab` (unchanged), tmux attach inside iTerm
- Windows: `wt new-tab cmd /k ao session attach <id>` for live sessions,
with `cmd /c start cmd /k ...` as the no-`wt` fallback. Both paths route
through `cmd /k` because `wt` and `start` call CreateProcess directly,
which doesn't honor PATHEXT and reports 0x80070002 for `ao` (really
`ao.cmd`). New tab anchors at `config.projects[id].path` so the spawned
attach can resolve `agent-orchestrator.yaml` via loadConfig's upward
search; without this attach fails with "No agent-orchestrator.yaml found"
when the user's homedir is the inherited cwd.
- Linux: dashboard URL via `openUrl()`. No consistent terminal-spawn API
across DEs, so we don't try.
Other behavior changes:
- read the live daemon's port from `running.json` so URLs stay correct
when the dashboard auto-picked a non-default port
- warn when the daemon is not running (URL fallback won't load)
- aggregate targets (`all`, `<project>`) hide terminated sessions; named
lookup keeps them in scope and opens the dashboard with the death
reason inline (`died at <ts>: session=<reason>, runtime=<reason>`) plus
a `ao session restore <id>` hint
- new `--browser` flag forces the URL path on any platform
Add `isMac()` to `platform.ts` (per the project rule that platform checks
live in one place rather than spread as ad-hoc `process.platform === ...`
guards) and re-export from core.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(cli): satisfy lint on ao open changes
- replace inline `import("node:child_process")` type annotation in vi.mock
with a top-of-file `import type * as ChildProcess` (consistent-type-imports)
- drop the `[]` initializer on `sessionsToOpen` since every branch assigns
before any read (no-useless-assignment)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* chore: add changeset for cross-platform ao open fix
Patch entry for d04fad33 / 32345ba8. Linked group already minor-bumping
via the Windows-support changeset, so this just contributes a distinct
CHANGELOG line.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore: fold ao open fix into the Windows-support changeset
Single umbrella entry is the right place for it — the separate patch
changeset was redundant given the linked-group minor bump already in
flight. Reverts 3557e556.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* Changes before error encountered
Agent-Logs-Url: https://github.com/ComposioHQ/agent-orchestrator/sessions/9f3caf0b-66fb-4eff-bd3f-7fb9ab889630
Co-authored-by: Priyanchew <57816400+Priyanchew@users.noreply.github.com>
* test(core): mock node:child_process via importOriginal in migration test
The atomic-write.ts → platform.js refactor in eaa27b9b pulled platform.ts
into migration-storage-v2.test.ts's module graph. platform.ts evaluates
promisify(execFile) at top level, but the test's bare-object child_process
mock omitted execFile, so the dynamic import crashed with "No 'execFile'
export is defined on the 'node:child_process' mock".
Switch to vi.doMock with importOriginal so any unmocked exports stay real.
This is robust against future imports adding more child_process surface.
Only Ubuntu CI surfaced the regression — the failing test sits inside
describe.skipIf(process.platform === "win32") so the windows-latest leg
never executed it.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* test(core): hoist child_process type to satisfy consistent-type-imports
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* chore(perf): add AO_PERF-gated instrumentation for dashboard load
Temporary tracing added to diagnose 15-20s dashboard terminal load on Mac
and Windows. Gated on AO_PERF=1 (server) and NEXT_PUBLIC_AO_PERF=1
(client) so production paths stay untouched. To be removed once the
bottleneck is fixed.
Wrap points:
- core/perf.ts: perfMark / perfTime helpers + perfCid
- web /api/sessions/[id]: per-stage timings (getServices, sm.get, audit,
enrichMetadata, total)
- core/session-manager: runtime.isAlive, agent.getActivityState,
agent.getSessionInfo, ensureHandleAndEnrich
- agent-codex: findCodexSessionFile (scanned/opened/matched counts) +
cache hit marker
- runtime-process/pty-client: connect outcome + isAlive (split
connectMs vs statusMs)
- web/lib/serialize: enrich legs (agentSummary vs issueTitle) timed
independently while still running concurrently
- web/sessions/[id]/page.tsx: client.fetch.start/end with cid header
forwarded for end-to-end correlation
- web/MuxProvider: ws.open + ws.firstByte per terminal
* fix(core): drop bogus session.agent reference from perf extras
Session has no `agent` field — typed as a metadata key, not a
top-level property. CI typecheck caught what local rtk-filtered
output had hidden. The session-id cid already disambiguates per-session
so the extra wasn't load-bearing.
* revert: remove AO_PERF instrumentation
Reverts b3f522f9 and 004b2a79. The perf marks pinpointed that the
server API path is fast — total <50ms after warm-up — so the 30s
dashboard-terminal delay lives in the WS / xterm path, not in the
session-manager hot path that this instrumentation covered.
Will re-instrument that layer (mux-websocket terminal-open ->
opened-sent -> firstByte) separately when we resume the investigation.
* fix(core): drop unused isWindows import after post-launch removal
The merge took main's no-op for post-launch prompt delivery, which was
the only user of isWindows() in this file. Removing the dangling import
to unblock lint.
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: yyovil <itsyyovil@gmail.com>
* 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>
* 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
* feat(core): add opt-in gh CLI tracer and migrate scm/tracker plugins
Introduces execGhObserved() in @aoagents/ao-core: a thin wrapper around
execFile("gh", ...) that writes a JSONL trace row to $AO_GH_TRACE_FILE
on both success and failure. Captures status line, HTTP status, ETag,
rate-limit headers, duration, stdout/stderr byte counts, exit code, and
signal. No-op when the env var is unset, so default behavior is
unchanged.
Migrates three call sites to the observer:
- scm-github/graphql-batch.ts — PR-list guard, commit-status guard,
GraphQL batch query
- scm-github/index.ts — gh() and ghInDir() helpers
- tracker-github/index.ts — internal gh() helper
This is Phase A1a of experiments/PLAN.md: tracer infrastructure +
migration. The full GhRunner contract (Promise<GhResult>,
GhRunnerError.ghResult on reject, body capture, redaction, 64 KB cap)
lands in A1b along with the scorecard baseline.
Also adds experiments/ reference docs: the v2.3 plan, the gh-CLI call
catalog, two ETag verification writeups, and a trace harness + summary
script.
* docs(experiments): add A1a validation status and A1b blockers
Record the five A1b pre-freeze blockers surfaced by Adil's 1,487-row
baseline and an independent drill run: graphql-batch missing -i,
extractOperation flag mis-bucketing, analyzer not segmenting burn by
reset window, CLI-subcommand opacity (GH_DEBUG=api stderr vs coarse
/rate_limit bracket — not equivalent), and sessionId/projectId not
threaded through plugin callsites. Note bare gh() helper cleanup as
known-open follow-up.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(tracer): close A1b blockers 1-4 — graphql-batch visibility, operation naming, analyzer segmentation
- Add -i flag to executeBatchQuery in graphql-batch.ts and split HTTP
headers from JSON body before parsing, making all gh.api.graphql-batch
rows visible to status and rate-limit analysis (was 186 invisible rows)
- Fix extractOperation() in gh-trace.ts to walk past -* flags before
picking the operation segment, eliminating the gh.api.--method bucket
- Add per-reset-window burn segmentation to both analyzers so runs
straddling a reset boundary produce per-window deltas instead of a
single invalid cross-reset delta
- Add experiment scripts: analyze-trace.mjs (deep trace analysis) and
drill-tracer.mjs (standalone tracer exerciser)
- Document Gap 1 decision in PLAN.md: accept CLI subcommands as opaque
for A1, bracket A2 runs with /rate_limit snapshots for coarse burn
- Add progress timeline to PLAN.md showing A→B→C track dependencies
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs(experiments): add A2 baseline matrix runbook
Practical execution plan for the Phase A2 scenario x scale x topology
matrix: 7 priority cells, per-cell procedure, /rate_limit bracketing
for Gap 1 subcommand burn, output format for baseline.md, and the
scorecard that gates Track B.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(tracer): guard stderr/stdout against undefined, bound operation cardinality
Addresses code review findings:
1. Guard Buffer.byteLength and parseIncludedHttpResponse against
undefined stderr/stdout — fixes 48 SCM test regressions where
mocked execFile paths don't populate stderr
2. extractOperation() now takes only the first path segment of REST
URLs (e.g. "repos" from "repos/acme/repo/pulls/123/...") to keep
operation bucket cardinality bounded and stable across runs
3. Fix A2 runbook /rate_limit snapshots to produce valid JSON using
jq's now|todate instead of appending raw timestamp
4. Add blocker 5 dependency to runbook prereqs and per-session cells
All 140 SCM tests pass (0 failures).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs(experiments): add rate-limiting research artifacts
Baseline measurements, discussion notes, benchmark harness spec,
and updated master plan from two independent trace runs at 5-6 sessions.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat(experiments): add benchmark harness for GH rate-limit measurement
Three modes: setup (spawn sessions, wait for PRs), measure (trace API
calls over a fixed window, produce scorecard), report (recompute from
existing trace). Node.js stdlib only, shells out to ao CLI and gh CLI.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(scm-github): handle 304 Not Modified in ETag guard catch blocks (B1)
`gh api -i` exits code 1 on HTTP 304 responses, causing the catch blocks
in checkPRListETag and checkCommitStatusETag to assume the resource changed
and trigger unnecessary GraphQL batch queries every poll cycle.
Fix: inspect stdout/stderr in the catch block for the 304 status line before
falling back to "assume changed". Also unifies the 304 detection regex to
handle HTTP/1.1, HTTP/2, and HTTP/2.0 status lines, and adds rateLimit
introspection to the batch GraphQL query.
Benchmark result (quiet-steady, 5 sessions, 15 min):
- GraphQL points/hr: 260/5,000 (5%) — down from 820–1,416 pre-fix
- ETag guard 304 rate: 100%
- GraphQL batch calls during measurement: 0
Also fixes the benchmark harness to create placeholder tmux sessions with a
claude symlink so the lifecycle actually polls sessions instead of
short-circuiting to "killed".
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs(experiments): update plan and notes with B1 benchmark results
B1 fix validated at 5, 10, and 20 sessions in quiet-steady state:
- 5 sessions: 260 GraphQL pts/hr (5% budget)
- 10 sessions: 640 pts/hr (13%)
- 20 sessions: 680 pts/hr (14%) — sub-linear scaling confirmed
- 50-session projection: ~800-1000 pts/hr (16-20%)
- ETag guard 304 rate: 100% at all scale points
- graphql-batch calls: 0 during measurement at all scale points
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat(core): log gh wrapper invocations for D1
* fix(core): preserve wrapper logging for dash-prefixed gh args
* feat(core): add gh wrapper cache for PR discovery and issue context (D4)
Add read-through caching to the ~/.ao/bin/gh wrapper, targeting the two
largest agent-side waste buckets identified in D4 analysis:
1. PR discovery (gh pr list --head): infinite TTL for positive results.
598 calls → ~10 per 10-session run (98% reduction).
2. Issue context (gh issue view): 300s TTL.
75 calls → ~20 per 10-session run (73% reduction).
The wrapper now caches successful read-only responses in
$AO_DATA_DIR/.ghcache/$AO_SESSION/ and serves them on subsequent
identical calls. Negative results (empty []) are never cached.
gh pr create populates the PR discovery cache immediately.
Also lifts PATH wrapper installation from individual agent plugins into
session-manager, making it universal for all agents including Claude Code:
- session-manager injects PATH + GH_PATH into every runtime.create()
- session-manager calls setupPathWrapperWorkspace() for all agents
- Removes duplicate buildAgentPath/setupPathWrapperWorkspace boilerplate
from codex, aider, opencode, and cursor plugins
Includes D4 implementation plans in experiments/.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs(experiments): add full capacity discovery (5→50 sessions) and CI churn results
Complete scaling curve measured: 50 sessions uses only ~28% of GraphQL
budget with 100% ETag guard hit rate at every scale. Poll cycle lag
identified as first bottleneck (66s at 50 sessions vs 30s target).
CI churn benchmark shows ETag invalidation is a latency problem, not
a rate-limit problem (+9% GraphQL, +4.4x p50 latency).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs(experiments): record real-agent catastrophe and Track D handoff
5-real-agent run on todo-app exhausted GraphQL bucket in 31 min (~9572 pts/hr,
~37x quiet-steady at the same session count). AO polling consumed ~10 calls;
the rest came from agents themselves via the metadata-only ~/.ao/bin/gh
wrapper, which has no tracing. Captures findings, adds Track D (agent-side
gh consumption) plus B5 (migrate remaining bare gh callsites to
execGhObserved), and includes the runbook + benchmark scripts Adil will
build on for the cross-machine reproduction.
* feat(core): add cache-hit/miss tracing to gh wrapper (D4)
The wrapper trace now logs a cacheResult entry for every cacheable
command: hit, miss-stored, miss-negative, or miss-error. This makes
benchmark runs conclusive — you can count cache hits vs real gh calls
directly from the JSONL trace instead of inferring from rate-limit
deltas.
Bump wrapper version to 0.4.1.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(scm-github): replace repo-scoped Guard 1 with PR-scoped ETag checks (D4)
Guard 1 now checks GET /repos/{owner}/{repo}/pulls/{number} per PR
instead of GET /repos/{owner}/{repo}/pulls?... per repo. This means:
- Only changed PRs flow into the GraphQL batch
- Unchanged PRs are served directly from the enrichment cache
- shouldRefreshPREnrichment returns a refresh plan (prsToRefresh +
cachedResults) instead of a boolean
When 1 of 10 PRs changes, the old guard refreshed all 10 via GraphQL.
Now only the 1 changed PR is fetched; the other 9 are served from cache
at zero GraphQL cost.
Trade-off: more REST guard calls (1 per PR instead of 1 per repo), but
304 responses cost zero rate limit points.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Forward AO_AGENT_GH_TRACE to session runtimes
* revert: remove PR-scoped ETag guards (Change 3)
Reverts 25ae6013. The per-PR Guard 1 added more REST calls (1 per PR
instead of 1 per repo) without meaningful GraphQL savings at 10-session
scale. Core REST delta went from 16 to 142 while GraphQL rate stayed
flat. The repo-scoped guard is sufficient for current workloads.
Preserves the subsequent 6fc64f4f commit (AO_AGENT_GH_TRACE forwarding).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(core): use real gh binary in execGhObserved, bypass wrapper
execGhObserved() was calling bare "gh" which resolved to ~/.ao/bin/gh
(the wrapper) when that directory was in PATH. This caused:
- AO-side gh calls going through the agent wrapper
- All trace rows with aoSession=null polluting the agent trace
- Cache functions silently failing (no AO_SESSION in AO process)
Now strips ~/.ao/bin from PATH and resolves the real gh binary
(e.g. /opt/homebrew/bin/gh) at startup. Cached after first resolution.
AO process → execGhObserved → real gh → AO_GH_TRACE_FILE
Agent process → ~/.ao/bin/gh wrapper → AO_AGENT_GH_TRACE + cache
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(core): harden gh wrapper caching and agent-side tracing
Cache correctness:
- Include --json fields in cache key (prevents stale partial responses)
- Only cache stdout, not stderr (prevents warning contamination)
- Fix trailing newline inconsistency in PR discovery cache
- Support --key=value arg syntax for all cached flags
- Remove PR create cache pre-population (hardcoded fields, no JSON escaping)
- Log miss-write-failed when ao_cache_write fails (previously silent)
Agent trace improvements:
- Add operation field to invocation rows (gh.pr.list, gh.issue.view, etc.)
- Add durationMs, exitCode, ok to cache outcome rows
- Log passthrough for all non-cached code paths (pr/create, default case)
- Replace exec with child process in default case to enable post-call tracing
Bump wrapper version to 0.6.0.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(runtime-tmux): re-export PATH after shell init to survive macOS path_helper
macOS zsh runs path_helper during shell startup which resets PATH,
wiping entries set via tmux new-session -e. This caused ~/.ao/bin
to be lost, so the gh/git wrappers were never intercepting agent
calls — no caching, no tracing, no metadata auto-updates.
Fix: send `export PATH=...` via send-keys after the shell has
initialized but before the launch command, ensuring PATH sticks.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(runtime-tmux): use launch script for PATH re-export instead of send-keys
The previous send-keys approach sent 1000+ literal keystrokes for the
PATH value, which broke terminal input buffers and caused stuck quote
prompts. Instead, include the PATH export in the launch script file
which is executed directly — no terminal buffer issues.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs(experiments): add AO-side gh rate-limit trace report
5-session, 15-minute trace analysis with full call breakdown,
ETag guard effectiveness, anomaly investigation, and ranked
reduction opportunities.
Key findings:
- GraphQL at 41%/hr with 5 sessions (bottleneck at ~12 sessions)
- 47% of calls are individual REST fallbacks that batch should cover
- Review thread GraphQL calls (55/15min) can be folded into batch
- detectPR() and guard failures are working as designed
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs(experiments): add AO rate-limit reduction plan with Step 1
Step 1: Remove individual REST fallback from determineStatus().
110 calls (65 pr view + 45 pr checks) eliminated per 15-min window.
Batch enrichment covers all PRs every 30s — fallback is unnecessary
insurance for an event that never occurred in real traces.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* experiments(m2): drop agent-trace gate for claude-code
Claude Code uses native PostToolUse hooks (.claude/settings.json), bypassing
the ~/.ao/bin/gh PATH wrapper, so AO_AGENT_GH_TRACE stays empty even when
Claude makes gh calls. The previous smoke gate required AGENT_ROWS>0 and
aborted every claude-code M2 batch at smoke.
- limit-finder.sh: add REQUIRE_AGENT_TRACE env + --no-require-agent-trace flag,
gate the AGENT_ROWS integrity check behind it.
- m2-ab-run.sh: bump SMOKE_DURATION to 420s; auto-pass --no-require-agent-trace
when AGENT=claude-code; simplify smoke_check to gate only on AO_ROWS>0
(B1 lives in AO-side scm-github, measured by AO trace).
Follow-up tracked as task #39: instrument Claude's hook/tool path or document
that AO_AGENT_GH_TRACE does not cover Claude Code.
* feat(tracker-github): cache issue reads in-process (5 min TTL)
The lifecycle worker polls getIssue/isCompleted repeatedly for the same
issue across a session. Trace data from a 5-session tier-5 bench run
showed the same (repo, issue) pair fetched 64+ times with >97% duplicate
rate — ~744 of 4,059 AO gh calls in 10 minutes were redundant issue views.
Adds an in-process Map<string, CachedIssue> per createGitHubTracker()
instance, keyed by `${repo}#${id}`, TTL 5 min, bounded to 500 entries
(LRU evict-oldest on overflow).
- getIssue: read-through cache, populate on miss
- isCompleted: routes through getIssue (was a separate narrow gh call)
- updateIssue: invalidate the entry before mutating
- createIssue: unchanged, naturally populates via the existing getIssue
- Failures are not cached
Cache lives inside createGitHubTracker so each create() returns an
isolated cache (test isolation comes for free).
Expected reduction: ~744 → ~15 gh issue view calls per tier-5 run.
Tests: 41 existing + 10 new cache tests, all passing.
* feat(scm-github): cache 5 gh pr view callsites with per-method TTLs
The lifecycle worker repeatedly polls each PR for state, summary, reviews,
and review decision. Trace data showed gh pr view was the single largest
AO-side endpoint at 1,280 calls per 5-session tier-5 run with >97% duplicate
rate (e.g. PR #184 polled 86× for --json state alone in 11.5 minutes).
Adds an in-process per-instance cache inside createGitHubSCM(), keyed by
${owner}/${repo}#${prKey}:${method} so different field-sets stay isolated.
Per-method TTLs balance reduction against staleness on decision-influencing
fields:
- resolvePR: 60s (identity metadata only)
- getPRState: 5s
- getPRSummary: 5s (includes state)
- getReviews: 5s
- getReviewDecision: 5s
assignPRToCurrentUser, mergePR, and closePR each invalidate the entire PR
cache for that PR after the mutation, so AO never sees stale state from its
own writes. Failures are not cached.
getCIChecksFromStatusRollup and getMergeability are intentionally NOT cached
here — those need ETag-based revalidation, not blind TTL, and will land
separately.
Expected reduction: ~1,165 of ~1,280 gh pr view calls per tier-5 run.
Tests: 73 existing + 12 new cache tests, all 153 passing.
* feat(scm-github): cache CI checks, mergeability, pending comments, detectPR
Completes the AO-side hot-read caching alongside the prior PR view cache.
All use 5s TTL per the approved policy for decision-influencing fields —
well under one lifecycle poll cycle so state transitions are still seen
next pass.
- getCIChecks (gh pr checks): 5s TTL
- getMergeability (composite pr view + CI + state): 5s TTL on the composite
- getPendingComments (gh api graphql review threads): 5s TTL —
ETag doesn't help on GraphQL per Experiment 2
- detectPR (gh pr list --head BRANCH): 5s TTL, POSITIVE-ONLY.
Empty results are never cached so a freshly created PR is discovered
on the very next poll. The branch-keyed cache entry is invalidated
by mergePR/closePR alongside the number-keyed entries.
Combined with the prior PR view cache, covers the top 6 AO-side gh
operation categories that accounted for ~85% of calls in tier-5 traces.
Tests: 85 existing + 9 new cache tests, all 162 passing.
* experiments(m2): parse REPO from yaml before using it in banner
m2-ab-run.sh referenced $REPO in the header banner before parsing it,
causing 'unbound variable' abort under 'set -u'. Parse it right after
CONFIG_FILE is set.
* test(core): mock full Issue shape in plugin-integration cleanup tests
After tracker-github routed isCompleted() through getIssue() to share
the issue cache, these mocks needed the full Issue shape (number, title,
body, url, state, stateReason, labels, assignees) instead of the narrow
{state} shape that worked when isCompleted made its own --json state call.
* perf(scm-github): tune cache TTLs based on trace replay
Replayed feat run1 + main run2 tier-5 traces (4059 + 1748 rows, 38 min, 5
sessions each) against the shipped cache logic. Three TTLs were materially
under-tuned for the actual lifecycle poll cadence:
- detectPR: 5s → 30s (was 0.5% hit rate; per-branch poll cadence
is ~90s, so 5s caught nothing. 30s catches
intra-cycle bursts when multiple sessions
share a branch. Positive-only stays.)
- getReviewDecision: 5s → 10s (within "10-30s TTL or ETag" policy)
- getPendingComments: 5s → 10s (same policy class)
All three are still well under one poll cycle; freshness contract unchanged
in practice. Other TTLs (5s on state/CI/mergeability, 60s on resolvePR,
5min on issue) hit the targets they were set for and stay as-is.
Replay results before/after:
- feat run1: 53.7% → 57.8% reduction (2179 → 2345 hits of 4059 calls)
- main run2: 47.4% → 52.6% reduction
- Net: ~55% AO-side gh calls eliminated across both traces
Adds experiments/cache-replay.mjs — a counterfactual replay tool that
walks an execGhObserved JSONL trace and simulates per-method cache hits
with the shipped TTLs. Useful as a regression check when tweaking cache
policy.
Tests: 162/162 passing.
* docs(experiments): add cache freshness check runbook
Seven-step manual runbook to validate the cache TTL contract doesn't
cause workflow lag. Covers each cached method with:
- exact gh CLI trigger command
- what to observe in the dashboard / lifecycle log
- pass/fail threshold (TTL + 30s poll cycle)
Companion to experiments/cache-replay.mjs — replay measures how much
we saved, runbook measures whether we lost anything in the process.
* docs(experiments): add Step 2 — consolidate review comment fetching
Single GraphQL call replaces GraphQL + REST for review comments.
Include comment data in agent reaction message to eliminate
agent-side gh read calls. Update future steps.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs(experiments): add duplicate API traffic analysis
Three independent sources hit GitHub API for the same PRs:
1. Dashboard serialize.ts — individual REST calls, no batch, no cache
2. CLI lifecycle manager — batch + guards
3. Web lifecycle manager — same batch + guards, 3s offset
~50% of all API traffic is pure duplication. Dashboard and dual
lifecycle managers are the root causes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs(experiments): add full cache architecture to duplicate traffic analysis
Three independent cache layers across two processes with zero shared
state. Web process creates its own plugin registry, SCM plugin, lifecycle
manager, and dashboard cache — all hitting GitHub independently.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs(experiments): add shared PR enrichment plan
Persist batch enrichment + review comments to session metadata files.
Dashboard reads from disk instead of making its own GitHub API calls.
Remove web's duplicate lifecycle manager.
Eliminates ~268 calls / 15 min (58% of all traffic). Dashboard data
gets fresher (30s vs 5min). Single writer (CLI lifecycle), web only reads.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs(experiments): update Step 1 — remove all three fallback paths
Remove fallback in determineStatus(), maybeDispatchCIFailureDetails(),
and maybeDispatchMergeConflicts(). All three follow the same pattern:
batch cache hit → use it, cache miss → skip (wait 30s for next batch).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* docs(experiments): promote Step 3 (remove dead reviews field) + detail Step 5 (issue caching)
Step 3: Remove reviews(last: 5) from batch query — fetched but never
consumed, reduces GraphQL complexity on every batch call.
Step 5: Persist issue data to session metadata at spawn — eliminates
27 gh issue view calls per 15 min (both processes re-fetch independently).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat(core): remove individual REST fallback from lifecycle polling
Remove fallback paths in determineStatus(), maybeDispatchCIFailureDetails(),
and maybeDispatchMergeConflicts() that made individual REST calls when the
batch enrichment cache missed. The batch runs every 30s — a cache miss
means the data arrives on the next cycle, not that it's lost.
Also add populatePREnrichmentCache() call to check() so single-session
checks also use the batch path.
Eliminates ~110 individual pr view/pr checks calls per 15-min window
(24% of all AO-side traffic).
* feat(core): consolidate review comment fetching into single GraphQL call
Add getReviewThreads() to SCM interface — returns all review threads
(human + bot) with isBot flag from a single GraphQL query. Lifecycle
manager splits locally for separate reaction pipelines.
- Eliminates the REST getAutomatedComments() call (40 calls / 15 min)
- Reaction messages now include inline comment data (file, line, author,
body, URL) so agents don't need to re-fetch via gh api
- Default config messages updated to not tell agents to call gh
- getAutomatedComments kept as optional for backward compatibility
* perf(scm-github): remove unused reviews(last: 5) from batch query
The batch query fetched reviews with author, state, submittedAt but
the data was never consumed — only used in a validation check.
The reviewDecision scalar field provides everything AO needs.
Reduces GraphQL complexity cost on every batch call.
* docs(experiments): add post-optimization trace report (Steps 1-3)
5-session, 17-minute trace after removing REST fallback, consolidating
review comments, and removing dead reviews field.
Results: GraphQL 35%/hr (was 41%), REST <1% (was 3%), automated
comment REST calls eliminated. 54% of remaining traffic is redundant
(duplicate lifecycle manager + dashboard individual calls).
* docs(experiments): add trace file gist link to post-optimization report
* feat(core,web): shared PR enrichment — dashboard reads from metadata
CLI lifecycle manager now persists batch enrichment data and review
comments to session metadata files (prEnrichment + prReviewComments
keys). The web dashboard reads from metadata instead of calling
GitHub API.
Changes:
- lifecycle-manager: add persistPREnrichmentToMetadata() after poll,
write prReviewComments in maybeDispatchReviewBacklog()
- serialize: replace enrichSessionPR (6 API calls) with metadata read
- services: stop web lifecycle polling (keep for webhook checks)
- cache: remove prCache (no longer needed)
- routes: remove timeout wrappers and cacheOnly pattern
Eliminates ~237 calls / 15 min (54% of all AO-side traffic).
Dashboard data freshness improves from 5min to 30s.
* fix(web): remove unused beforeEach import in serialize test
* docs(experiments): add final trace report — 56% GraphQL reduction achieved
5-session, 24-min trace after all optimizations including shared
enrichment. GraphQL 905/hr (was 2,072), REST 5/hr (was 168).
Single lifecycle manager confirmed. Dashboard API calls eliminated.
Max sessions before budget exhaustion: ~27 (was ~12).
* docs(experiments): add REST budget breakdown to final report
* fix(core): use storageKey for getSessionsDir in persistPREnrichmentToMetadata
* fix(test): use OpenCodeSessionManager type in plugin-integration tests
* fix(test): update bugbot-comments and auto-cleanup tests for new review API
* fix(web): fix syntax error and missing import from rebase
* docs(experiments): add complete rate-limiting change log and update final report numbers
* fix(web): fix tmux session resolution for legacy wrapped storageKeys
* fix(web): pass tmuxName directly to terminal server instead of reverse-resolving
* perf(core): gate detectPR behind Guard 1 ETag — skip when PR list unchanged
* perf(core): always run Guard 1 for all repos, dedup issue views, include threadId in review messages
* feat(core): add Guard 3 (review ETag), enrich review data with summaries, dedup issue views, gate detectPR for all repos
* fix(web): reuse cached tmuxSessionId on re-open, add 15-session trace report and comparison docs
* perf(scm-github): reduce contexts to first:10, add -i to review GraphQL for rate limit tracing
* feat(core): merge CI details into transition, enrich merge conflict message, reduce batch contexts, add graphqlCost tracing
* chore(experiments): remove working artifacts, keep final reports and reference docs
* chore: remove experiments directory
* refactor: remove getAutomatedComments from SCM interface and all implementations
* fix: address all PR review comments
- gh-trace: make binary resolution async via fs.access (no event loop
blocking, no shell injection), cache mkdir for trace writes, async
fire-and-forget appendFile, document 10MB maxBuffer rationale
- lifecycle-manager: log detectPR failures via observer instead of
silent catch, add getPRState fallback for terminal states
(merged/closed) when batch enrichment cache misses
- scm-gitlab: implement getReviewThreads with bot+human threads and
isBot flag, fixing silent feature regression after
getAutomatedComments removal
- scm-github: clear reviewThreadsCache in invalidatePRCache, document
first:11 CI checks cost budget
- runtime-tmux: use printf+JSON.stringify for PATH export to prevent
shell injection from single quotes
- agent-workspace-hooks: add cache timestamp sanity check, include
--repo in cache keys to prevent cross-repo collisions
- services: document dashboard dependency on CLI polling
- tests: update gh binary path assertions for resolved paths
* fix: address all 17 PR review comments
- gh-trace: use path.delimiter, cache-only-on-success, last HTTP status
line, await writes with warn-once, redact secrets, gate JSON.parse
- agent-workspace-hooks: validate cache keys, redact wrapper trace args,
sha256 cache keys to prevent collisions, 120s TTL ceiling
- session-manager: skip PATH wrappers for claude-code (native hooks)
- types: add deprecation JSDoc for getReviewThreads
- graphql-batch: clear Guard 3 in clearETagCache, re-read ETag on 304,
switch Guard 2 to check-runs endpoint, drop per_page=1 from Guard 3
- Add gh-trace unit tests for extractOperation, redactArgs, parseHttp
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: adil <adil.business4064@gmail.com>
Co-authored-by: iamasx <adilshaikh4064@gmail.com>
Represent missing activity probes as first-class signal states so lifecycle inference only treats valid idle evidence as proof. This prevents false stuck transitions, keeps API/UI lifecycle truth aligned, and makes root monorepo verification deterministic by serializing recursive build and typecheck.
Renames all npm package scopes from @composio/* to @aoagents/* and
updates GitHub repo references from ComposioHQ/agent-orchestrator
to aoagents/ao throughout the codebase.
- All package.json names and dependencies
- README badges, links, and install instructions
- Documentation references
- Changeset config
- Source code imports and test files
* fix(lifecycle): reduce GitHub API rate limiting from batch enrichment bypass
Three optimizations to prevent API storms in the lifecycle manager poll cycle:
1. **CRITICAL - maybeDispatchMergeConflicts**: Gate the getMergeability()
fallback to only run when batch enrichment didn't run at all. Previously
it called getMergeability() (3 REST calls) whenever hasConflicts was
undefined, even when the batch had already fetched PR data. Now uses
cachedData.hasConflicts ?? false when the batch ran.
2. **HIGH - maybeDispatchCIFailureDetails**: Use batch enrichment ciChecks
when available instead of calling getCIChecks() (separate REST call)
on every poll. The GraphQL batch query now fetches statusCheckRollup
contexts (individual check names, statuses, URLs) alongside the rollup
state. Falls back to getCIChecks() only when batch didn't run.
3. **MEDIUM - maybeDispatchReviewBacklog**: Throttle getPendingComments +
getAutomatedComments API calls to at most once per 2 minutes per session.
These were called every 30s even when nothing had changed.
Impact: ~8-10 API calls/PR/poll reduced to ~2-4, enabling 3-4x more
concurrent sessions before hitting GitHub's 5,000/hr REST limit.
Also extends PREnrichmentData with ciChecks?: CICheck[] and adds
parseCheckContexts() helper to graphql-batch.ts for parsing CheckRun
and StatusContext nodes from the GraphQL statusCheckRollup.contexts field.
* fix(scm-github): fall back to getCIChecks() when contexts list is truncated
When a PR has >20 CI checks, contexts(first: 20) silently truncates the
list. Setting ciChecks to undefined when pageInfo.hasNextPage is true
ensures maybeDispatchCIFailureDetails falls back to the getCIChecks()
REST call, which returns all checks without truncation.
Also adds pageInfo { hasNextPage } to the contexts GraphQL query so
truncation can be detected.
* fix(lifecycle): prune lastReviewBacklogCheckAt in pollAll cleanup loop
Add the new throttle map to the existing pruning loop that removes stale
entries for sessions no longer in the session list. Previously the map
was only cleared on terminal status transitions, leaving orphaned entries
for sessions removed externally (killed + cleaned up without transition).
* fix(lifecycle): bypass throttle on review transition; fix StatusContext conclusion
Two fixes for automated review findings:
1. Bypass review backlog throttle when a transition reaction just fired for
humanReactionKey or automatedReactionKey. The transitionReaction branch
needs to read the current fingerprint via the API to record
lastPendingReviewDispatchHash. Without bypassing, the throttle prevents
this write and the next unthrottled poll sees a stale (empty) hash,
clears the reaction tracker, and fires a duplicate dispatch.
2. Set conclusion on StatusContext nodes in parseCheckContexts() to match
the REST getCIChecksFromStatusRollup() format (rawState.toUpperCase()).
The CI failure fingerprint includes c.conclusion ?? '', so inconsistent
conclusion values between GraphQL and REST paths caused phantom fingerprint
changes when switching sources, triggering duplicate dispatches.
* fix(scm-github): normalize CheckRun conclusion and map NEUTRAL to skipped
Two consistency fixes in parseCheckContexts() vs the REST path:
1. NEUTRAL conclusion: was mapped to 'passed' (with SUCCESS), but
mapRawCheckStateToStatus() in the REST path maps NEUTRAL to 'skipped'.
Changed to treat NEUTRAL the same as SKIPPED.
2. CheckRun conclusion: was stored as the raw GraphQL string (may be
lowercase). REST getCIChecks/getCIChecksFromStatusRollup always store
conclusion as rawState.toUpperCase(). Now stores rawConclusion which
is already uppercased during the status branching logic.
Both fixes prevent phantom fingerprint changes when maybeDispatchCIFailureDetails
switches between GraphQL batch and REST fallback across poll cycles.
* fix(scm-github): map STALE/NOT_REQUIRED/NONE conclusions to skipped
parseCheckContexts() was mapping these conclusions to 'failed' via the
else fallback, while mapRawCheckStateToStatus() in the REST path
explicitly maps all of them to 'skipped'. Added them to the skipped
branch alongside SKIPPED and NEUTRAL to fully mirror the REST mapping.
* fix(scm-github): map QUEUED/WAITING to pending not running
parseCheckContexts() mapped QUEUED and WAITING CheckRun statuses to
'running', but mapRawCheckStateToStatus() in the REST path maps both
to 'pending'. Only IN_PROGRESS maps to 'running' in the REST path.
Fixes fingerprint inconsistency when switching between GraphQL batch
and REST fallback across poll cycles.
* fix(scm-github): map STARTUP_FAILURE to skipped; guard null pageInfo
- STARTUP_FAILURE conclusion now falls through to the "skipped" branch
(matching mapRawCheckStateToStatus() REST default) instead of the
explicit failure enumeration catch-all
- Null pageInfo guard prevents TypeError from typeof null === "object"
JavaScript quirk when accessing hasNextPage on a null pageInfo field
- Tests added for both cases
* fix(scm-github): map COMPLETED+null conclusion to skipped not passed
When a CheckRun has status COMPLETED and conclusion null, the REST path's
mapRawCheckStateToStatus() converts it to "" which maps to "skipped".
The GraphQL path was incorrectly mapping it to "passed" via !rawConclusion.
Fix: only map rawConclusion === "SUCCESS" to "passed"; null falls through
to the else branch → "skipped", matching the REST path exactly.
## Approach
The orchestrator polling loop previously made individual API calls for each PR's
state, CI status, and review decision - 3 separate calls per PR per poll.
With multiple PRs being monitored, this quickly exhausted GitHub's 5,000-point
hourly rate limit.
This PR implements GraphQL batching using aliases, which allows fetching data
for up to 25 PRs in a single GraphQL query. Additionally, a 2-Guard ETag
strategy is used to skip queries entirely when nothing has changed.
## Implementation
### GraphQL Batching
- `generateBatchQuery()` creates a single GraphQL query with unique aliases (pr0, pr1, pr2...)
- Each PR gets the same set of fields: state, CI status, review decision, mergeability
- Uses inline fragments for union types (CheckRun/StatusContext)
- Variable types: String! for owner/repo, Int! for PR numbers
### 2-Guard ETag Strategy
Before running expensive GraphQL queries, two lightweight REST ETag checks detect if
anything changed:
**Guard 1 (PR List ETag):**
- Checks `/repos/{owner}/{repo}/pulls` with If-None-Match header
- Returns 304 if no changes → skips GraphQL (0 points)
- Detects: New commits, title/body edits, labels, reviews, state changes
**Guard 2 (Commit Status ETag):**
- Checks `/repos/{owner}/{repo}/commits/{sha}/status` per cached PR
- Returns 304 if no changes → skips GraphQL (0 points)
- Detects: CI status transitions (failing → passing, passing → failing, etc.)
### Caching
- LRU caches for PR metadata (max 200 entries), ETags (100/500 entries)
- Cache misses trigger individual API fallback via lifecycle-manager
- No placeholder caching on errors - allows proper fallback behavior
## Impact
- **API reduction:** ~88% fewer REST calls (216 vs 1,800 calls/hour for 5 PRs)
- **GraphQL efficiency:** Batch query fetches 25 PRs for ~40 points vs ~400 for individual calls
- **Polling interval:** Still 30s, but most polls return cached data (0 cost)
- **Fallback:** Individual SCM calls still work for edge cases (permissions, cache misses)
## Testing
- Unit tests for query generation and parsing helpers
- Integration tests for real GraphQL API calls (skipped by default)
- Covers batch failures, partial success, empty arrays, edge cases
* feat(core): add scm webhook contract
Defines a provider-agnostic SCM webhook contract in core types and
config so SCM plugins can verify and normalize inbound webhook events
without reshaping project config later.
* feat(scm): trigger lifecycle checks from github webhooks
Adds GitHub webhook verification and event parsing, exposes a web
webhook endpoint, and routes matching PR/branch events through the
existing lifecycle manager so CI and review reactions update immediately.
* fix(scm): verify github signatures with raw webhook bytes
Preserves the original webhook bytes alongside the decoded payload so
GitHub HMAC verification uses the exact request body while the route
continues to drive lifecycle checks through the existing manager.
* fix(web): wire scm webhook route into main branch services
Restores the main-branch service and route-test wiring while keeping the
new webhook route coverage and scoped lifecycle helper in place.
* fix(webhooks): use singleton lifecycle manager and fail closed on scm API errors
Reuses the existing services lifecycle manager for webhook-triggered checks
so reactions and state transitions don't replay from a fresh instance, and
restores fail-closed behavior for GitHub review comment fetch failures.
* fix(webhooks): tighten project matching and restore scm compatibility methods
Prevents repository-less webhook events from matching all projects, restores
GitHub SCM PR utility methods and CI status rollup fallback, and adds tests
covering the compatibility paths and safer project matching behavior.
* fix(webhooks): pre-check content length and continue on parse errors
Adds an early content-length guard against configured maxBodyBytes before
reading the body and changes candidate parse failures to fail-forward so
one malformed payload path does not abort other valid candidate handling.
* fix(scm-github): parse review-comment timestamps from comment payload
Use comment.updated_at/created_at for pull_request_review_comment webhook
timestamps so normalized events retain temporal data for comment events.
* fix(webhooks): apply early size guard only when all candidates are bounded
Uses the broadest candidate limit for pre-read content-length checks and
skips early rejection when any matching project has no configured limit,
while retaining per-candidate verification limits.
* fix(webhooks): normalize repo matching and skip terminal sessions
Match webhook repository names case-insensitively against configured project
repos and avoid lifecycle checks for terminal sessions when resolving
webhook-affected sessions.
* fix(webhooks): fail forward when lifecycle checks throw
* fix(scm-github): parse push webhook branch and sha
* refactor(scm-github): dedupe cli exec helper wrappers
* fix(scm-github): tighten exec helper type and comment timestamps
* fix(scm-github): prefer head_commit timestamp for push events
* fix(webhooks): tighten repository parsing and helper visibility
* fix(scm-github): ignore non-head refs for push branch
* chore: trigger bugbot rerun
* feat(scm-gitlab): add webhook verification and event parsing
* fix(webhooks): share parser utils and handle check_run branch
* fix(webhooks): ignore gitlab tag refs in ci branch mapping
* fix(scm-gitlab): harden token and tag ref handling
* chore(scm-gitlab): update webhook helpers around bugbot threads
* feat: add PR claim flow for agent sessions
* feat: support PR claim during spawn
* fix: address PR review feedback
* feat: add lifecycle worker automation
* fix: prevent duplicate messages on unconfirmed delivery and deduplicate review prompt
sendWithConfirmation now treats unconfirmed delivery as a soft success
since the message was already sent, preventing the dispatch hash from
staying stale and causing duplicate sends on the next poll cycle.
review-check command now sources its prompt from the lifecycle reaction
config instead of hardcoding it, keeping both paths aligned.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: pass AO_CONFIG_PATH to spawned lifecycle worker and log duplicate-worker early exit
The spawned lifecycle worker now receives AO_CONFIG_PATH in its
environment so it finds the correct config regardless of CWD. Also
added a log message when exiting early due to an existing worker.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: prevent lifecycle worker from clearing metadata on SCM fetch failures
SCM methods (getPendingComments, getAutomatedComments) silently returned []
on error, causing maybeDispatchReviewBacklog to treat fetch failures as
"no comments" and clear all tracking metadata every poll cycle. The worker
appeared to do nothing because it kept resetting its own state.
- SCM methods now propagate errors instead of returning []
- maybeDispatchReviewBacklog distinguishes null (fetch failed, skip) from
[] (confirmed empty, safe to clear)
- pollAll() logs errors instead of silently swallowing them
- Added heartbeat logging and stdout flush before process.exit
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add debug breadcrumbs when review comment fetch fails
Logs a console.debug message when getPendingComments or
getAutomatedComments fails, making it clear the lifecycle loop is
preserving metadata due to a fetch failure rather than genuinely
having no comments.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: scope lifecycle worker polling to its project and add missing test
pollAll() now passes the worker's projectId to sessionManager.list(),
so each per-project lifecycle worker only polls its own sessions. This
prevents duplicate SCM API calls and race conditions in multi-project
configs.
Also fixed misleading test name and added a test verifying that
--no-dashboard alone still starts the lifecycle worker.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: remove dead confirmation check and respect project-level reaction overrides
Removed the unreachable "could not be confirmed" guard from the retry
logic since sendWithConfirmation now returns silently on unconfirmed
delivery.
review-check now resolves the prompt per-session by checking
project-level reaction overrides before falling back to the global
config, matching lifecycle worker behavior.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: close TOCTOU race in lifecycle worker spawn and unexport getRegistry
Write the child PID from the parent immediately after spawn, closing
the window where a concurrent ensureLifecycleWorker could pass the
"not running" check and spawn a duplicate worker.
Also removed the unnecessary export on getRegistry since it has no
external consumers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: always include ao base prompt on spawn
* fix: clear heartbeat on shutdown and add initial delay to confirmation loop
Clear the heartbeat interval in the shutdown handler to prevent it
firing during the stream-flush window after lifecycle.stop().
Move the sleep in sendWithConfirmation to before each check (including
the first), so the runtime has time to reflect the message before
the first confirmation attempt.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: resolve lint errors in lifecycle and session manager
- Replace dynamic delete with object reconstruction in lifecycle-manager
- Add { cause } to re-thrown errors in session-manager for preserve-caught-error rule
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* feat: implement seamless onboarding with enhanced documentation
- Add comprehensive README.md (18KB) with quick start, core concepts, and FAQ
- Add detailed SETUP.md (16.5KB) with prerequisites, integration guides, and troubleshooting
- Add examples/ directory with 5 ready-to-use config templates:
- simple-github.yaml: Minimal GitHub setup
- linear-team.yaml: Linear integration
- multi-project.yaml: Multiple repos
- auto-merge.yaml: Aggressive automation
- codex-integration.yaml: Using Codex agent
- Add environment detection (git repo, remote, branch, auth status)
- Auto-fill prompts with smart defaults from detected environment
- Add prerequisite validation (git, tmux, gh CLI)
- Show actionable next steps and warnings
- Parse owner/repo from git remote automatically
- Detect LINEAR_API_KEY and SLACK_WEBHOOK_URL in environment
- Prompt for Linear team ID when Linear tracker selected
- Format all files with Prettier for consistency
Reduces onboarding time from 30+ minutes to ~5 minutes:
1. Install CLI: `npm install -g @composio/ao-cli`
2. Run init: `ao init` (auto-detects everything)
3. Spawn agent: `ao spawn my-project ISSUE-123`
Users no longer need to:
- Manually parse git remote URLs
- Look up current branch names
- Remember YAML syntax
- Search for Linear team IDs
- Debug missing prerequisites
- ✅ pnpm build - All packages compile
- ✅ pnpm typecheck - No TypeScript errors
- ✅ pnpm lint - No new linting issues
- ✅ pnpm format - All files formatted
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* docs: update installation instructions to reflect npm not yet published
Package is not published to npm yet, so users must build from source.
Updated README.md and SETUP.md to:
- Make 'build from source' the primary installation method
- Add note that npm publishing is coming soon
- Include pnpm as a prerequisite
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* feat: add ao init --auto --smart for zero-config setup
Implements intelligent config generation with project type detection.
## What's New
### ao init --auto
- Zero prompts - auto-generates config with smart defaults
- Detects: git repo, remote, branch, languages, frameworks, tools
- Generates project-specific agentRules based on detected tech stack
### Project Detection
- Languages: TypeScript, JavaScript, Python, Go, Rust
- Frameworks: React, Next.js, Vue, Express, FastAPI, Django, Flask
- Tools: pnpm workspaces, test frameworks
- Package managers: pnpm, yarn, npm
### Rule Templates
Created templates for:
- base.md - Universal best practices
- typescript.md - TS strict mode, ESM, type imports
- javascript.md - Modern ES6+ patterns
- react.md - Hooks, composition, best practices
- nextjs.md - App Router, Server Components
- python.md - Type hints, PEP 8
- go.md - Error handling, defer patterns
- pnpm-workspaces.md - Monorepo commands
### Example Output
```bash
ao init --auto
# Detects:
# ✓ TypeScript + pnpm workspaces
# ✓ React + Next.js
# ✓ Vitest
# Generates:
agentRules: |
Always run tests before pushing.
Use TypeScript strict mode.
Use ESM modules with .js extensions.
Use React best practices (hooks, composition).
Before pushing: pnpm build && pnpm typecheck && pnpm lint && pnpm test
```
## Benefits
- **5 seconds** instead of 5 minutes
- **Zero config knowledge** required
- **Context-aware rules** tailored to your stack
- **Still customizable** - edit the generated config
## Future: --smart (AI-powered)
Flag added but not yet implemented. Will use Claude Code to:
- Analyze CLAUDE.md, CONTRIBUTING.md
- Read CI/CD config
- Generate custom rules based on project patterns
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix: detect repo default branch instead of current branch
Fixes Bugbot issue: "Current branch wrongly suggested as default base branch"
## Problem
detectEnvironment was using `git branch --show-current` to suggest
defaultBranch in the config. If a user ran `ao init` while on a feature
branch like `feat/my-work`, the wizard would suggest that feature branch
as the default, causing agents to branch from the wrong base.
## Solution
Added detectDefaultBranch() function with 3 fallback methods:
1. git symbolic-ref refs/remotes/origin/HEAD (most reliable)
2. GitHub API via gh CLI (if ownerRepo known)
3. Check common branch names: main, master, next, develop
Now EnvironmentInfo tracks both:
- currentBranch: The checked-out branch (for display only)
- defaultBranch: The repo's base branch (for config)
## Testing
Tested on feat/seamless-onboarding branch:
- Current branch: feat/seamless-onboarding (displayed)
- Default branch: main (correctly detected for config)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix: prevent duplicate framework detection in Python projects
Fixes Bugbot issue: "Duplicate frameworks when multiple Python config files exist"
## Problem
When both requirements.txt and pyproject.toml exist and mention the same
framework (e.g., FastAPI), the detection loop added it to the frameworks
array twice, causing duplicate rules in the generated config.
## Solution
Added addFramework() helper that checks if framework already exists before
adding to the array. Also prevents pytest from being set multiple times as
testFramework.
## Testing
Verified with test repo containing both files with FastAPI:
- Before: Would add 'fastapi' twice
- After: Only adds 'fastapi' once ✓
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix: address Bugbot review comments
- Remove redundant conditional in --smart flag (both branches were identical)
- Include templates directory in npm package files
* fix: add existence check for base.md template file
Add existsSync guard before reading base.md to handle missing templates gracefully, consistent with other template file reads.
* fix: use direct tool invocation instead of which command
Replace 'which' with direct tool invocation (tmux -V, gh --version)
for better portability on minimal Linux systems where 'which' may
not be installed.
* fix: address Bugbot review comments
- Simplify gh auth status check to rely on exit code instead of output string
- Remove async from synchronous functions (detectProjectType, generateRulesFromTemplates)
* feat: add setup script for one-command installation
Add scripts/setup.sh that:
- Installs pnpm if not present
- Installs dependencies
- Builds all packages
- Links CLI globally
Updated README with simplified setup instructions using the script.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix: correct npm link command in setup script
Remove incorrect -g flag from npm link command. The correct syntax is to cd into the package directory and run npm link without flags.
* fix: address Bugbot review comments on init command
- Validate --smart flag requires --auto (prevents silent ignore)
- Fix path validation to check user-specified path (not CWD)
These fixes address medium and low severity issues found by Cursor Bugbot
in PR #66 review.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* docs: add DirectTerminal troubleshooting and fix setup script
- Add TROUBLESHOOTING.md documenting node-pty posix_spawnp error
- Update setup.sh to rebuild node-pty from source (fixes DirectTerminal)
- Ensures seamless onboarding with working terminal out-of-the-box
Resolves DirectTerminal WebSocket failures from incompatible prebuilt binaries.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix: resolve variable scope issue in init command validation
- Move path variable outside if block to fix TypeScript scope error
- Only validate path existence if projectId is provided
- Use inline tilde expansion instead of missing expandHome import
Fixes build error that prevented setup.sh from completing.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix: automate node-pty rebuild to eliminate terminal issues
- Add postinstall hook to automatically rebuild node-pty after pnpm install
- Create scripts/rebuild-node-pty.js for automatic rebuild with error handling
- Remove manual node-pty rebuild from setup.sh (now automatic)
This ensures DirectTerminal works correctly on every installation without
manual intervention. Fixes posix_spawnp errors from incompatible prebuilt
binaries across different systems and installations.
Resolves issue where users would encounter blank terminals after setup.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* docs: update TROUBLESHOOTING with automatic node-pty rebuild
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* docs: add comprehensive README with quick start guide
- 3-line magical setup: clone → setup → init → start
- Architecture overview with plugin slots table
- Usage examples and auto-reaction configuration
- Links to detailed docs (SETUP.md, TROUBLESHOOTING.md, examples/)
- Philosophy: push not pull, amplify judgment
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix: resolve ESLint errors in rebuild-node-pty script
- Add scripts directory configuration to eslint.config.js
- Configure Node.js globals (console, process) for scripts
- Remove unused error variable from catch block
Fixes lint CI failure.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix: warn when auto mode uses placeholder repo value
- Detect when 'owner/repo' placeholder is used in --auto mode
- Show warning: 'Could not detect GitHub repository'
- Update next steps to emphasize editing config when placeholder used
- Prevents silent failures when spawning agents with invalid repo
Addresses Bugbot review comment about silent placeholder values.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix: prevent merged PRs from showing "Merge conflicts" status
GitHub returns `mergeable: null` for merged/closed PRs. The SCM plugin
was misinterpreting this as unknown/problematic status, leading to
false "Merge conflicts" warnings in the dashboard.
Fixed in two layers:
1. SCM plugin: Check PR state first in getMergeability(). Return clean
result immediately for merged/closed PRs without querying mergeable.
2. Dashboard: Skip merge conflict display for non-open PRs in
SessionDetail component.
SessionCard already had protection via early return in getAlerts().
Closes: bug described in task description
Tests: Added tests for merged/closed PR cases, all 54 tests passing
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix: only skip mergeability checks for merged PRs, not closed PRs
Addresses review comment: A closed-but-unmerged PR should still have
its mergeability checked accurately. Only merged PRs should skip the
checks since they're already merged.
Changes:
- SCM plugin: Only return clean status for state === "merged"
- Dashboard: Show conflicts for closed PRs (pr.state !== "merged")
- Tests: Updated to verify closed PRs still get checked
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
* feat: add npm publishing support with @composio scope
Set up Changesets for version management, add publish metadata to all 20
packages under the @composio scope, create an unscoped wrapper package
(@composio/agent-orchestrator) for global install, and add a GitHub
Actions release workflow.
- Rename all packages from @agent-orchestrator/* to @composio/ao-*
- Add @composio/agent-orchestrator wrapper (bin shim → @composio/ao-cli)
- Add license, repository, homepage, bugs, files, engines to all packages
- Add .npmrc (access=public), MIT LICENSE file
- Add .changeset/ config with linked versioning for all packages
- Add .github/workflows/release.yml (changesets publish CI)
- Add changeset, version-packages, release scripts to root
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix: exclude private web package from release build
The release script now filters out @composio/ao-web, matching the
workflow's existing exclusion and preventing a Next.js build failure
from blocking npm publishing.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix: resolve dashboard GitHub API rate limiting and PR enrichment issues
This commit addresses critical dashboard performance and reliability issues:
**Core Issues Fixed:**
1. GitHub API rate exhaustion (~84 calls/refresh → ~7-10 calls/refresh)
2. Silent failures showing misleading PR data when rate-limited
3. Missing SessionStatus values ("done", "terminated")
4. Unnecessary enrichment of merged/closed PRs
5. No caching of API responses
**Key Changes:**
- Add "done" and "terminated" to SessionStatus type
- Update getAttentionLevel to correctly classify terminal sessions
- Skip PR enrichment for terminal sessions (merged, done, terminated)
- Implement 60-second TTL cache for PR enrichment data
- Handle rate limit errors gracefully with explicit "unavailable" messages
- Improve default values in basicPRToDashboard (no longer misleading)
- Add orchestrator terminal button to Dashboard header
**Test Coverage:**
- 54 new test cases across 3 test files
- Tests for cache behavior, attention level classification, and serialization
- All tests passing (cache: 9/9, types: 29/29, serialize: 16/16)
**Performance Impact:**
- 10× reduction in API calls (84 → 7-10 per refresh)
- 10× improvement in rate limit exhaustion time
- 60s cache prevents redundant API calls on page refresh
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix: address bugbot comments (cache leak, PR skip, CI alert)
Fixes three issues identified by bugbot:
1. **TTL cache memory leak (Medium)**: Cache only evicted expired entries
on get(), causing unread keys to accumulate indefinitely. Added periodic
cleanup via setInterval (runs every TTL period) with unref() to prevent
blocking process exit.
2. **PR skip condition never triggers (Low)**: Check for merged/closed PRs
was using sessions[i].pr.state which is always "open" (default from
basicPRToDashboard). Fixed by checking cache for merged/closed state
before enrichment, avoiding unnecessary API calls.
3. **SessionCard "0 CI check failing" bug**: When GitHub API fails,
ciStatus is "failing" but ciChecks is empty, showing nonsensical
"0 CI check failing" alert. Fixed to show "CI status unknown" instead
when failCount is 0.
**Tests Added:**
- Cache cleanup interval test (async real timer)
- SessionCard CI status unknown test (verifies no "0 failing" or "ask to fix")
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix: CRITICAL - fix field name mismatch in getCIChecks causing all checks to fail
Root cause of "CI failing" everywhere: scm-github plugin was requesting
non-existent fields from gh CLI, causing all checks to map to "failed".
**The Bug:**
- Requesting: `conclusion` and `detailsUrl` (don't exist in gh pr checks)
- Since `conclusion` was always undefined, every check hit the else clause
and was marked as "failed"
**The Fix:**
- Use correct field names: `state` (contains SUCCESS/FAILURE/PENDING directly)
and `link` (replaces detailsUrl)
- Parse `state` directly instead of looking for non-existent `conclusion`
- Map state values: SUCCESS → passed, FAILURE → failed, PENDING → pending, etc.
**Impact:**
This was the #1 bug causing false "CI failing" status everywhere, not rate
limiting. All PRs with passing CI were incorrectly shown as failing.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix: update plugin-integration tests for getCIChecks field name changes
The getCIChecks fix changed field names from `conclusion`/`detailsUrl`
to `state`/`link`. Updated test mocks to match:
- Changed `conclusion: "SUCCESS"` → `state: "SUCCESS"`
- Changed `conclusion: "FAILURE"` → `state: "FAILURE"`
- Changed `detailsUrl` → `link`
Tests now pass with correct field names.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix: update scm-github plugin tests for correct field names
Updated all test mocks to use correct gh pr checks field names:
- Changed `conclusion: "SUCCESS"/"FAILURE"/etc` → `state: "SUCCESS"/"FAILURE"/etc`
- Changed `detailsUrl` → `link`
- Removed redundant `state: "COMPLETED"` prefix (state contains result directly)
All 52 scm-github plugin tests now pass.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix: apply cached data when skipping enrichment + improve rate-limit detection
Fixes two issues identified in bugbot comments:
1. **Cached terminal PR state never applied** (issue #2807979137):
- When skipping enrichment for merged/closed PRs, we now copy all cached
fields to the session before returning
- Previously the session kept default basicPRToDashboard() values (e.g.,
state: "open"), causing terminal PRs to render with stale data
2. **Rate-limit detection cannot trigger reliably** (issue #2807979141):
- Changed from "all failed" to "majority failed" detection (>= 50%)
- Some SCM methods (like getCISummary) return fallback values instead of
throwing, so allFailed was too strict
- Now detects rate limiting even when some methods return defaults
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix(web): apply partial enrichment data when rate-limited + fix type errors
Addresses bugbot comment #2807998258: Rate-limit detection should not
discard partial successful enrichment data.
**Changes:**
1. Remove early return when mostFailed - continue to apply any fulfilled results
2. Add rate-limit blocker message to mergeability after applying partial data
3. Fix cached data application - use correct field names (unresolvedThreads/unresolvedComments)
4. Add proper type casts for cached ciChecks status field
5. Fix tsconfig to exclude test files from type-checking (jest-dom type extensions
don't work with tsc, but tests run fine with vitest)
**Behavior change:**
- Before: 3+ failed API calls → skip enrichment entirely, show "API rate limited"
- After: 3+ failed API calls → apply any successful results + add blocker message
This allows partial data (e.g., PR state, title, passing CI checks) to be displayed
even when some API calls fail, providing better UX during rate limiting.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix: apply cached data to terminal sessions + always cache partial enrichment
Addresses two new bugbot comments:
1. **Terminal sessions keep stale open PR state** (#2808037050):
- Problem: page.tsx returned early for terminal sessions before checking cache
- Result: Terminal sessions kept basicPRToDashboard() defaults (pr.state="open")
- Fix: Check cache FIRST, apply cached data, THEN skip enrichment for terminal sessions
2. **Partial rate-limit results are never cached** (#2808037054):
- Problem: Caching was gated by `if (!mostFailed)`, so partial data wasn't cached
- Result: During rate-limits, sessions repeatedly re-hit SCM APIs every refresh
- Fix: Always cache enrichment results (including partial data from rate-limited requests)
**Behavior changes:**
- Terminal sessions now show correct cached PR state (merged/closed) instead of "open"
- Partial enrichment data is cached for 60s, reducing API pressure during rate-limit periods
- Updated test expectations to reflect new caching behavior
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* fix: apply all cached fields + allow terminal sessions to enrich once
Addresses two new bugbot comments:
1. **Cached terminal data applied incompletely** (#2808048773):
- Problem: Only copied some fields (state, ciStatus, etc.) but omitted title, additions, deletions
- Fix: Added missing fields when applying cached data
2. **Terminal PRs remain permanently unenriched** (#2808048771):
- Problem: Terminal sessions with no cache never got enriched → kept stale defaults forever
- Fix: Removed the "skip enrichment for terminal with no cache" logic
- Behavior: Terminal sessions now enrich at least once (or when cache expires), then skip subsequent enrichments
**Behavior change:**
- Before: Terminal session without cache → skip enrichment forever → stale data
- After: Terminal session without cache → enrich once → cache for 60s → skip while cached
This ensures terminal sessions get accurate PR data at least once, while still avoiding
unnecessary API calls for sessions that already have fresh cached data.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
* feat: implement SCM and tracker plugins (github, linear)
Implement three plugin interfaces from packages/core/src/types.ts:
- scm-github: Full GitHub PR lifecycle via `gh` CLI — PR detection by
branch, state tracking, CI checks, reviews, review decision, pending
comments, automated bot comment detection (cursor[bot], codecov, etc.),
and merge readiness (CI + reviews + conflicts + draft).
- tracker-github: GitHub Issues tracker via `gh` CLI — issue CRUD,
completion check, branch naming (feat/issue-N), prompt generation,
list/filter/update/create with label and assignee support.
- tracker-linear: Linear issue tracker via GraphQL API (LINEAR_API_KEY) —
issue fetch, completion check, branch naming (feat/IDENTIFIER), prompt
generation, list with team/state/label filters, state transitions via
workflow state resolution, issue creation with teamId config.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address PR review — GraphQL injection, N+1 queries, timeouts, tests
- tracker-linear: use GraphQL variables in listIssues to prevent injection
- tracker-linear: add 30s HTTP timeout to linearQuery
- tracker-linear: fix issueUrl to use workspace slug from project config
- tracker-linear: add labels/assignee support to createIssue
- scm-github: rewrite getPendingComments to use GraphQL reviewThreads
with real isResolved status instead of hardcoding false
- scm-github: simplify getAutomatedComments to single API call (N+1 fix)
- scm-github: add 30s timeout to gh CLI calls
- scm-github: fix parseDate to return epoch instead of fabricating dates
- scm-github: add repo format validation in detectPR
- scm-github: fix getCISummary to not count all-skipped as passing
- tracker-github: add 30s timeout to gh CLI calls
- tracker-github: fix createIssue to not use unsupported --json flag
- Add comprehensive vitest tests for scm-github and tracker-github
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address Codex review — assignee lookup, GraphQL vars, status checks
Iteration 1 fixes (8 issues from Codex review):
- tracker-linear: remove invalid assigneeDisplayName, resolve assignee
by display name to ID via users query after creation
- tracker-linear: add HTTP status code check in linearQuery
- tracker-linear: throw on missing workflow state in updateIssue
- scm-github: use GraphQL variables ($owner, $name, $number) in
getPendingComments instead of string interpolation
- scm-github: guard against empty comment nodes in review threads
- scm-github: use mergeStateStatus in getMergeability (BEHIND, BLOCKED)
- tracker-github: default description to "" in createIssue
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address PR review + lint — error context, GraphQL vars, mergeability
- scm-github/tracker-github: wrap gh() errors with command context + cause
- scm-github: use GraphQL variables ($owner, $name, $number) in
getPendingComments to prevent injection
- scm-github: guard against empty review thread nodes
- scm-github: incorporate mergeStateStatus (BEHIND/BLOCKED) in getMergeability
- tracker-linear: add identifier vs UUID comment in updateIssue
- Fix ESLint preserve-caught-error violations
- Remove unused mockGhRaw in scm-github tests
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add pagination to getAutomatedComments and increase thread limit
- getAutomatedComments: add --paginate flag to REST API call to fetch
all review comments beyond the default 30-item first page
- getPendingComments: increase GraphQL reviewThreads limit from 100 to
250 (GitHub's max for connections)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: use per_page=100 instead of --paginate for JSON-safe pagination
Replace --paginate with -F per_page=100 in getAutomatedComments to
avoid concatenated JSON arrays that break JSON.parse on multi-page
responses. 100 is GitHub's max per_page value.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: filter out resolved threads in getPendingComments
The method name implies it should only return unresolved/pending review
threads. The isResolved data was already fetched via GraphQL but was not
being filtered on, causing resolved threads to be included in results.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* test: add comprehensive tracker-linear test suite (53 tests)
Covers all Tracker interface methods: getIssue, isCompleted, issueUrl,
branchName, generatePrompt, listIssues, updateIssue, createIssue.
Mocks node:https request to simulate Linear GraphQL API responses.
Tests state mapping, error handling, assignee/label resolution, and
GraphQL variable injection.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address 6 bugbot review comments on PR #4
- GraphQL reviewThreads(first: 250) → first: 100 (GitHub max)
- noConflicts false when mergeable is UNKNOWN (not just CONFLICTING)
- updateIssue now handles labels and assignee (not just state/comment)
- Add res.on("error") handler in linearQuery to prevent crashes
- createIssue returns only actually-applied labels, not all requested
- Tests added/updated for all fixes (144 total across 3 plugins)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: make Linear updateIssue labels additive to match GitHub behavior
Linear's issueUpdate replaces all labels, while GitHub's --add-label is
additive. Now fetches existing label IDs first and merges with new ones
before sending to issueUpdate, ensuring consistent behavior across both
tracker implementations.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: default listIssues to open state in tracker-linear
When no state filter is specified, tracker-github defaults to "open"
but tracker-linear was returning all issues. Now defaults to excluding
completed/canceled issues, matching tracker-github behavior.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add Composio SDK support to tracker-linear
Allow users with a COMPOSIO_API_KEY to use the Linear tracker without
a separate LINEAR_API_KEY. The plugin auto-detects which key is available
and routes through either the direct Linear GraphQL API or Composio's
LINEAR_RUN_QUERY_OR_MUTATION tool. All existing queries and response
parsing are reused via a GraphQLTransport abstraction.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address 2 bugbot review comments on PR #4
1. getCIChecks now throws on error instead of silently returning [],
preventing a fail-open where CI appears healthy when we can't fetch
check status. getCISummary catches the error and returns "failing".
2. Handle GitHub's UNSTABLE mergeStateStatus (required checks failing)
as a merge blocker in getMergeability.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: prevent unhandled promise rejection in Composio timeout race
When the timeout wins Promise.race in the Composio transport, the
resultPromise is left without a rejection handler. If the SDK call
later rejects, it becomes an unhandled promise rejection that crashes
Node.js 20+ with --unhandled-rejections=throw.
Attach no-op .catch() to both resultPromise and timeoutPromise before
the race so whichever promise loses has its rejection silently handled.
Also adds plugin integration tests (core -> real plugins -> mocked gh CLI).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: chain error cause in getCIChecks and fix core build
- getCIChecks catch now preserves the original error via { cause: err }
instead of discarding it, matching the pattern used by the gh() helper
- Add tsconfig.build.json to core that excludes __tests__/ from build
compilation, preventing TS5055 errors from circular workspace deps
(integration tests import plugin packages that depend back on core)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: remove circular devDeps, address bugbot comments
- Remove plugin devDependencies from core/package.json that created a
circular dependency (core -> plugins -> core), breaking CI build order
- Document in_progress state as intentional no-op in tracker-github
updateIssue (GitHub Issues only supports open/closed)
- Remove @composio/core optional peerDependency from tracker-linear;
the dynamic import() already handles the missing package gracefully,
and the peer dep was pulling composio + transitive deps into lockfile
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add real integration test for tracker-linear against Linear API
Creates a test that exercises the full tracker-linear plugin lifecycle
against the real Linear API — createIssue, getIssue, isCompleted,
listIssues, updateIssue (comment, close, reopen), generatePrompt,
branchName, issueUrl. Each run creates a throwaway test issue and
trashes it in cleanup. Skipped when LINEAR_API_KEY is not set.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* ci: pass LINEAR_API_KEY and LINEAR_TEAM_ID to integration tests
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: support both LINEAR_API_KEY and COMPOSIO_API_KEY in integration test
The tracker-linear integration test now runs with either credential:
- LINEAR_API_KEY: direct Linear API (full cleanup via trash)
- COMPOSIO_API_KEY: via Composio SDK (cleanup falls back to closing)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: don't pass COMPOSIO_API_KEY to CI integration tests
When both LINEAR_API_KEY and COMPOSIO_API_KEY are set, the plugin
prefers the Composio transport which requires @composio/core SDK.
The SDK isn't installed in CI, so use the direct LINEAR_API_KEY
transport which has no extra dependencies.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: use import.meta.url in vitest config, default createIssue description
- Replace __dirname with import.meta.url-derived dirname in ESM config
- Default createIssue description to "" for defensive safety
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: map CANCELLED and ACTION_REQUIRED CI conclusions to failed
Terminal check conclusions (CANCELLED, ACTION_REQUIRED) were falling
through to "pending", causing the lifecycle manager to wait forever.
Also changed the default else branch to "failed" (fail-closed).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>