- 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>
@clack/prompts text() can return undefined when submitted with just
the placeholder value. Guard with typeof check in promptText and
defensive (entered || "") in both callers to prevent .trim() crash.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Anchor repo validation regex with $ to reject trailing junk like
"acme/repo extra" or "acme/repo#frag" — both prompt locations
- Widen detectEnvironment and addProjectToConfig remote regex to
match gitlab.com in addition to github.com, so GitLab repos get
auto-detected owner/repo instead of silently skipping it
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add test for orchestrator-prompt with repo: undefined (verifies
"not configured" fallback instead of showing literal "undefined")
- Add c8 ignore for interactive prompt code in autoCreateConfig and
promptText (same pattern as existing promptConfirm/promptSelect)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Instead of silently omitting the repo field when no GitHub remote is
found, interactively ask the user to enter their owner/repo. This way
users on GitLab or other hosts can provide the correct value during
first-run setup rather than having to manually edit the yaml afterward.
If the user skips the prompt, the config is still valid (repo remains
optional) with a clear warning about what features are unavailable.
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>
The --decompose flag and supporting decomposer module had two
unfixable bugs (#1045): it crashed when ANTHROPIC_API_KEY was unset,
and it created multiple branches/PRs per issue, fragmenting history
and complicating review/merge. Removing the feature instead of
patching either bug.
- Delete packages/core/src/decomposer.ts and all re-exports
- Drop @anthropic-ai/sdk dependency from @composio/ao-core
- Remove --decompose / --max-depth options from ao spawn
- Remove decomposer field from ProjectConfig (zod default-strip
silently ignores existing decomposer: blocks in user yaml)
- Remove lineage / siblings from SessionSpawnConfig and the
prompt-builder Layer 4 block (only used by decomposer)
- Gut the matching backlog reactor branch in web/services.ts
so the polling path no longer hits the same bugs
- Remove decompose parameter from openclaw-plugin ao_spawn tool
- Drop decomposer mocks from web services.test.ts
- Resolve @composio → @aoagents package renaming conflicts
- Add cursor agent to BUILTIN_PLUGINS in plugin-registry.ts
- Add cursor agent to AGENT_PLUGINS in detect-agent.ts
- Add cursor agent import and registration in plugins.ts
- Add cursor agent dependency and import in web services.ts
- Update cursor plugin package naming to @aoagents/ao-plugin-agent-cursor
- Add cursor agent to changeset linked group
- Fix test imports to use new @aoagents package naming
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
Replace hardcoded magic number 3000 across spawn.ts, session.ts,
open.ts, dashboard.ts, and start.ts with a shared DEFAULT_PORT
constant from lib/constants.ts. 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>
* fix: reduce dashboard JS bundle from 1.7MB to 170KB (gzipped) (#792)
Switch `ao start` default from `next dev` (7.6MB uncompressed) to optimized
production builds (128KB per route). Add `--dev` flag for HMR when editing
dashboard UI. Add bundle analyzer, server-only guards, and lazy-load
DirectTerminal via next/dynamic.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: skip dashboard rebuild when assets exist, fix CI timeout
Skip the production build step when .next/BUILD_ID and dist-server/
already exist (e.g. after pnpm build in CI). Add c8 ignore for
untestable process-spawning startup code to fix diff coverage.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* refactor: adopt PR #903 patterns — centralize rebuild logic and add preflight web artifact checks
Extract rebuildDashboardProductionArtifacts into dashboard-rebuild.ts, add
isInstalledUnderNodeModules/assertDashboardRebuildSupported guards, remove
findProcessWebDir, and verify .next/BUILD_ID + dist-server/start-all.js in
preflight. Dashboard command now always uses production server.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: skip production preflight in --dev mode, add coverage for rebuild helpers
- Skip preflight.checkBuilt() when --dev is passed (dev mode uses HMR,
doesn't need .next/BUILD_ID or dist-server/start-all.js)
- Add c8 ignore to dashboard.ts process-spawning code (matches start.ts pattern)
- Add tests for rebuildDashboardProductionArtifacts (success, failure, npm guard)
- Add preflight tests for npm-install web artifact hint paths
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: align preflight skip with actual dev-server condition
Only skip production artifact preflight when both --dev is passed AND
we're in the monorepo (where dev mode actually works). For npm global
installs, --dev is silently ignored and production server runs, so
preflight must still validate .next/BUILD_ID and dist-server/start-all.js.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: match @next/bundle-analyzer version to next@^15.1.0
The @next/* packages follow the Next.js release train. Pin the bundle
analyzer to ^15.1.0 to match the project's next dependency.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: correct recovery command for npm installs and remove redundant guard
- Change stale-build recovery suggestion from "ao update" (which only
works in source checkouts) to "npm install -g @composio/ao@latest"
for npm global installs
- Remove redundant assertDashboardRebuildSupported call in dashboard.ts
since rebuildDashboardProductionArtifacts already calls it internally
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* feat: support multiple concurrent orchestrators with isolated worktrees
- Each orchestrator session gets a numbered ID ({prefix}-orchestrator-N)
and an isolated git worktree, replacing the single shared orchestrator
- reserveNextOrchestratorIdentity atomically reserves the next available
number and detects conflicting project prefix configurations early
- isOrchestratorSession / isOrchestratorSessionName accept optional
allSessionPrefixes for cross-project false-positive prevention
- getProjectPause and resolveGlobalPause track the longest active pause
across all concurrent orchestrators instead of returning the first found
- cleanupWorktreeAndMetadata helper used consistently on all failure paths
including post-launch; system prompt file included in cleanup
- pollBacklog, status.ts, ProjectSidebar, sessions page, global-pause,
project-utils, serialize, and sessions API route all pass
allSessionPrefixes to isOrchestratorSession
- Web sessions/[id]/page.tsx fetches project prefix map and passes it
through prefixByProjectRef to avoid stale closure in fetchProjectSessions
* fix: seed sseAttentionLevels from fresh sessions on full refresh
When scheduleRefresh dispatches a reset action, derive sseAttentionLevels
from the freshly fetched sessions using getAttentionLevel. This clears
phantom entries for removed sessions and seeds correct levels for new
sessions, preventing stale favicon color and attention counts until the
next SSE snapshot.
* fix: merge duplicate @/lib/types imports into single import statement
Combining the separate import type and value import from @/lib/types
into one import to satisfy the no-duplicate-imports lint rule.
- Wrap statSync in try/catch in resolveLocalPluginEntrypoint so
permission-denied errors return null instead of crashing
- Add 30s AbortSignal timeout to refreshMarketplaceCatalog fetch
to prevent indefinite hangs on slow networks
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Export isPluginModule, normalizeImportedPluginModule,
resolvePackageExportsEntry, and resolveLocalPluginEntrypoint from
@composio/ao-core and import them in the CLI instead of maintaining
independent copies that have already diverged.
Removes ~70 lines of duplicated code from plugin-marketplace.ts.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
createRequire resolves relative to the compiled dist/lib/ directory
where the JSON file doesn't exist. Use readFileSync with a path
resolved from the package root (../../src/assets/) which works from
both src/lib/ and dist/lib/.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Import attributes aren't supported with module: "Node16". Switch to
createRequire for the plugin-registry.json import and copy assets
to dist during build.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add `with { type: "json" }` to the plugin-registry.json import to
satisfy Node.js 20's ERR_IMPORT_ASSERTION_TYPE_MISSING requirement.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Node.js v20 requires `with { type: "json" }` for JSON module imports.
This fixes the ERR_IMPORT_ASSERTION_TYPE_MISSING error in the onboarding
integration test.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add top-level "default" key fallback in CLI's resolvePackageExportsEntry
to match core's plugin-registry.ts (fixes local plugin resolution for
packages using exports: { "default": "./dist/index.js" })
- Remove dead exports: MARKETPLACE_PLUGIN_CATALOG, printPluginList,
getAgentFromRegistry, resolveInstalledPluginSpecifier
- Fix incomplete string escaping in plugin scaffold (backslash/newline)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- openclaw-probe.ts: fix trailing ": " when response body is empty —
use conditional interpolation instead of .trim() on the full string
- doctor.ts: split connectivity check and test-notify into separate
try-catch blocks with distinct error messages so failures are
attributed correctly ("Notifier connectivity check failed" vs
"Sending test notifications failed")
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Use consistent regex for OPENCLAW_HOOKS_TOKEN detection and replacement
in shell profile (prevents silent no-ops for non-exported lines)
- Broaden token detection regex to match lines with/without export prefix
and leading whitespace
- Fix misleading --non-interactive help text (token is auto-generated)
- Fix doctor.ts catch block to say "Notifier checks failed" not "load config"
- Fix 204 mock in Discord notifier test (ok: true, not ok: false)
- Fix weak no-duplicate assertion in setup.test.ts (actually count list items)
- Add discord to notifier options comment in config-instruction.ts
- URL-encode threadId in Discord webhook URL construction
- Add aoCwd to required[] in openclaw.plugin.json configSchema
- Add HTTPS recommendation comment to agent-orchestrator.yaml.example
- Add rimraf for cross-platform clean script in notifier-discord
- Rename "Recommended Settings" to "Required: Disable Conflicting Built-in Skills"
with explicit warning in docs
- Add /ao setup post-setup reminder to manually disable coding-agent skill
- Fix misleading README non-interactive example wording
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>