- Replace CPU-burning spin loops in running-state.ts with async setTimeout
and jittered backoff; make all exports async
- Fix TOCTOU race in acquireLock by extracting tryAcquire helper with
retry loop instead of force-remove-and-retry-once
- Hoist dynamic imports in addProjectToConfig/choice-2 to static imports
- Add try/finally around readline in detectAgentRuntime
- Validate session prefix uniqueness in "Start new orchestrator" menu
- Add ConfigNotFoundError class to @composio/ao-core, replace fragile
string matching in ao start with instanceof check
- Fix setup.sh to recommend `ao start` instead of deprecated `ao init`
- Fix start-all.ts: resolve next binary with fallback, wait for children
on SIGTERM instead of immediate process.exit
Co-Authored-By: Claude Opus 4.6 (1M context) <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>
- Change != null to !== undefined && !== null in caller-context.ts and
session-manager.ts (3 locations) to satisfy eqeqeq lint rule
- Add displayName field to all 4 agent plugin test manifest assertions
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Replace require() with execFileSync in all 4 agent plugin detect()
functions — fixes ReferenceError in ESM-only environments
- Remove non-existent -p flag from orchestrator prompt spawn/batch-spawn
docs — prevents orchestrator agent from generating broken commands
- Return actual port from runStartup() and pass to register() — fixes
running.json storing wrong port when auto-escalation picks a free port
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Reduces onboarding to `npm install -g @composio/ao && ao start`.
- Absorb init and add-project logic into `ao start` with auto-config
creation, environment detection, and project type detection
- Add single-instance tracking via running.json with already-running
interactive menu (human) and info+exit (agent)
- Add caller context detection (human/orchestrator/agent) via TTY and
AO_CALLER_TYPE env var
- Add plugin-based agent runtime detection — no hardcoded binary paths,
each plugin exports detect() and displayName
- Simplify `ao spawn` to just `ao spawn <issue>` with auto-detected
project (backward compat: `ao spawn <project> <issue>` still works)
- Set AO_CALLER_TYPE, AO_PROJECT_ID, AO_CONFIG_PATH, AO_PORT env vars
on all spawned sessions (worker, orchestrator, restore)
- Add `ao config-help` subcommand for config schema reference
- Deprecate `ao init` to thin wrapper, fully remove `ao add-project`
- Register/unregister in running.json on start/stop
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Publish @composio/ao-web dashboard as npm package (removed private flag,
added files field, production entry point, node-pty made optional)
- CLI auto-detects dev vs production mode for dashboard startup
- Use local next binary instead of npx in production start-all.ts
- findWebDir() throws with install-specific guidance instead of returning
broken path
- Fix CI-silent failure: setup.sh and ao-update.sh exit 1 on non-interactive
npm link failure
- Deduplicate detectDefaultBranch into shared cli/lib/git-utils.ts
- Add EACCES permission guidance to README.md and SETUP.md
- Move resolveProjectIdForSessionId to @composio/ao-core
- Update design doc with problems #8-13, changes #9-12, known limitations
section, and expanded test plan
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(lifecycle): implement stuck detection using agent-stuck threshold
The agent-stuck reaction config supported a threshold field (e.g.
"10m"), but determineStatus() never returned "stuck" — there was no
code path that consumed the threshold or transitioned sessions based
on idle time. Sessions would stay parked at pr_open/working forever
even when the agent had been idle for hours.
Added idle-time check in determineStatus(): when getActivityState()
reports "idle" or "blocked" with a timestamp, compare the idle
duration against the agent-stuck.threshold config. If exceeded,
return "stuck" so the reaction system can fire notifications.
Also removed the priority !== "info" guard on transition
notifications, so all priority levels (including info) are routed
through notificationRouting. This lets the config control which
notifiers receive each priority level, rather than silently dropping
info-level transition events.
* fix(lifecycle): add post-PR stuck detection as safety net
The original stuck check in step 2 (before PR checks) can be bypassed
when getActivityState() returns null (session file not found, cache miss,
I/O failure). When this happens, the code falls through to the PR path
which returns 'pr_open' without ever checking idle duration.
Fix: extract isIdleBeyondThreshold() helper and call it in three places:
1. Step 2: before PR checks (fast path, catches most cases)
2. Step 4b: after PR checks return 'pr_open' (safety net)
3. Step 5: after all checks, for agents that finish without a PR
This ensures stuck detection fires even when the JSONL activity detection
fails to return idle state. Sessions can no longer get permanently stuck
at 'pr_open' when the agent has been idle beyond the threshold.
Also removes the debug console.error calls from the previous commit.
* fix(lifecycle): treat reviewDecision 'none' as approved for merge readiness
PRs with no required reviewers never reached 'mergeable' status because
getReviewDecision returned 'none', which was not handled. The lifecycle
poll fell through to 'review_pending' or the default, so merge.ready
never fired and the approved-and-green reaction never triggered.
Also: skip stuck short-circuit when session has an open PR so merge
readiness checks in step 4 can still run. Without this, idle agents
with open PRs get stuck status and never transition to mergeable.
Closes composio#0 (internal fix)
* fix(opencode): classify activity state from session timestamps
* test(lifecycle): cover opencode idle-threshold stuck transition
* fix(lifecycle): preserve global stuck threshold with project overrides
* fix(lifecycle): run PR auto-detection before stuck transitions
---------
Co-authored-by: Harsh <harshb012@gmail.com>
Treat persisted tmuxName as metadata-derived runtime identity when runtimeHandle is missing so active sessions are not misclassified as exited/unknown across agents.
* fix: gate fresh session sends on interactive readiness
Wait for spawning tmux-backed sessions to settle before injecting input, and use OpenCode session timestamps to confirm delivery without relying on pane churn alone.
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
* fix: satisfy lint in opencode send regression helper
Remove unnecessary escaping in generated shell snippets used by the new OpenCode confirmation tests so CI lint passes cleanly.
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
* fix: avoid false opencode delivery confirmation
Only treat OpenCode timestamp confirmation as delivered when updatedAt advances beyond a known baseline, and add regression coverage for transient baseline visibility.
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
---------
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
* feat: add end-to-end observability across core, web, and terminal
Instrument lifecycle/API/websocket flows with correlation-aware metrics and operator health surfaces so the system can self-diagnose and escalate failures.
* fix: realign session restore set and unblock claim PR typecheck
* fix(web): restore project-filtered sessions route after main merge
* fix(core): remove unused orchestrator import to unblock lint
* fix(core): always record lifecycle poll failures and remove dead review branches
* fix(web): harden websocket metrics and reuse SSE observers
* fix(web): preserve primary session API errors when services bootstrap fails
* fix(web): use full orchestrator config type in SSE observer helper
* fix(web): restore project-scoped SSE and guard observability error paths
* fix(web): address remaining Bugbot review gaps
* fix(web): align SSE project attribution and share session project resolver
* fix(web): require request arg for SSE route and align tests
* fix(web): record websocket error disconnects as failures
* fix(web): use path alias for session project resolver import
* fix(web): include active connection count in disconnect metrics
* fix: skip PR auto-detection for orchestrator sessions
Orchestrator sessions sit on the base branch (e.g. master) and should
never own a PR. When the lifecycle worker ran detectPR for these
sessions, any PR whose head branch matched master (e.g. a master->prod
deploy PR) would get incorrectly attached and keep re-attaching after
manual cleanup.
Add a role=orchestrator check to the PR auto-detection guard so
orchestrator sessions are skipped entirely.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add session ID suffix fallback for orchestrator PR auto-detection skip
Pre-existing orchestrator sessions spawned before the role metadata field
was added lack role=orchestrator. Mirror the session-manager pattern by
also checking session.id.endsWith("-orchestrator") as a fallback.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Drop the lifecycle manager's local orchestrator predicate so the final remaining core polling path follows the same shared detection logic as session cleanup, web filters, and CLI status.
Carry legacy project ownership through recovery, drop the redundant dashboard worker filter, and remove the now-unused orchestrator helper so the PR branch stays aligned with the shared per-project orchestration contract.
Preserve the legacy project fallback in the shared session loader and keep the sessions route filtering by project after the clean branch cherry-pick, so the final PR branch matches the verified multi-project behavior.
Recover the owning project from the sessions directory and archived metadata so older session files still resolve to the correct project and reuse the right orchestrator mapping.
* 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
* fix: preserve PR number in recovery and dedupe validation utilities
Bugbot #2908822306: Use parsePrFromUrl utility to correctly extract
PR number from URL instead of defaulting to 0. Applied to both
recovery/actions.ts and session-manager.ts for consistency.
Bugbot #2908822310: Extract safeJsonParse and validateStatus to
shared utils/validation.ts to eliminate duplication between
recovery/validator.ts and session-manager.ts.
Clean rebuild from origin/main with only recovery-specific changes.
* fix: add PR number fallback and remove unused import
Bugbot #2908822306: Add fallback regex to extract PR number from URL
ending when full GitHub URL pattern doesn't match (e.g., non-GitHub URLs).
Bugbot #2908822310: Remove unused SessionStatus import from session-manager.ts
(now imported from utils/validation.ts).
Tests: 406 passed, typecheck clean.
* fix(core): harden recovery action selection and escalation
* fix(core): preserve recovered agent summary metadata
* fix(core): reuse canonical PR type in URL parser
Avoid shadowing the core PRInfo type in the recovery URL parser while keeping the non-GitHub trailing-number fallback covered by tests.
* fix(core): align recovery metadata and session reconstruction
Persist restored timestamps under the canonical metadata key, share
session reconstruction logic between recovery and session loading, and
log the real escalation reason when recovery aborts on retry limits.
* fix(core): keep dry-run escalation reasons accurate
Use the assessment's actual escalation reason in dry-run results so preview output matches real execution behavior for partial-session escalations.
* fix(core): dedupe recovery scanning and honor custom log path
Reuse metadata listing rules in scanner to avoid duplicated session ID filters,
and preserve user-supplied recovery logPath in recovery manager APIs.
* fix(core): align dry-run recovery behavior with real actions
Compute recovery escalation decisions before dry-run returns and route
single-session dry runs through executeAction so action-specific fields
(reason/manual-intervention) are preserved.
* fix(core): derive dry-run recovery report from action execution
Run dry-run recovery classification through executeAction so report actions
match real execution logic, including max-attempt escalation decisions.
* feat: wire lifecycle manager, backlog auto-claim, and dashboard overhaul
- Start LifecycleManager in dashboard server (30s polling) so reactions
actually fire: CI failures, review comments, merge conflicts are now
auto-forwarded to agents
- Add backlog auto-claim poller (60s interval) that watches for issues
labeled `agent:backlog` and auto-spawns agent sessions up to max
concurrent limit (5)
- Add tabbed dashboard UI: Board (kanban), Backlog (issue queue), PRs
- Add issue creation form in dashboard — creates GitHub issues with
`agent:backlog` label for immediate agent pickup
- Add API routes: /api/backlog, /api/issues, /api/setup-labels
- Pass notifier config through plugin registry (slack webhook fix)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add task decomposition layer (classify → decompose → recurse)
Adds LLM-driven recursive task decomposition upstream of session spawning.
Complex issues are broken into atomic subtasks before agents start working.
Each agent receives lineage context (where it fits in the hierarchy) and
sibling awareness (what parallel agents are doing).
Core changes:
- New decomposer module (core/src/decomposer.ts) — classify, decompose,
plan tree, lineage formatting, using Claude API
- Extended SessionSpawnConfig with lineage/siblings fields
- Prompt builder Layer 4: decomposition context (hierarchy + siblings)
- ProjectConfig.decomposer config section with Zod validation
- Tracker plugin: added removeLabels support for label management
CLI:
- `ao spawn <project> <issue> --decompose` flag
- `--max-depth <n>` option for decomposition depth
- Spawns multiple sessions with lineage context for composite tasks
Backlog poller:
- Respects project.decomposer.enabled for auto-decomposition
- Posts plan as issue comment when requireApproval=true
- Auto-spawns subtasks with lineage when requireApproval=false
Config example:
projects:
my-app:
decomposer:
enabled: true
maxDepth: 3
requireApproval: true
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add verification gate — issues stay open until human confirms fix
PR merge no longer auto-closes GitHub issues. Instead:
1. On PR merge: issue labeled `merged-unverified`, stays open
2. Human checks staging, then runs `ao verify <issue>` to close
3. Or `ao verify <issue> --fail` to flag verification failure
Changes:
- services.ts: labelIssuesForVerification() replaces closeIssuesForMergedSessions()
- New CLI command: `ao verify` (verify/fail/list modes)
- New API route: GET/POST /api/verify
- Dashboard: new Verify tab with one-click verify/fail buttons
- ao status: shows count of issues awaiting verification
- Idle session detection + auto-nudge reaction
- Use TERMINAL_STATUSES in batch-spawn dedup check
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: rename decomposerConfig to avoid variable shadowing
Addresses Bugbot medium severity issue where inner variable
shadowed outer from getServices().
* fix: update pnpm-lock.yaml for new @anthropic-ai/sdk dependency
* fix: resolve remaining merge conflicts and syntax errors
- Remove leftover conflict markers in types.ts
- Remove orphaned code in services.ts
- Fix semicolon to comma in config.ts
- Remove unused import in verify.ts
* fix: address final Bugbot issues
- requireApproval path now exits early with continue to prevent
fall-through to in-progress label and session spawned comment
- remove packages/core/package-lock.json (pnpm workspace should only
use root pnpm-lock.yaml)
* fix: idle sessions now transition back to working
When agent resumes activity after being idle, the status correctly
transitions to 'working' instead of remaining stuck in 'idle' state.
* fix(backlog): remove agent:backlog label when claiming issues
When claiming issues from the backlog, the poller now removes the
agent:backlog label in addition to adding agent:in-progress. This
prevents duplicate work if all spawned sessions reach terminal status
and the poller rediscovers the issue.
* fix(test): use Set for TERMINAL_STATUSES mock
The mock for TERMINAL_STATUSES was an array, but the real export is a
ReadonlySet. Changed to use a Set so tests with non-empty sessions won't
crash when calling .has().
* fix(web): resolve backlog/dashboard regressions after branch sync
* fix(web): align dashboard events hook and SSE test mocks
* fix(notifier-openclaw): apply exponential delay from retry index
* fix(integration-tests): align openclaw retry delay expectation
* fix(web): keep dashboard header stats in sync
* fix(openclaw): keep first retry at base delay
---------
Co-authored-by: Agent <agent@example.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Harsh <harsh@Ubuntu-24-Forrest.lan>
Co-authored-by: Harsh <harsh@example.com>
* 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)
* fix: capture exact OpenCode session ID from JSON stream to prevent orphan sessions
- Primary: Extract session_id from opencode run --format json step_start event
- Fallback: Title-based match with newest-first sorting for delayed visibility
- Core: Add atomic reserveSessionId check in spawnOrchestrator to prevent race conditions
- Prevents orphan sessions when multiple spawns use same title or delayed discovery
(cherry picked from commit 42cc0cfa1a)
* fix: capture OpenCode session id from stream output
(cherry picked from commit 42b3bf3ba6)
* fix: respawn orchestrators when stale metadata is left behind
(cherry picked from commit e519628017)
* fix: align respawn guard and opencode test expectation
* fix: remove unused helper from opencode launch path
* fix: remove unused function and align tests with session capture format
- Remove unused buildSessionLookupScript function from opencode agent
- Update integration tests to expect --format json flag in launch commands
- Fix assertions for exec opencode --session wrapper format
* fix: address Bugbot findings on PR #366
- Fix orphaned runtime leak when reuse strategy finds alive runtime but
get() returns null (now destroys the orphaned runtime)
- Restore session ID format validation in fallback script with
isValidId regex check
- Add regression tests for both fixes
* fix: address additional Bugbot findings on PR #366
- Add timestamp helper to fallback sort to avoid NaN from invalid dates
- Add session ID type/format validation to primary capture script
- Add tests for both robustness improvements
* fix: address bugbot follow-ups in send and opencode discovery
* fix(test): update stale integration test expectation for opencode bootstrap
The getLaunchCommand implementation now uses a robust bootstrap pattern
that captures the session ID between 'opencode run' and 'exec opencode --session'.
Updated test to check both parts separately instead of expecting a contiguous
string that no longer exists.
* fix: avoid NaN in sort comparator when both timestamps are missing
The comparator (b.updatedAt ?? -Infinity) - (a.updatedAt ?? -Infinity)
produces NaN when both timestamps are missing because -Infinity - (-Infinity)
is NaN in IEEE 754. A comparator returning NaN violates ECMA-262's
'consistent comparison function' requirement, making sort results
implementation-defined.
Fixed by adding equality check before subtraction:
- If both timestamps are equal (including both -Infinity), return 0
- Otherwise return the difference
* 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: add `ao start <url>` one-command project onboarding
When `ao start` receives a repo URL instead of a project ID, it now:
1. Parses the URL (supports GitHub, GitLab, Bitbucket — HTTPS and SSH)
2. Clones the repo (or reuses if already present with matching remote)
3. Auto-generates agent-orchestrator.yaml with sensible defaults:
- SCM/tracker plugins inferred from URL host
- Default branch detected from local refs
- Project type detected (language, package manager)
- Post-create install commands set based on package manager
4. Starts the orchestrator + dashboard as usual
New core module `config-generator.ts` handles URL parsing, SCM detection,
project info detection, and config generation. All logic is in core (not
CLI) for reusability. 36 unit tests cover URL parsing, SCM detection,
default branch detection, project info, config generation, and clone
target resolution.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address bugbot review feedback
1. Non-JS postCreate fix: Only set postCreate install commands for JS
package managers (pnpm/yarn/bun/npm). Rust/Go/Python projects no
longer get an incorrect "npm install" command.
2. Deduplicate startup logic: Extract shared `runStartup()` helper used
by both URL-mode and normal-mode start flows. Eliminates ~100 lines
of duplicated dashboard+orchestrator startup code.
3. SSH URL matching: Normalize SSH URLs to HTTPS format in
`isRepoAlreadyCloned()` so repos cloned via SSH
(git@github.com:owner/repo) are correctly detected as matching.
4. SCM/tracker always explicit: Always set scm and tracker fields in
generated config so `applyProjectDefaults()` doesn't silently
override with github defaults for non-GitHub repos.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: sanitize repo names for project IDs and session prefixes
Address remaining bugbot comments:
- Add sanitizeProjectId() to handle dots/special chars in repo names
(e.g. "my.app" → "my-app" instead of failing Zod validation)
- Preserve original case for generateSessionPrefix() so CamelCase
detection works (e.g. "DevOS" → prefix "dos", not "dev")
- Strip invalid chars before prefix generation to prevent dots
leaking into sessionPrefix
Also update README and SETUP docs with `ao start <url>` quick
onboarding flow.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: auto-open browser to orchestrator page on `ao start`
Opens the orchestrator session page (/sessions/{id}) instead of the
root dashboard, since the Kanban board is empty on first startup
with no worker sessions yet.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: replace fixed 3s delay with port polling for browser open
Poll the dashboard port via TCP connection attempts (300ms intervals,
30s timeout) instead of a hardcoded setTimeout. Opens the browser only
once the server is actually accepting connections — deterministic
regardless of how long Next.js takes to compile.
Applied to both `ao start` and `ao dashboard` commands.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* test: add CLI tests for start/stop commands
12 new tests covering:
- Project resolution: single project, explicit arg, missing project,
multi-project ambiguity, empty config
- URL argument: reuse existing clone, git clone flow, existing config
detection, clone failure error handling
- Port polling: waitForPortAndOpen polls isPortAvailable before opening
browser (proves deterministic behavior vs fixed delay)
- Stop command: session kill + dashboard stop, graceful missing session
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: deduplicate waitForPortAndOpen into shared utility
Move waitForPortAndOpen to lib/web-dir.ts (alongside isPortAvailable)
so both start.ts and dashboard.ts share a single cancellation-aware
implementation. start.ts now uses AbortSignal like dashboard.ts —
polling stops immediately if the dashboard process exits early.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* fix: use JSONL-based activity detection in lifecycle manager
Replace terminal-output-based detectActivity() with JSONL-based
getActivityState() as the primary activity detection in determineStatus().
Falls back to terminal parsing when getActivityState returns null.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address bugbot review comments on lifecycle JSONL detection
- Pass config.readyThresholdMs to getActivityState() for consistency
- Fix idle-but-running test to exercise fallback path (getActivityState → null)
- Deduplicate identical exited tests; replace duplicate with fallback-path test
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
- BUG-28: Verify no .tmp files left behind after writeMetadata/updateMetadata,
and rapid sequential writes produce correct final state
- BUG-29: Verify restoredAt roundtrips through writeMetadata/readMetadata,
persists in key=value file, and can be set via updateMetadata
- BUG-12: Verify computeStats counts active/idle/ready as working,
excludes exited and null activity
- BUG-13: Verify errored/needs_input/stuck sessions with activity=active
land in respond zone, not working
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The session reconstruction path (readMetadataRaw → metadataToSession)
was not mapping the restoredAt field, so session.restoredAt was always
undefined after a server restart despite the value existing in the
metadata file.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- BUG-28: Use atomic write-then-rename for metadata files to prevent
race condition data loss between concurrent TS and bash writers
- BUG-29: Read and write restoredAt field in metadata so it persists
across server restarts
- BUG-12: Count all non-exited sessions (idle, ready, active) as
"working" instead of only active ones
- BUG-13: Check status-based error conditions (errored, needs_input,
stuck) before activity-based checks in getAttentionLevel() so errored
sessions immediately appear in the Respond Kanban zone
Closes#238
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: handle "invalid issue format" error for ad-hoc spawn
gh CLI returns "invalid issue format" when the issue argument is free-text
rather than a valid issue number/URL. This error was not matched by
isIssueNotFoundError, causing ad-hoc spawns to fail instead of gracefully
falling through to ad-hoc mode.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: sanitize ad-hoc issue text into valid git branch names
When tracker lookup fails (ad-hoc mode), the raw free-text was used
directly as a branch name, causing git to reject it. Now:
- Only use tracker.branchName() when the issue was actually resolved
- Slugify ad-hoc text (lowercase, replace non-alphanumeric with hyphens,
trim, cap at 60 chars) to produce valid branch names
e.g. "fix the cold start issue" → feat/fix-the-cold-start-issue
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: preserve casing for branch-safe issue IDs, only slugify free-text
The slug sanitization was lowercasing all issue IDs without a resolved
issue (e.g. INT-9999 → int-9999). Now only applies sanitization when
the issueId contains characters invalid for git branch names.
Adds tests for slug sanitization, casing preservation, truncation,
empty-slug fallback, and isIssueNotFoundError patterns.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* test: assert tracker.branchName not called for unresolved issues
Adds assertion to existing test that tracker.branchName is NOT called
when the issue wasn't found. Adds end-to-end test for "invalid issue
format" error flowing through spawn with free-text slugification.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: reject '..' in branch-safe check, verify generatePrompt skipped
The isBranchSafe regex allowed '..' which is invalid in git refs.
Also asserts that tracker.generatePrompt is not called when the
issue wasn't resolved, and adds a test for the '..' edge case.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: trim dashes after truncation, not before
Bugbot correctly noted that .slice(0, 60) after dash-trim can
reintroduce a trailing dash. Reorder so trim runs after slice.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Prateek <karnalprateek@gmail.com>
* docs: add demo video and article links to README
Add tweet screenshot for the video demo prominently after the intro,
and link to the full article thread. Replaces the TODO placeholder.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: use article screenshot for README article link
Replace text-only article link with clickable screenshot showing
the article title, preview image, and engagement stats.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: replace demo video screenshot with higher quality version
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Default dangerouslySkipPermissions to true for autonomous agent operation
Spawned agents need to run CLI commands without permission prompts. Changed
the agentConfig.permissions schema default from undefined to "skip" so
--dangerously-skip-permissions is included by default. The orchestrator
session is hardcoded to "skip" regardless of config since it must always
run ao CLI commands autonomously.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Default agentConfig to {} so permissions default propagates
When agentConfig is absent from YAML, .optional() leaves it undefined,
so project.agentConfig?.permissions evaluates to undefined instead of
"skip". Change to .default({}) so Zod always constructs the object and
applies the inner permissions default.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* fix: skip orchestrator sessions during cleanup
The cleanup command could kill the orchestrator's own tmux session
(named {prefix}-orchestrator) because it has no PR or issue, making
it eligible for the "dead runtime" cleanup heuristic.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add role metadata for orchestrator session identification
Store role=orchestrator in metadata during spawnOrchestrator() and
check it in cleanup() for explicit identification. Keeps the name
fallback (endsWith -orchestrator) for pre-existing sessions.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: preserve role in restore path, add integration test
- restore() now preserves the role field when recreating metadata
from archive (previously dropped, breaking role-based detection)
- Add integration test: orchestrator skipped while sibling session
with same closed issue gets killed (verifies real tracker plugin)
- Verified: both unit and integration tests fail without the guard
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: persist role field in metadata, isolate role test from name fallback
Bugbot caught two real issues:
- writeMetadata() and readMetadata() never serialized the role field,
so role=orchestrator was silently dropped on disk
- The role-based test used "app-orchestrator" as session name, so the
name fallback caught it — role check was never actually exercised
Fix: add role to metadata read/write, rename test session to "app-99"
so only the role metadata path can protect it.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>