* feat(cli): hide terminal session rows by default in session ls
- Filter with TERMINAL_STATUSES unless --include-terminated
- Hint when completed sessions are hidden; docs/CLI.md updated
Made-with: Cursor
* fix(cli): keep session ls JSON inventory stable
Address review concerns by preserving terminal sessions in `ao session ls --json` output while keeping terminal rows hidden by default in text output. Use the core terminal-session predicate, improve hidden-row messaging, and add regression tests for mixed/hinted and JSON behavior.
Made-with: Cursor
* fix(cli): resolve rebase conflicts with latest session ls behavior
Align session command and CLI docs with the current mainline JSON contract (`data` + `meta.hiddenTerminatedCount`) while preserving terminal-filter defaults and updated regression coverage.
Made-with: Cursor
* fix: fail send when killed session delivery is not confirmed
* fix(core): drop requireConfirmation throw that re-introduced duplicate-message bug
The PR's sendWithConfirmation added a throw when confirmation heuristics
did not flip within SEND_CONFIRMATION_ATTEMPTS on a restored session.
But runtimePlugin.sendMessage had already fired, so the throw bubbled up
to the lifecycle manager's catch-all, leaving lastCIFailureDispatchHash
(and its merge-conflict twin) unset. Next poll re-dispatched the same
message — exactly the duplicate-message bug that commit 77685a5 removed.
Real fix for #1074 is preserved: restoreForDelivery still throws when
waitForRestoredSession returns false, which happens before sendMessage
fires. Killed sessions that cannot be revived are still reported as
failures, with no duplicate-send risk.
- sendWithConfirmation no longer takes requireConfirmation; unconfirmed
delivery always returns (soft success).
- prepareSession returns Session (the tuple only existed to drive the
removed throw).
- send's retry predicate reverts to prepared.restoredAt === undefined
&& isRestorable(prepared).
- Adds regression test: restored session + sendMessage fires +
confirmation never flips → send() resolves.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
* feat(cli): filter terminated sessions from ao session ls / ao status by default
Closes#1310. Terminated sessions (killed/terminated/done/merged/errored/cleanup,
plus lifecycle-driven terminal states) are now hidden from `ao session ls` and
`ao status` by default. A dim footer reports how many were hidden and how to
surface them. Pass `--include-terminated` to restore the full list.
JSON output wraps into `{ data: [...], meta: { hiddenTerminatedCount } }` on
both commands so text and machine-readable views tell the same story. This is a
breaking change for script consumers of `--json`; `--include-terminated` is the
escape hatch.
Orthogonal to `-a, --all` (orchestrator visibility, unchanged). Restore of
terminated sessions by id is unaffected — that path goes through `sm.get`, not
`sm.list`.
Docs (`SETUP.md`, `docs/CLI.md`) updated to match. Tests cover both the legacy
status branch and the canonical lifecycle branch of `isTerminalSession`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(cli): drop unused `lc` param in lifecycle-alive test case
ESLint's no-unused-vars rejects unprefixed unused args. The "alive — should
remain visible" branch of the new lifecycle-driven filter test in
`session.test.ts` took `lc` but never touched it. Switch to `()`. Matches the
equivalent case in `status.test.ts`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(core): preserve pr.state=merged when legacy metadata lacks pr= URL
Review blocker on PR #1340: a metadata file with `status=merged` but no `pr=`
URL was still showing as active in `ao session ls` / `ao status` by default.
Root cause: `synthesizePRState()` in lifecycle-state.ts short-circuited to
`{ state: "none" }` whenever no PR URL was present, ignoring the fact that the
legacy `status` column already encodes terminal truth. Once lifecycle was
synthesized as `session.state="idle"` + `pr.state="none"`, `deriveLegacyStatus`
returned `"idle"` and `isTerminalSession()` (lifecycle branch) returned false.
The new CLI filter then let the session through.
Fix: when legacy `status === "merged"` and no URL is available, synthesize
`pr.state="merged", reason="merged"` with `number: null, url: null`. The
terminal signal survives the flat-metadata → canonical-lifecycle round trip.
Also:
- Export `sessionFromMetadata` from the core barrel. CLI tests and external
consumers need it to round-trip metadata through the canonical lifecycle.
- Update CLI `buildSessionsFromDir` helpers to route through `sessionFromMetadata`
so mocked `sm.list()` reflects production reconstruction (the old shortcut
bypassed synthesis entirely and was the reason the bug slipped past the
original test suite).
- Add regression tests: one at the core level (`parseCanonicalLifecycle` for
merged-without-URL) and one integration-style test per CLI command asserting
the reviewer's exact repro produces the expected filtered output.
- One pre-existing test expectation updated: when metadata has `status=working`
and `pr=<url>`, the reconstructed status is `pr_open`, not `working`. That's
what production `sm.list()` has always returned; the test was previously
hiding behind the reconstruction shortcut.
Changeset bumped to include ao-core (patch).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(cli): route review-check helper through sessionFromMetadata
Last remaining test-fidelity shortcut flagged by codex on PR #1340. The
`buildSessionsFromDir` helper in review-check.test.ts fabricated Session
objects by hand, bypassing the canonical lifecycle reconstruction that
production `sm.list()` runs. Doesn't affect review-check's actual behavior
(which reads `session.metadata["pr"]` directly), but aligns this test with
the equivalent helpers in session.test.ts and status.test.ts so future
lifecycle changes don't silently skip this surface.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fixes#1048. ao start used to allocate a fresh `{prefix}-orchestrator-N`
on every invocation instead of reattaching to the previous session, and
the dashboard's orchestrator link pointed at a different id than the
CLI just printed.
Changes:
runStartup (packages/cli/src/commands/start.ts):
- On startup, list existing orchestrators for the project, partition
them into live (runtime still running) and restorable (terminal but
sm.restore()-able) buckets, pick the most-recently-active from the
chosen bucket, and reuse/restore that id instead of spawning a new
one. Only spawn fresh when both buckets are empty.
- Live is preferred UNCONDITIONALLY over restorable — a newer killed
record can never beat an older-but-running one. Without this, a
cross-bucket sort could resurrect a killed record via sm.restore()
while the live orchestrator kept running, leaving two alive.
- Restored sessions get an explicit "(restored)" marker in the CLI
summary so the resurfaced id isn't a surprise.
- The phantom `${prefix}-orchestrator` id constant is removed from
every URL print, browser-open target, and summary line. Everything
now uses the real selected id.
registerStop (same file):
- ao stop now resolves the real orchestrator via sm.list(projectId)
+ isOrchestratorSession filter + most-recently-active sort, then
calls sm.kill on that id. The old phantom `${prefix}-orchestrator`
target never matched a real numbered record, so ao stop was a
silent no-op and the orchestrator kept running on disk between
start cycles. sm.list-failure warning no longer duplicates with the
generic "no orchestrator found" message.
isOrchestratorSession (packages/core/src/types.ts):
- Tightened: legacy bare-id records (`{projectId}-orchestrator` with
no role metadata) are no longer recognized as orchestrators by the
public predicate. This was the source of the dashboard/CLI id
divergence — stale bare records with a different prefix than the
numbered form were leaking into the dashboard's orchestrator list.
session-manager repair (packages/core/src/session-manager.ts):
- Split `isOrchestratorSessionRecord` (permissive, used by cleanup
protection) from a new `isRepairableOrchestratorRecord` (stricter,
used only by repairSingleSessionMetadataOnRead and
repairSessionMetadataOnRead).
- The strict repair predicate accepts role-stamped records, the bare
`{sessionPrefix}-orchestrator` correct-prefix legacy shape, and the
numbered `{sessionPrefix}-orchestrator-N` worktree shape. It
rejects foreign bare names like `{projectId}-orchestrator`, so
those records never get `role: orchestrator` backfilled on read
and therefore can no longer pass `isOrchestratorSession()` in real
`sm.list()` output via the role-metadata branch.
Tests added (~12):
- runStartup: live reuse, restore-on-killed, ignore-stale-bare
legacy records, live-beats-restorable regression, multi-live
reuse, URL fallback when --no-orchestrator.
- ao stop: kills the actual numbered id (not the phantom), handles
multiple orchestrators, tolerates sm.list throwing.
- isOrchestratorSession: rejects stale bare ids without role
metadata; accepts bare ids with role metadata stamped.
- listDashboardOrchestrators: stale bare excluded, numbered live
included, role-stamped legacy included.
- session-manager repair: does not backfill role onto foreign bare-id
records (issue #1048 regression guard).
Unblocks: review comments from cursor[bot] (dead else-if branch,
double messaging, redundant isTerminalSession check) and illegalcall
(cross-bucket sort, repair-backfill bypass of predicate tightening) —
all addressed in-place with the multi-orchestrator model preserved.
Verified: core 606/606, cli 450/450, typecheck clean across core/cli/web.
- New repo-utils.ts with extractOwnerRepo() and isValidRepoString()
shared across detectEnvironment, autoCreateConfig, addProjectToConfig
- Remote regex now supports GitLab subgroup paths (group/subgroup/repo)
- Repo validation accepts multi-segment paths (owner/repo and deeper)
- Updated prompt text to mention group/subgroup/repo format
- Updated ProjectConfig.repo docstring to be provider-neutral
- Tests import from shared helpers instead of duplicating regex
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- detect-env: test GitHub/GitLab HTTPS/SSH remote extraction, unknown
hosts returning null, missing remote, and non-git directories
- repo-validation: test the anchored regex accepts owner/repo, rejects
empty, lone slash, missing segments, whitespace, and nested paths
- config-validation: test SCM/tracker inference skipped when repo is
missing or has no slash, and inferred correctly with owner/repo
- scm-webhooks: test eventMatchesProject returns false when project
has no repo configured
- orchestrator-prompt: existing test covers repo:undefined → "not configured"
- prompt-builder: existing test covers BASE_AGENT_PROMPT_NO_REPO selection
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
afterEach calls vi.restoreAllMocks() which restores the top-level mock
to the real function. Use vi.spyOn on the module namespace instead of
vi.mocked on the destructured import so the mock survives restoration.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The test has no ownerRepo detected, which triggers the interactive
repo prompt. Mock isHumanCaller to false so the prompt is skipped
in the non-interactive test environment.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Move ensureGit() after path dedup check so already-registered paths
return early without requiring git
- Use realpathSync for canonical path comparison (resolves symlinks,
case variants, trailing slashes)
- Bail out with error when both SIGTERM and SIGKILL fail to stop AO
instead of unconditionally unregistering a live instance
- Rewrite path-dedup test to exercise the path-arg branch via
AO_CONFIG_PATH, covering addProjectToConfig's dedup lines
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The test was mocking detectEnvironment with isGitRepo: false, which now
correctly triggers the fail-fast error for non-git directories. Updated
to isGitRepo: true since the test is verifying config generation
defaults, not non-git behavior.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. Replace SIGINT/SIGTERM handlers with a process `exit` handler to avoid
conflicting with the shutdown handler that flushes lifecycle state and
exits with the correct code (130 for SIGINT).
2. Expand dashboard process pattern to match dev mode (next dev, ao-web)
in addition to production (next-server, start-all.js).
3. Only kill dashboard-matching PIDs from lsof output, leaving unrelated
co-listeners (sidecars, SO_REUSEPORT) untouched.
4. Use killDashboardOnPort for the first port attempt too, preventing
blind kills on the configured port when running.json is stale.
5. Add test for mixed-PID filtering on a single port.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. Port scan now uses killDashboardOnPort() which checks `ps -p <pid> -o args=`
for next-server/start-all.js before killing, avoiding collateral damage to
unrelated services on nearby ports.
2. SIGINT/SIGTERM handlers now call process.exit() after killing the child,
restoring default exit behavior so Ctrl+C doesn't leave the parent hung
if the child is unresponsive.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds test for stopDashboard finding an orphaned dashboard on a reassigned
port via the port-range scan fallback. Marks signal handler body with
c8 ignore since it only fires on process termination.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two bugs caused `ao stop` to fail when the dashboard port was auto-reassigned:
1. Ctrl+C did not propagate to the dashboard child process because Node.js
doesn't guarantee signal forwarding. Added SIGINT/SIGTERM/exit handlers
in runStartup() to explicitly kill the dashboard child.
2. stopDashboard() only checked the configured port, missing orphaned
dashboards on reassigned ports. Refactored into killOnPort() helper
and added a port-range scan fallback (up to MAX_PORT_SCAN ports).
Closes#645
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The "errors when multiple projects and no arg" test expects the error
path (not the prompt path) in resolveProject. Set mockIsHumanCaller
to false so the test reliably hits the non-interactive error branch.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Cover the moved isAlreadyRunning() check (non-TTY exit, quit, open,
restart, new orchestrator) and the path-based dedup guard in
addProjectToConfig(). Uses hoisted mocks for isAlreadyRunning,
isHumanCaller, promptSelect, unregister, and waitForExit to ensure
proper test isolation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor(cli): remove lifecycle-worker subprocess model, run polling in-process
Replaces the per-project `ao lifecycle-worker` subprocess with an in-process
polling loop managed inside `lifecycle-service`. All registered projects are
now polled from the single long-lived `ao start` process.
- Drops the `lifecycle-worker` command and its registration.
- Rewrites `lifecycle-service` around a `Map<projectId, ActiveLoop>`,
with SIGINT/SIGTERM/beforeExit graceful shutdown.
- Removes PID-file coordination (PID file, log file, status, etc.) —
these only existed to track subprocess state.
- Per-project error isolation is preserved: the core lifecycle manager's
`pollAll()` already catches per-cycle errors, and stop failures in one
project can't prevent others from stopping.
- Updates `start`/`spawn` callers and tests to drop the PID/logFile shape.
- Adds `lifecycle-service.test.ts` covering idempotency, unknown projects,
error isolation across projects, and graceful stop-all.
Closes#1185
* fix(cli): address bugbot review on lifecycle-service in-process refactor
- `ao spawn` / `ao batch-spawn` no longer call `ensureLifecycleWorker`.
That call used to start `setInterval` polling in the one-shot spawn
process, which (a) kept the CLI alive forever after the session spawned
and (b) duplicated polling already running in `ao start`. Replaced with
a `warnIfAONotRunning()` helper that checks `running.json` and prints a
hint if the orchestrator isn't up.
- `ao stop` no longer calls `stopLifecycleWorker`. That call always ran
against a fresh in-memory map (stop is a separate process from start),
so it always returned false and printed "Lifecycle worker not running"
misleadingly. SIGTERM to the `ao start` PID already triggers the shared
shutdown handler in `lifecycle-service`, which stops every loop.
- Drop duplicated shutdown closure inside `registerSignalsOnce` — signal
handlers now reference `stopAllLifecycleWorkers` directly.
- Update tests accordingly: spawn.test.ts mocks `running-state.getRunning`,
start.test.ts drops `stopLifecycleWorker` expectations.
* fix(cli): drop SIGINT/SIGTERM listeners from lifecycle-service
Installing listeners for those signals removes Node.js's default
"exit on signal" behavior (per Node docs: "its default behavior will
be removed — Node.js will no longer exit"). Since the registered
listener doesn't call process.exit(), the `ao start` process would
hang on SIGTERM with the setInterval timer keeping the event loop
alive forever — effectively breaking `ao stop`.
Default signal handling terminates the process cleanly; the OS
reclaims the interval timer and dashboard child. `stopAllLifecycleWorkers`
stays exported for callers that want explicit cleanup before exit.
* fix(cli): project-scoped spawn warning + flush lifecycle health on exit
Addresses reviewer feedback on PR #1186:
- `warnIfAONotRunning` now takes a projectId and warns not only when no
AO is running, but also when the running instance isn't polling the
target project (e.g. `ao start A` then `ao spawn` in B left users
silent about the fact that B wasn't being polled).
- `running.json` now records only the project this `ao start` actually
polls, not every project in config. Previously this list was a lie —
`ensureLifecycleWorker` is called for the selected project only.
- `ao start` installs SIGINT/SIGTERM handlers that call
`stopAllLifecycleWorkers()` (flushing per-project "stopped" health
state) and then `process.exit()`. Installing the handler safely
requires an explicit exit because SIGINT/SIGTERM listeners remove
Node's default exit behavior.
* fix(cli): remove dead stopLifecycleWorker, add missing test mock
Addresses bugbot comments on PR #1186:
- Delete `stopLifecycleWorker` from `lifecycle-service.ts`. It was only
called from `ao stop` in the old subprocess model; in the in-process
model, SIGTERM to the `ao start` pid + the shutdown handler in
`start.ts` covers cleanup. No production caller remains.
- Add `stopAllLifecycleWorkers: vi.fn()` to `start.test.ts`'s
`lifecycle-service.js` mock. Without it, `vi.mock` replaced the module
and the named import resolved to undefined; the shutdown handler's
try/catch would silently swallow the resulting TypeError, hiding any
regression in how shutdown is wired.
- Update `lifecycle-service.test.ts` to drop references to the removed
export (one test removed, one repurposed as a no-op smoke test for
`stopAllLifecycleWorkers` against an empty active map).
* feat(cli): install-aware update command, startup notifier, and doctor version check
Make `ao update` detect whether AO was installed via npm or git source and
route accordingly: git installs use the existing shell script, npm installs
prompt to run `npm install -g @aoagents/ao@latest`, and unknown installs
print guidance. Add `--check` flag for JSON version info output.
Add a startup version notifier that reads a cached update check (no network
on startup) and prints a one-liner to stderr when outdated. Background
cache refresh runs after command completion via unref'd timer.
Add a version freshness check to `ao doctor` using cached data only (no
network dependency in diagnostics).
Closes#1136
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test(cli): high coverage for update service, command routing, and notifier
Add comprehensive tests per maintainer request for full coverage of all
update paths:
update-check.ts (57 tests):
- isVersionOutdated: major/minor/patch comparisons, equal, newer, missing parts
- detectInstallMethod: all 4 scenarios (git, npm-global, partial, unknown)
- readCachedUpdateInfo: fresh, expired, 23h boundary, version mismatch,
invalid JSON, missing fields, empty file
- fetchLatestVersion: success, 404, 500, network error, timeout, non-JSON,
missing version, non-string version, AbortSignal presence
- checkForUpdate: cache hit, cache bypass with force, stale cache fetch,
cache write on success, no cache write on failure, version match,
unreachable registry, result shape
- maybeShowUpdateNotice: positive print case, not outdated, no cache,
non-TTY, AO_NO_UPDATE_NOTIFIER, CI, AGENT_ORCHESTRATOR_CI,
each skipArg (update/doctor/--version/-V/--help/-h)
- scheduleBackgroundRefresh: does not throw
update.ts (17 tests):
- conflicting flags rejection
- --check: valid JSON output, forces fresh fetch, no side effects
- git: default args, --skip-smoke, --smoke-only, cache invalidation
- npm-global: no script-runner, already up-to-date, registry unreachable
exits non-zero, --skip-smoke warning, forces fresh fetch
- unknown: help message, shows latest version, registry unreachable,
suggests npm command
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(cli): address all review feedback — coverage gaps, code fixes, Codex findings
Code fixes:
- Extract classifyInstallPath() for testable npm-global detection
- Convert IS_TTY from module-level constant to isTTY() function call
(testable without import-time mocking)
- Fix non-TTY npm update silently exiting 0 → now exits 1 so scripts
detect that no upgrade happened (Codex P2)
- Handle pre-release version tags in isVersionOutdated() — strips
suffixes before comparing, handles NaN safely
- Export getCacheDir() and writeCache() for test coverage
Test coverage (69 + 24 + 9 + 3 = 105 tests):
- classifyInstallPath: npm-global (Unix + Windows paths), git, partial, unknown
- isVersionOutdated: pre-release tags, NaN handling, same-with-prerelease
- getCacheDir: XDG_CACHE_HOME override, fallback to ~/.cache
- writeCache: success, EACCES mkdir failure, ENOSPC write failure
- handleNpmUpdate full flow: confirm → spawn success, spawn failure with
sudo suggestion, spawn error (ENOENT), non-TTY exit, flag warning,
user decline, registry unreachable
- --check with unreachable registry still outputs valid JSON
- maybeShowUpdateNotice: happy path print, AGENT_ORCHESTRATOR_CI guard,
each skipArg individually
- scheduleBackgroundRefresh: no-throw, swallows errors
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(cli): move background refresh earlier, distinguish local vs global node_modules
- Move scheduleBackgroundRefresh() before parseAsync() so the fetch runs
in parallel with command execution. Short-lived commands now have a
better chance of seeding the cache before exit. (Codex P2)
- Distinguish global npm installs (lib/node_modules, .pnpm store) from
local project node_modules (npx, linked installs). Local installs now
classify as "unknown" instead of "npm-global" to avoid suggesting
npm install -g to npx users. (Codex P2)
- Add tests: pnpm global store path, nvm global path, npx local path
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(lint): remove forbidden import() type annotations in test mocks
Replace `vi.importActual<typeof import("node:fs")>(...)` with untyped
`vi.importActual(...)` to satisfy @typescript-eslint/consistent-type-imports.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(cli): don't misclassify local pnpm installs as npm-global
Local pnpm projects have node_modules/.pnpm/ which matched the /.pnpm/
heuristic. Replace with /pnpm/global/ check which only matches pnpm's
actual global store path (~/.local/share/pnpm/global/...).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(cli): non-TTY exits 0, remove misleading sudo suggestion
- Non-TTY ao update now exits 0 after printing the command. Exit 1
implied error but the user just needs to run the command manually.
- Remove blanket sudo suggestion on npm failure. npm can exit 1 for
many reasons (network, version conflict, engine mismatch). Just
print the exit code and let the user diagnose.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(cli): pnpm global support, defer background refresh after parseAsync
- Detect pnpm global installs separately from npm global. pnpm users
now see `pnpm add -g @aoagents/ao@latest` instead of the npm command.
New InstallMethod value: "pnpm-global".
- Move scheduleBackgroundRefresh() to .then() after parseAsync() completes.
Prevents in-flight fetch from holding the event loop open during
short-lived commands when the registry is slow/offline. (Codex P2)
- Use info.recommendedCommand instead of hardcoded npm string in
handleNpmUpdate, so the correct package manager command is always used.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(cli): use actual command name in error message, not hardcoded "npm"
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(cli): handle signaled npm update exits
* fix(cli): detect prereleases behind stable releases
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fix test flakiness: Use configurable property descriptor for process.platform
mocking to prevent TypeError on property redefinition
- Fix spawn detection: Return null when caffeinate fails to spawn (child.pid
undefined) instead of returning handle that does nothing
- Fix TypeScript: Make power config optional in OrchestratorConfig interface
to avoid breaking existing consumers (populated post-validation)
- Add test for spawn failure case
Co-Authored-By: Claude <noreply@anthropic.com>
Add idle sleep prevention on macOS using `caffeinate -i -w <pid>` to keep
the Mac awake while AO is running, enabling remote dashboard access (e.g.,
via Tailscale) without the machine going to sleep.
Changes:
- Add `preventIdleSleep()` helper in packages/cli/src/lib/prevent-sleep.ts
- Add `power.preventIdleSleep` config option (defaults to true on macOS)
- Wire sleep prevention into `ao start` command
- Add tests for the helper function and config validation
- Document the feature in README and example config
Note: Lid-close sleep is enforced by macOS hardware and cannot be prevented
by userspace assertions. Use clamshell mode for that use case.
Closes#1072
Co-Authored-By: Claude <noreply@anthropic.com>
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
Add mockClear() before mockReturnThis() in spawn test beforeEach to
prevent call history accumulating across tests. Fixes Bugbot review
feedback about stale mock.calls.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When dashboard is disabled, print `ao session attach <id>` instead of
an unreachable dashboard URL. Fixes Bugbot review feedback.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace raw `tmux attach -t` commands in CLI output with web dashboard
URLs (`http://localhost:{port}/sessions/{sessionId}`) across all four
CLI commands: spawn, session restore, open, and start.
Add `stripHashPrefix` helper in session-utils to extract AO session IDs
from hash-prefixed tmux session names. Remove unused `tmuxTarget`
variable from start command.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Address review feedback:
- Remove duplicate created/spawned messages — use single spinner.succeed
- Add "(if running)" qualifier for dashboard mention since ao spawn
does not start the dashboard itself
- Update test mock to expose spinner for output assertions
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace verbose multi-line spawn output (worktree, branch, raw tmux
attach) with a single-line message that directs users to the dashboard
or `ao session attach <id>` instead of exposing internal tmux session
hashes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When dashboard is enabled, `ao start` now prints the session detail page
URL (e.g. http://localhost:3000/sessions/<id>) instead of the tmux attach
command. Falls back to tmux attach when --no-dashboard is used.
Closes#947
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Use parseAsync() with a .catch() handler in the CLI entrypoint so that
ConfigNotFoundError prints a single actionable message instead of a raw
Node.js stack trace. Add tests covering the error handler and re-throw
paths to satisfy diff coverage requirements.
Fixes#981
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: 2acdb7413776