* 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.
The --interval flag is only relevant when --watch is enabled.
Previously, passing `--interval 0` without `--watch` threw an error,
whereas `--interval 5` was silently ignored. Now, invalid intervals
are ignored when `--watch` is omitted, ensuring consistent behavior.
Also adds a test case to prevent regression.
Cover the following previously-uncovered lines in status.ts:
- 65-69: maybeClearScreen() isTTY branch
- 255-256: maybeClearScreen() call on watch refresh (refreshing=true)
- 262-266: loadConfig() throw → fallback to tmux session discovery
- 269-271: unknown --project flag → process.exit(1)
- 388, 390-396, 398: tracker block (registry.get, listIssues, error catch)
- 402-405: unverified issues warning banner
- 424-436: setInterval guard skips render when already in progress
Also hoists mockGetPluginRegistry as a vi.fn() so individual tests
can override the plugin registry returned by getPluginRegistry.
- Remove unused `resolve` import from plugin.ts
- Use strict equality (`!== null && !== undefined`) instead of `!=` in start.ts
- Attach `cause` to re-thrown errors in plugin install/update rollback
- Fix forbidden `import()` type annotation in plugin test
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Export runSetupAction from setup.ts (fixes TS2459 build error)
- Use rollbackErr as cause in re-thrown errors (preserve-caught-error)
- Remove import() type annotation from test mock (consistent-type-imports)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove unused `resolve` import from node:path
- Add `{ cause: err }` to re-thrown errors (preserve-caught-error rule)
- Fix import() type annotation in plugin test (consistent-type-imports)
Co-Authored-By: Claude Opus 4.6 (1M context) <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>
Fixes all 12 issues identified in the Cursor Bugbot review:
#4 – Setup tests now assert non-interactive mode skips validation and
auto-generates tokens; removed incorrect validateToken call expectations.
#5 – Replaced module-level mutable `tsFailures` in doctor.ts with a
`makeFailCounter()` closure that is local to each command invocation,
eliminating potential state bleed between invocations.
#6 – Both `notify`, `notifyWithActions`, and `post` in notifier-discord
now consistently guard on `effectiveUrl` (which includes thread_id),
not on the raw `webhookUrl`. Removes non-null assertions.
#7/#12 – setup.ts now writes `${OPENCLAW_HOOKS_TOKEN}` as the token
value in the YAML config instead of the raw token, so credentials are
never committed to version control. setup.test.ts already expected this
placeholder; the test was correct, the code was not.
#8 – `ao_batch_spawn` follow-up setTimeout handles are tracked in
`batchSpawnFollowUpTimeouts[]` and cleared when the health service stops,
preventing timer leaks after plugin shutdown.
#11 – Discord 429 Retry-After handling no longer double-delays: a
`skipNextBackoff` flag is set after waiting for Retry-After so the
following iteration skips the standard exponential backoff.
Also removes the unused `yamlStringify` import from setup.ts.
Issues #1/#2/#3/#9/#10 were already correctly addressed in previous commits.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Refactor ensureTmux() to use askYesNo() + tryInstallWithAttempts()
matching the existing ensureGit() pattern — no silent sudo
- Add log line before each auto-install attempt in preflight.checkTmux()
- Update config warning: "will prompt to install at startup"
- Update design doc to reflect interactive consent flow
- Include prior uncommitted changes: interactive install prompts for
git/gh/agent-runtime, ensureGit(), canPromptForInstall(), askYesNo()
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Store board scanner initial setTimeout and clear on stop
- Fix deliver assertion in openclaw-probe test (true -> false)
- Fix Discord thread_id test to check URL query param, not body
- Use yaml Document API to preserve comments in config writes
- Fix setup tests to match actual nonInteractiveSetup behavior
(skips validation, auto-generates missing tokens)
- checkTmux() now attempts auto-install (brew/apt/dnf) before erroring
- Config generation uses runtime: "process" when tmux unavailable
- start.ts tries auto-install during config creation, falls back gracefully
- Fix restart bookkeeping in start-all.ts: slot-based tracking prevents
duplicate children entries that broke cleanup countdown
- Updated design doc reflecting all fixes and review feedback
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix shell injection in writeShellExport: use single-quoted token with
escaped embedded single quotes instead of double-quoted interpolation
- Fix board scanner initial setTimeout not cleared on stop: store the
timeout handle and clear it in the stop handler
- Fix openclaw-probe test asserting deliver:true when code sends false
- Fix Discord notifier thread_id test to check URL query param instead
of body, and remove redundant thread_id from post() body payload
- Remove redundant project-existence check after autoDetectProject
- Wrap SIGTERM in try/catch for restart flow (dead process case)
- Remove unused setCallerContext export from caller-context.ts
- Fix unused vi import lint error in init.test.ts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add vi.mock() for running-state, caller-context, detect-env,
detect-agent, and project-detection modules imported by start.ts
- Without these mocks, vitest threads never exit (open handles)
- Remove init.test.ts test that triggered dynamic import of start.js
- Relax waitForPortAndOpen assertion (port resolution depends on
full dashboard startup which is mocked out)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When users run `ao spawn <project> <issue>`, show a yellow warning
explaining the new syntax (`ao spawn <issue>`) instead of Commander's
raw "too many arguments" error.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- spawn now takes only `[issue]` — project is always auto-detected
- batch-spawn now takes only `<issues...>` — no project prefix
- Commander rejects extra positional args automatically
- Update orchestrator prompt to remove projectId from spawn example
- Rewrite spawn tests to use auto-detected project (single project in config)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Rewrite init.test.ts to match simplified deprecated init command
(old tests referenced removed --output, --auto, --smart flags)
- Add eslint-disable for consistent-type-imports in direct-terminal-ws.ts
(node-pty is optional, must use import() type annotations)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Keep orchestrator control sessions visible in CLI status without counting them as worker sessions, and preserve explicit roles in JSON output for multi-project tooling.
* fix(core): enforce single-owner PR claim consolidation
* refactor: remove dead `takeover` option from claimPR
Since PR consolidation is now automatic (single-owner enforcement),
the `--takeover` flag became dead code. Users passing it got no
error but it had zero effect.
This commit:
- Removes takeover from ClaimPROptions interface
- Removes --takeover flag from spawn and session CLI commands
- Updates orchestrator-prompt.ts examples
- Updates tests to reflect automatic consolidation
Tests: ao-core 403 passing, ao-cli 189 passing (spawn+session)
* fix: remove stale --takeover from Quick Start example
Remove remaining --takeover reference in orchestrator prompt Quick Start section.
* feat(core): document and test asymmetric PR ownership model
- Add JSDoc to claimPR documenting RULE A (exclusive PR->Agent) and
RULE B (Agent->Many PRs) ownership contract
- Add tests for:
- Same session claiming multiple PRs sequentially (RULE B)
- Idempotent re-claim by same owner
- Stale/dead prior owner handoff
- Exclusive PR ownership enforcement (RULE A)
139 tests passing
---------
Co-authored-by: Harsh <harsh@Ubuntu-24-Forrest.lan>
* fix: pause workers on model limits and stabilize session visibility
Detect model limit exhaustion, pause project worker operations until reset, and expose pause state in dashboard/API. Also harden killed-session cleanup and SSE session reconciliation so sessions do not appear ghost-active or disappear until reload.
* fix: satisfy lint in rate-limit pause probe
* fix: address bugbot feedback for pause handling
* fix(lifecycle): prevent infinite re-pause loop for duration-based rate limits
Duration-based rate limits (e.g., 'usage limit reached for N hours') were
causing infinite re-pause loops because they always calculate reset time as
Date.now() + duration, which extends the pause on every poll cycle if the
message remains in terminal output.
Now checks for existing active pause before setting a new one:
- Skips override if same session already has active pause
- Preserves longer pauses from other sessions
Fixes infinite loop described in PR #367 review comment.
* fix(core): export global pause constants to prevent duplication
Export GLOBAL_PAUSE_*_KEY constants and parsePauseUntil utility from
@composio/ao-core so web package can import them instead of hardcoding.
This prevents silent breakage if key values ever change - now there's
a single source of truth.
Addresses review comment on PR #367.
* fix(web): globalPause as first-class state in SSE event flow
globalPause is now part of the same reducer/event flow as sessions,
not derived from provider-specific output text in the UI.
Changes:
- useSessionEvents: manage globalPause alongside sessions in reducer state
- Dashboard: consume globalPause from hook instead of SSR-only prop
- types: re-export GlobalPauseState from shared lib (provider-agnostic contract)
- Tests: 15 new tests proving banner appears/disappears from state updates
alone, regardless of agent model/plugin (Claude Code, OpenCode, Codex)
Design requirements satisfied:
- First-class state in same reducer/event flow as sessions
- Key names sourced from shared core contract via export/import
- Provider-neutral: no Anthropic/OpenAI string coupling in control logic
- State-driven: banner visibility from reducer state updates via SSE
Addresses Bugbot finding: Dashboard pause banner never updates after
initial render (eba24a0b-9e4c-47e3-91c9-7d10be01e3cf)
* fix: remove unused import in test file
* fix(cli): consistent purge default for session kill and stop commands
The kill method's default changed from opt-in (=== true) to opt-out (!== false).
Both session kill and stop commands now use the same logic:
purgeOpenCode = opts.purgeSession === true ? true : opts.keepSession !== true
This ensures consistent behavior across all kill paths.
Adds --keep-session flag to both commands.
Adds regression tests for provider-agnostic behavior verified.
Addresses Bugbot finding: orchestrator start cleanup inconsistent
with new purge default (2bc2535f-b2d1-4f64-96d6-150f97ed8564)
* 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>
* fix: auto-detect free port in ao start <url> config generation
When generating a new config via `ao start <url>`, auto-detect a free
port instead of hardcoding 3000. Reuses `findFreePort` (extracted to
shared web-dir.ts from init.ts). When port was auto-selected, skip the
strict preflight port check and silently find another free port if
needed (race condition).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add polling to listIssues integration test for Linear API consistency
The listIssues test was failing because Linear's API has eventual
consistency — a just-created issue may not appear in list queries
immediately. Other tests in this file already use pollUntilEqual for
the same reason. Use pollUntil to retry until the issue appears.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: restore coupling between MAX_PORT_SCAN and findFreePort
MAX_PORT_SCAN was decoupled from findFreePort after extraction to
web-dir.ts — init.ts kept a local constant only used in messages while
findFreePort had its own default, and start.ts hardcoded 99. Export
MAX_PORT_SCAN from web-dir.ts as the single source of truth, use it as
findFreePort's default parameter, and import it in both callers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>