Commit Graph

58 Commits

Author SHA1 Message Date
Priyanshu Choudhary ea8cb406de test(runtime-process): poll for payload instead of fixed sleep
The Windows ConPTY round-trip (named pipe -> pty-host -> pwsh -> findstr
-> rolling buffer) varies from hundreds of ms to seconds depending on
runner load, AV scanners, and cold caches. The previous 1500 ms fixed
sleep flaked on slow Windows runners (observed empty getOutput buffer at
sample time). Replace it with a 10 s deadline poll that checks for the
actual payload substring, robust to both timing variance and incidental
shell banners arriving first.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05 00:57:40 +05:30
Priyanshu Choudhary e071f63e5a Merge origin/main into feat/windows-platform-adapter
Resolves 15 conflicts and reconciles main's storage V2 redesign,
DirectTerminal hooks split, opencode shared cache, and PR refactors
with the branch's Windows platform adapter.

Test suite is fully green on Windows after this merge. Changes:

Mechanical/portable fixes:
- Path-separator-agnostic regex matchers in spawn.test.ts and
  update-check.test.ts (Windows uses backslashes).
- Fixed broken char-class regex in script-runner.test.ts path escape.
- Bash matcher accepts both POSIX (`bash`) and Windows (`bash.exe`).
- USERPROFILE override added alongside HOME in filesystem-browse-api
  test (node's os.homedir() reads USERPROFILE on Windows, not HOME).
- Outside-HOME absolute path in browse test is now platform-aware
  (C:\Windows on win32, /etc on POSIX) so realpathSync() resolves.
- Added missing enrichSessionIssue import in serialize.test.ts.
- agent-cursor execFileSync expectation loosened to objectContaining.

Windows-only test skips (with explanatory comments):
- migration-storage-v2.test.ts: 3 describe blocks skipped — they
  migrate FROM the legacy hash-dir layout that only ever shipped on
  Linux/macOS in V1. Future Windows migration coverage would need a
  Windows-shaped fixture rewrite.
- migration-codex-restore.integration.test.ts: same legacy-layout
  reason.
- bun-tmp-janitor.test.ts: startBunTmpJanitor() is a no-op on win32
  (no opencode Windows binary, kernel disallows unlinking mapped
  files).
- start.test.ts \"full stop\" test: now goes through killProcessTree()
  which calls `taskkill /T /F` on win32, not process.kill.
- script-runner.test.ts POSIX-fixture tests: skip on win32.
- filesystem-browse symlink test: skipped on win32 (symlinkSync
  requires admin or Developer Mode).

Windows fs-slowness adjustments:
- agent-report.test.ts: bumped per-test timeout to 30s for the
  audit-trail test (260 atomic-write cycles are slow on Windows due
  to AV scanning of every rename).
- lifecycle-manager.test.ts: replaced fixed 25ms wait with a
  poll-until-called pattern (deadline 2000ms, 10ms intervals) to
  remove a flake under full-suite load on Windows.

Pre-existing main test bugs (skipped, NOT introduced by this merge):
- api-routes.test.ts: 2 tests assert old async (dashboard, scm, pr,
  opts) signature of enrichSessionPR, but commit a8bc7469 on main
  simplified it to a synchronous, single-arg metadata read. Skipped
  with explanatory comment; should be filed as separate main issue.
- page.test.tsx: \"renders inline missing-session state\" references
  an undefined TestErrorBoundary symbol (commit 0538e07b on main
  removed the class but missed these usages). Page now renders 404
  inline rather than throwing, so the test would need a different
  assertion strategy. Skipped; should be filed as separate main
  issue.

Smoke-tested on Windows:
- pnpm build clean, pnpm test green, pnpm typecheck clean.
- ao --version, ao doctor (16 PASS / 1 expected WARN / 0 FAIL),
  ao update --check, ao doctor --help — all working. Bash
  auto-detect resolved to Git Bash and ran ao-doctor.sh via
  spawn() with windowsHide:true.

Follow-ups (additive, not blocking):
- Re-add main's 3 Linux port-scan unit tests in start.test.ts as
  POSIX-only tests (code path is intact in start.ts; only unit-test
  coverage is missing post-merge).
- Add Windows runRepoScript unit tests in a separate
  script-runner-windows.test.ts (branch's vi.mock-heavy tests were
  incompatible with main's real-fs tests).
2026-04-29 04:37:04 +05:30
Harshit Singh Bhandari 36fed87b2e
refactor(core): storage redesign — projectId-based paths, JSON metadata (#1466)
* refactor(core): switch metadata format from key=value to JSON and add V2 path functions

Phase 1-2 of the storage redesign: adds new projectId-based path functions
(getProjectDir, getProjectSessionsDir, etc.) alongside deprecated storageKey-based
ones, and switches metadata serialization from key=value flat files to JSON with
.json extension. Structured fields (runtimeHandle, statePayload) are stored as
proper JSON objects instead of stringified strings within key=value.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor: wire V2 projectId-based paths and remove storageKey system

Switch all consumers from hash-based storage paths to projectId-based
paths (Phase 4) and completely remove the storageKey system (Phase 5).

Phase 4 — V2 path wiring:
- session-manager.ts: all 9 getProjectSessionsDir() calls use projectId
- lifecycle-manager.ts, recovery/scanner.ts, recovery/actions.ts: V2 paths
- portfolio-session-service.ts: JSON metadata + projectId-based paths
- web routes (sessions/[id], projects/[id]): V2 paths
- cli report command: V2 paths
- All test files updated with HOME isolation for parallel safety

Phase 5 — storageKey removal:
- Types: removed storageKey from ProjectConfig, PortfolioProject,
  DegradedProjectEntry
- Schemas: removed from ProjectConfigSchema, GlobalProjectEntrySchema
- Removed: StorageKeyCollisionError, deriveProjectStorageIdentity,
  ensureProjectStorageIdentity, findStorageKeyOwner, relinkProject,
  relinkProjectInGlobalConfig, applyWrappedLocalStorageKeys,
  moveStorageDirectory, countSessionEntries
- CLI: removed `project relink` command
- Web: removed storageKey from settings UI, simplified collision handling
- Simplified registerProjectInGlobalConfig and resolveProjectIdentity

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor(core): restructure SessionMetadata types for storage redesign Phase 3

Complete the typed field restructuring on SessionMetadata:
- statePayload/stateVersion → lifecycle?: CanonicalSessionLifecycle
- runtimeHandle: string → RuntimeHandle (with backward-compat parsing)
- prAutoDetect: "on"/"off" → boolean (with legacy string conversion)
- dashboardPort/terminalWsPort/directTerminalWsPort → nested dashboard object
- LifecycleDecision: flat detecting* fields → nested detecting object

Includes migration command (ao migrate-storage), V2 path functions,
storageKey removal, and updated test plan (to-test.md).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(core): address review findings for storage redesign migration

Fix all HIGH-priority review findings and blockers from external review:
- Detect bare 12-hex hash directories during migration inventory
- Skip observability directories during migration
- Detect V2 tmux session naming patterns for active session check
- Derive status from lifecycle when not stored in migrated JSON
- Fix rollback to preserve storageKey format and post-migration data
- Extract shared flattenToStringRecord utility to avoid duplication
- Handle prAutoDetect "true"/"false" string variants

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(core): update displayName test for JSON metadata format

The upstream displayName test asserted key=value file format and
bare filename. Update to check JSON content and .json extension.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(core): fix runtimeHandle type in upstream restore test

The upstream displayName restore test passed runtimeHandle as
JSON.stringify(makeHandle(...)) — a string. Our type change requires
the RuntimeHandle object directly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(core): address PR review comments

- Handle empty files from reserveSessionId() in mutateMetadata() —
  treat empty/whitespace content as empty record instead of throwing
  on JSON.parse
- Fix archive doc comment: archives live under <sessionsDir>/archive/,
  not <projectDir>/archive/
- Remove migration test file from gitleaks path allowlist — no false
  positives are triggered, so blanket file exclusion is unnecessary

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: use targeted regex instead of path allowlist for gitleaks

Replace the blanket file allowlist with a regex matching the specific
test placeholder hash "abcdef012345" that triggers the generic-api-key
rule. This keeps secret scanning active for the migration test file.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: replace high-entropy test placeholder to avoid gitleaks false positive

Use `aaaaaa000000` instead of `abcdef012345` as the dummy 12-hex-char
hash in migration tests. The old value triggered gitleaks' generic-api-key
rule when combined with `storageKey:` in YAML-like test fixtures. This
eliminates the need for any gitleaks allowlist entry for this file.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(cli): auto-register flat local config in ao start

When running `ao start` in a directory with a flat
agent-orchestrator.yaml (no `projects:` key) that isn't registered
in the global config, the Zod validation error was surfacing as a
raw error dump. Now auto-registers the project in the global config
and retries, matching the behavior of `ao start <path>`.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(core): correct migration error message to use ao session kill

The error message referenced `ao kill --all` which doesn't exist.
The correct command is `ao session kill --all`.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(core): address review findings — worktree paths, archive location, recovery log

- Migration now writes absolute worktree paths instead of relative
  (relative paths resolved against cwd, not project dir, breaking restore)
- Archive directory moved from projects/{pid}/archive/ to
  projects/{pid}/sessions/archive/ to match runtime deleteMetadata behavior
- getProjectArchiveDir() updated to return sessions/archive/ consistently
- fixArchiveFilename() handles sanitized timestamps (dashes replacing colons)
- getRecoveryLogPath() fallback uses AO base dir instead of synthetic
  projects/_recovery/ directory

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(core): update metadata hooks for JSON format and .json extension

Both the Claude Code PostToolUse hook and the PATH wrapper hooks
(gh/git) were constructing metadata paths without .json extension and
using key=value sed to update metadata. This broke after the storage
V2 migration which uses .json files with JSON content.

Changes:
- Try {sessionId}.json first, fall back to bare {sessionId} for
  pre-migration layouts
- Detect JSON format (first char '{') and use jq for updates
- Fall back to key=value sed for legacy metadata files
- Bump WRAPPER_VERSION 0.3.0 → 0.4.0 to force wrapper reinstall

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(core): reset lifecycle on restore and keep killed sessions in active metadata

Two runtime bugs fixed:

1. Restore: lifecycle object was not reset — lifecycle manager read the old
   terminal state and immediately transitioned back to Done. Now resets
   lifecycle to working/alive via cloneLifecycle + buildLifecycleMetadataPatch.

2. Kill: sessions were immediately archived, making them invisible to list()
   and get(). Dashboard showed "Page not found" instead of Done/Terminated.
   Now keeps killed sessions in active metadata with terminal status.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(core): address storage redesign review findings

Fix CI blocker and several correctness/consistency issues found during
review of PR #1466:

1. Fix codex plugin test failures — WRAPPER_VERSION bumped to 0.4.0 in
   agent-workspace-hooks.ts but codex tests still expected 0.3.0

2. Add agentReport and reportWatcher to jsonFields in
   unflattenFromStringRecord — these object fields were missing from the
   known-fields set, causing silent data corruption on mutateMetadata
   roundtrip (object → string → stays string instead of reparsing)

3. Normalize prAutoDetect writes from "off" to "false" in
   session-manager — the JSON round-trip converts "off" to boolean false
   on disk, which flattens to "false" on read-back. Writing "false"
   directly avoids the ambiguity and matches the round-trip behavior

4. Fix STORAGE_REDESIGN.md to match implementation — archive path is
   sessions/archive/ (not a sibling of sessions/), and status is still
   persisted (computed-only deferred to follow-up)

5. Keep detecting fields at top level during migration — the lifecycle
   manager reads detectingAttempts/detectingStartedAt/detectingEvidenceHash
   from session.metadata (top-level), not from lifecycle.detecting.
   Nesting them during migration caused silent reset on first poll

6. Remove to-test.md development artifact (895 lines)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(core): remove unused readMetadata import in lifecycle test

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(core): fix 3 critical migration issues

1. Orchestrator blindness: stop extracting orchestrators to orphaned
   orchestrator.json — write them to sessions/ where runtime reads from.
2. Pre-lifecycle "unknown": preserve status in migrated JSON when no
   statePayload exists, preventing readMetadata fallback to "unknown".
3. Archive timestamp collision: add counter to archive filenames to
   prevent same-millisecond overwrites. Fix dead-code ternary.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(core): eliminate status dual truth, fix jsonFields whitelist, add rollback dry-run and tests

- Status is now computed on read from lifecycle (single source of truth).
  deriveLegacyStatus maps session.reason to specific terminal statuses
  (killed, cleanup, errored) instead of relying on stored previousStatus.
- Remove jsonFields whitelist in unflattenFromStringRecord — auto-detect
  JSON by checking if value starts with { or [. Prevents silent
  stringification of new JSON fields.
- Add dryRun option to rollbackStorage and wire through CLI --dry-run.
- Add 18 tests for V2 path functions (getProjectDir, assertSafeProjectId,
  compactTimestamp, parseTmuxNameV2, etc.).
- Add migration edge case tests: worktree dir migration, pre-lifecycle
  status preservation, archive filename uniqueness, active session blocking.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(core): fix stray worktree recursion, rollback data loss, and worktree path rewrite

- moveStrayWorktrees now recurses into ~/.worktrees/{projectId}/{sessionId}/
  (default workspace plugin layout) instead of only scanning top-level entries
- Rollback checks for post-migration sessions before deleting project dirs,
  preserving sessions created after migration with a warning
- Worktree path rewrite only fires when the destination directory actually
  exists, keeping original paths for worktrees not yet moved

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(core): reset terminal PR state on session restore

When restoring a session whose PR was already merged/closed, the
lifecycle manager would immediately re-detect the merged PR and
terminate the session again — making restore useless for merged sessions.

On restore, if pr.state is "merged" or "closed", reset it to "none"
with reason "cleared_on_restore". This lets the session run freely;
if the agent creates a new PR, auto-detect picks it up normally.
Also clears mergedPendingCleanupSince to prevent stale cleanup.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(core): remove stale previousStatus args and unused SessionStatus import

Two call sites in lifecycle-manager.ts still passed session.status as
a second argument to deriveLegacyStatus and buildLifecycleMetadataPatch
after the previousStatus parameter was removed. Also removes unused
SessionStatus import from metadata.ts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(core): address PR review comments — parser, docs, delete route, prefix sanitization

- parseTmuxNameV2: allow hyphens in prefix to match sessionPrefix
  validation ([a-zA-Z0-9_-]+), fixing "my-app-1" parsing
- SessionMetadata: update stale doc comments — JSON format, no hash prefix
- DELETE /api/projects/[id]: report actual removedStorageDir based on
  whether the directory existed before deletion
- start.ts registerFlatConfig: sanitize projectId before deriving
  sessionPrefix, matching config-generator.ts behavior

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(agent-claude-code): add --dangerously-skip-permissions for all restored sessions

getRestoreCommand only added the flag for orchestrator sessions, but
getLaunchCommand adds it for any session with permissionless/auto-edit.
This caused restored worker sessions to lose permissionless mode.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(core): skip .migrated dirs in inventory to prevent .migrated.migrated on re-run

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(core): rollback worktree preservation, scoped tmux detection, JSON parse whitelist

- Move worktrees back to restored hash dirs before deleting project dir on rollback
- Scope v2OrchestratorPattern to known project prefixes instead of matching any tmux session
- Restrict unflattenFromStringRecord JSON parsing to known structured fields only

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(cli): show "Add this project" option in ao start project picker

When running ao start in a git repo that isn't registered, the project
selector now includes an option to add the current directory as a new
project instead of requiring the user to run a separate command.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(cli): show "Add project" in already-running menu when cwd is unregistered

When AO is already running and the user runs ao start from an
unregistered git repo, the menu now offers to add that directory
as a new project alongside the existing open/restart/quit options.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(cli): add --reports flag to ao status for agent report history

Adds --reports option to `ao status` that displays the agent report
audit trail per session. Accepts "full" for all entries or a positive
integer for the last N entries.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(cli): replace removed storageKey reference with getProjectSessionsDir in status command

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(core,web): address PR review issues — crash safety, atomic ops, corrupt data handling, test fixes

- metadata.ts: handle corrupt JSON gracefully (return null instead of crashing), use atomic renameSync for archive, conditionally persist status only when lifecycle is not an object
- storage-v2.ts: add crash-safety marker file for migration, fix archive filename handling for .json suffix and compact timestamps, use Date parsing for duplicate session resolution
- lifecycle-state.ts: add JSDoc and clarify deriveLegacyStatus default case behavior
- lifecycle-transition.ts: add JSDoc clarifying buildTransitionMetadataPatch scope
- AddProjectModal.test.tsx: fix pre-existing jsdom localStorage mock so saveRecentPath works in tests
- Add tests for corrupt JSON handling, migration markers, and crash recovery

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(core): harden storage-redesign against edge cases (EC-1 through EC-8, EC-14, EC-27)

Address 10 edge cases found during systematic review of storage redesign:

- EC-1: Wrap mutateMetadata read-modify-write in withFileLockSync to prevent race conditions
- EC-2: Replace existsSync+readFileSync TOCTOU pattern with try-catch in readMetadata/readMetadataRaw
- EC-3: Append PID to archive filenames to prevent same-second collision
- EC-4/5: Add crossDeviceMove helper with EXDEV fallback (cpSync+rmSync) for migration renames
- EC-6/13: Restrict project ID validation to [a-zA-Z0-9][a-zA-Z0-9._-]* with 128-char max
- EC-7: Guard rollback rename against pre-existing target directory
- EC-8: Add mtime+path tiebreaker for duplicate session resolution
- EC-14: Fix misleading "Resuming" log message in migration
- EC-27: Extend readMetadataRaw status override to handle statePayload-only sessions

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(core,cli): prevent silent data loss on upgrade — V1 detection, git worktree repair, storageKey preservation

Three P0 fixes for storage-redesign migration UX:

1. Warn on `ao start` when legacy hash-based directories exist,
   telling users to run `ao migrate-storage` before sessions disappear.

2. Run `git worktree repair` from each project's repo root after
   migration moves worktree directories — fixes broken git references
   that would otherwise make git status/push fail inside moved worktrees.

3. Preserve `storageKey` in global config allowlist so it isn't silently
   stripped on load before migration has a chance to use it.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(core): skip active session check during migrate-storage --dry-run

Dry run is read-only — blocking on active sessions defeats the purpose
of previewing what migration would do.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(integration-tests): update archive filename regex for PID suffix

EC-3 appended -p{pid} to archive filenames to prevent same-second
collisions. Update the integration test regex to match the new format.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(core): address final merge review — Zod schema gaps, worktree repair, rollback safety, status priority

5 fixes from final review:

1. Add storageKey to GlobalProjectEntrySchema (Zod) so it survives
   parse→save round-trips until migration strips it.

2. Add 5 missing reason values to lifecycle Zod schemas
   (auto_cleanup, pr_merged, cleared_on_restore, pr_merged_cleanup)
   so lifecycle isn't silently reconstructed from stale status on restart.

3. Run repairGitWorktrees when stray worktrees are moved, not only
   when hash-dir worktrees are moved (was checking wrong counter).

4. Count archived post-migration sessions in rollback safety check
   so rollback warns before silently deleting user's archived data.

5. Fix portfolio-session-service status priority to prefer lifecycle-
   derived status over stored, matching metadata.ts behavior.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(core): harden storage redesign migration rollback

* fix(core,cli,web): allocate suffixed project ids on duplicate names

* fix(core,cli): graceful migration errors + skip orchestrator selector

- Migration: wrap per-project migration in try/catch so one failure
  doesn't abort the entire run. Handle ENOTEMPTY when .migrated target
  already exists from an interrupted previous run.
- CLI: ao start now always opens the selected orchestrator's dashboard
  page directly instead of the orchestrator selector.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(core,cli): align with upstream to reduce merge conflicts

Bump WRAPPER_VERSION from 0.4.0 to 0.6.0 to match upstream's gh CLI
tracer changes (#1238), and update start.test.ts URL assertion to use
canonical orchestrator IDs (no number suffix) per upstream's orchestrator
identity fix (#1487). These pre-merge alignments eliminate 3 of the 11
conflicts when merging upstream/main.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: resolve Phase 1+2 merge conflicts with upstream/main (#1487, #1238)

* fix(core,web): allow restoring merged sessions

Remove "merged" from NON_RESTORABLE_STATUSES and delete the
hasMergedLifecyclePR guard so sessions with merged PRs can be
restored like any other terminal session. Previously clicking
"Restore" on a merged session returned a misleading 409 error
("session is not in a terminal state") — the session was terminal,
just explicitly blocked.

Also fix Dashboard.tsx to show the restore button for merged
sessions and improve the error message in restore() to include
the actual status and activity state.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* Implement hashed project identity

* fix(core,cli,web): address PR #1466 review findings

1. Patch session JSON worktree field after moving stray worktrees
2. Preserve migration marker and skip config stripping on partial failure
3. Sanitize legacy project IDs with unsafe characters during migration
4. Use sed-based JSON update when jq is unavailable instead of corrupting
   JSON metadata with key=value fallback
5. Fall back to flat local config repo during first registration when
   git origin provides no repo identity
6. Return and print effective registered project ID from ao project add
7. Update web route tests to use effective hashed project IDs and fix
   repairWrappedLocalProjectConfig to find entries by content fallback

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(core): use strict equality to satisfy eqeqeq lint rule

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(core,web): address Copilot review comments

1. parseTmuxNameV2: accept digit-leading prefixes to match the config
   schema validation ([a-zA-Z0-9_-]+)
2. DELETE /api/projects/[id]: return 400 for unsafe project IDs instead
   of letting getProjectDir throw into the 500 catch-all
3. POST /api/projects: return structured 409 on collision with
   existingProjectId, suggestedProjectId, and suggestion fields so the
   AddProjectModal collision UI actually works

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(core,workspace): route new worktrees to V2 project directory

The workspace-worktree plugin defaulted to ~/.worktrees/ for all new
worktrees, bypassing the V2 layout entirely. New sessions created
worktrees at ~/.worktrees/{projectId}/{sessionId} instead of
~/.agent-orchestrator/projects/{projectId}/worktrees/{sessionId}.

Add optional worktreeDir to WorkspaceCreateConfig so session-manager
can pass getProjectWorktreesDir(projectId) per spawn/restore call.
The plugin uses this override when provided, falling back to the
plugin-level default for backward compat.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(cli): prefix unused addCwdOption variable to satisfy lint

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(core): address migration review findings and orchestrator tmux double-prefix

Migration (storage-v2.ts):
- Use atomicWriteFileSync for all session JSON writes (crash safety)
- Wrap stripStorageKeysFromConfig in withFileLockSync (concurrency safety)
- Add case-insensitive projectId collision detection (macOS HFS+/APFS)
- Call repairGitWorktrees in rollback path (git worktree ref repair)
- Skip stray worktree moves for failed projects (partial-failure safety)

Session manager:
- Fix orchestrator tmux name double-prefix: getOrchestratorSessionId
  already returns "{prefix}-orchestrator", so tmuxName should use
  sessionId directly, not "${prefix}-${sessionId}"

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor(core): remove archive path functions from paths.ts and index.ts

Remove getProjectArchiveDir, getArchiveFilePath, and compactTimestamp
from V2 path helpers as part of archive system removal. Sessions will
stay in sessions/ with lifecycle.state: "terminated" instead of being
moved to sessions/archive/. Callers in metadata.ts and migration will
be updated in subsequent tasks.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor(core): remove archive logic from metadata.ts

Remove archive system from metadata layer: simplify deleteMetadata to
permanent-only deletion, delete readArchivedMetadataRaw and
updateArchivedMetadata functions, and update unit/integration tests.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor(core): remove archive code from session-manager.ts

Remove all archive-related logic from the session manager now that
terminated sessions stay in sessions/ instead of being moved to an
archive directory.

Changes:
- Remove readArchivedMetadataRaw/updateArchivedMetadata imports
- Delete listArchivedSessionIds and markArchivedOpenCodeCleanup functions
- Remove archive search from findOpenCodeSessionIds
- Remove listArchivedSessionIds from reserveNextSessionIdentity
- Replace archive fallback in kill() with readMetadataRaw + lifecycle check
- Remove archive fallback in restore() (findSessionRecord finds all sessions)
- Replace archive iteration in cleanup() with terminated session iteration
- Remove boolean archive flag from all deleteMetadata calls
- Remove unused readdirSync import
- Update lifecycle and restore tests to use terminated state instead of archive

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(core): stop archiving sessions on cleanup in recovery/actions.ts

Remove the deleteMetadata call that archived sessions after marking them
terminated. Sessions now remain in sessions/ with terminated state.
Also remove the now-unused deleteMetadata import.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(core): flatten archives into sessions/ during migration instead of copying to archive dir

Remove the archive system from storage-v2 migration: old V1 archives are now
flattened into sessions/ as terminated session records instead of being copied
to sessions/archive/. Duplicate sessions across hash dirs are skipped with a
warning instead of being archived. Remove fixArchiveFilename(), compactTimestamp
import, archives field from result types, and archive counting from rollback.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* refactor(core): remove archive directory filter from listMetadata

The isFile() check already excludes directories. Archive filter was only
needed when sessions/archive/ was actively used.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(core): update tests to match archive-removal behavior

cleanupSession in recovery/actions.ts now marks sessions as terminated
instead of deleting metadata. Updated two recovery-actions tests to
assert on terminated status instead of file deletion. Also fixed
metadata and integration tests for the new deleteMetadata signature
(no boolean archive arg).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(core): address build/test issues from archive removal

- Fix writeMetadata calls missing required fields in test files
- Remove boolean archive arg from deleteMetadata calls in integration tests
- Update recovery-actions tests to expect terminated state instead of deletion
- Remove unused readdirSync import from migration test

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: update handoff doc — archiving removed

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs: remove handoff document

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(cli): add last-stop state persistence for ao stop/start restore

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(cli): ao stop kills all active sessions and records them

ao stop now kills all active sessions (orchestrator + workers), not just
the orchestrator. Killed session IDs are saved to last-stop.json for
restore on next ao start.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(cli): ao start offers to restore sessions from last ao stop

On interactive startup, if last-stop.json exists with sessions for the
current project, the user is prompted to restore them. The orchestrator
is skipped (already restored by ensureOrchestrator). The file is cleared
after the prompt regardless of choice.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(cli): update stop tests for all-sessions kill behavior

Update test mocks to return proper KillResult shape and adjust test
assertions for the new all-sessions stop behavior. Add console.log
fallback for killed session IDs (non-TTY/test capture).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(core): address review — sed JSON corruption, sanitizeBasename dot

- Replace sed-based JSON fallback with node -e in workspace hooks and
  claude-code plugin. sed "s|}|...|" replaces the first } per line,
  corrupting nested JSON (lifecycle, runtimeHandle). node is a hard dep
  and handles nested objects correctly via JSON.parse/stringify.
- Drop . from sanitizeBasename allowed chars — config.ts Zod schema
  rejects dots in project keys, so my.app_hash would fail loadConfig.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: address storage redesign review issues

* fix(core): persist stale runtime state + show cross-project sessions in ao stop/start

- session-manager: persist lifecycle to disk when enrichment detects dead
  runtime (missing/exited) — prevents stale "alive" metadata from keeping
  terminated sessions on the active sidebar (ao-100 bug)
- lifecycle-state: map runtime_lost reason to "killed" legacy status
- ao stop: list ALL sessions across projects, not just targeted project;
  display and record cross-project sessions in last-stop.json
- ao start: show sessions from other projects that were stopped, so user
  knows they need separate ao start for those projects

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(cli): scope ao stop to target project when explicit arg is given

ao stop (no arg) kills all sessions across all projects since it also
kills the parent ao start process. ao stop <project> now correctly
scopes to just that project's sessions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(cli): show all projects in tab completions by merging global config

listProjects() only read the local config (found via cwd search), which
may contain just one project. Now also reads the global config at
~/.agent-orchestrator/config.yaml to include all registered projects
in shell completions for ao stop, ao start, etc.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(cli): fall back to global config when project arg not in local config

ao stop <project> and ao start <project> failed when cwd has a local
agent-orchestrator.yaml that doesn't contain the targeted project.
Now falls back to ~/.agent-orchestrator/config.yaml which has all
registered projects, matching what tab completions already show.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(cli): ao stop <project> must not kill parent process or dashboard

ao stop donna was killing the parent ao start process and dashboard,
which serve ALL projects. Now only kills the parent process and
dashboard when no project arg is given (full shutdown). When targeting
a specific project, only that project's sessions are killed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(cli): always load global config for ao stop to see all projects

sm.list() iterates config.projects to find sessions. When loadConfig()
finds the local agent-orchestrator.yaml (1 project), ao stop only sees
that project's sessions — other projects' tmux sessions survive. Now
ao stop always loads the global config which has all registered projects.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(cli): ao start restores all sessions including cross-project ones

ao start showed sessions from all projects but only restored the
current project's sessions. Now restores all sessions listed, using
the global config so the session manager can see all projects.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(web): sidebar shows all sessions regardless of active project

Remove project scoping from useSessionEvents so the sidebar always
receives sessions from every project. Kanban filtering is applied
client-side via a projectSessions memo.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(cli): Ctrl+C on ao start performs full graceful shutdown

Previously Ctrl+C only stopped lifecycle workers and exited, leaving
sessions alive in tmux and not recording last-stop state for restore.
Now the SIGINT/SIGTERM handler mirrors ao stop: kills all sessions,
records last-stop state, and unregisters from running.json. A 10s
timeout ensures the process always exits even if cleanup hangs.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* docs: update architecture docs for CLI, lifecycle, and dashboard changes

- CLAUDE.md: add canonical lifecycle states/reasons, stale runtime
  reconciliation, LastStopState + running.json to storage section,
  config resolution note, CLI behavior section (ao start/stop/Ctrl+C),
  key files (lifecycle-state.ts, running-state.ts, start.ts, sidebar)
- AGENTS.md: add lifecycle-state.ts, start.ts, running-state.ts to key
  files, add CLI behavior notes section
- copilot-instructions.md: add lifecycle-state.ts + start.ts to
  high-risk files, add common mistakes for runtime_lost, sidebar
  scoping, and ao stop project scoping
- DESIGN.md: add decision log entry for sidebar cross-project sessions

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* docs: update PR behavior dashboard with behavioral fixes and cross-project CLI

Add sections for stale runtime reconciliation, dashboard sidebar
scoping, tab completions, config resolution, Ctrl+C graceful shutdown,
and documentation updates. Update stats to 90 files, +6481/-2421.
Update ao stop/start panels with cross-project behavior. Update
summary with runtime reconciliation and cross-project CLI verdicts.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* Revert "docs: update PR behavior dashboard with behavioral fixes and cross-project CLI"

This reverts commit 6d968b9ff5.

* fix(cli): add removeProjectFromRunning and targeted stop tests

- Add removeProjectFromRunning() to running-state.ts — removes a
  project from running.json so ao start <project> can restart without
  hitting the "already running" gate after ao stop <project>
- Add projectNeedsRestart check in ao start — skips "already running"
  menu when the project was removed from running.json by a targeted stop
- Add 6 tests for targeted stop behavior: no parent kill, no unregister,
  removes project from running.json, kills correct sessions, full stop
  still tears down parent+dashboard, last-stop records correct scope

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* docs: update handoff docs with accurate checkout recipes and CLI details

Fix checkout instructions to not assume everyone has the same fork as
origin — add separate sections for the PR author vs new contributors.
Correct stop.ts references (doesn't exist — stop logic is in start.ts).
Document removeProjectFromRunning, projectNeedsRestart gate, isProjectId
guard, and Ctrl+C signal handler details.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(cli): handle URL/path args when AO is already running

Previously `ao start <URL>` or `ao start <path>` while the daemon was
already running silently ignored the arg and showed a menu about cwd.
The user's URL was dropped.

Now, for TTY callers, when AO is already running and a URL/path arg is
provided:

- If the project is already registered AND in running.projects, just
  open the dashboard. No menu, no re-clone.
- Otherwise, register the project against the active config (clone for
  URLs via handleUrlStart, or addProjectToConfig for paths) and open
  the existing dashboard. Don't fall through to runStartup — that would
  spawn a duplicate dashboard on a different port.

Non-TTY callers (scripts/agents) keep the old "AO is already running"
message and do NOT mutate config behind the user's back.

Adds two tests:
- Path arg already registered + running → opens dashboard, no menu, no
  YAML mutation.
- Path arg unregistered + AO running → registers without prompting, no
  menu, prints "Opening the dashboard".

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(cli): register URL/path args in global config and spawn orchestrator

Previously `ao start <URL>` while AO was already running would register
the new project in the cwd's local config (polluting an unrelated
project's YAML) and tell the user to `ao stop && ao start <id>` to
actually spawn the orchestrator — clunky and surprising.

Now the flow:

- Always register against ~/.agent-orchestrator/config.yaml (global),
  never the cwd's local config. URLs go through handleUrlStart to
  clone, then are re-registered globally; paths go through
  addProjectToConfig with a global-config arg so it routes to
  registerProjectInGlobalConfig.
- Spawn the orchestrator session via sm.ensureOrchestrator so the
  dashboard immediately shows it.
- Warn that lifecycle polling for the new project requires
  `ao stop && ao start <id>` (the running daemon's worker can only
  poll projects it knew about at startup).
- Open the existing dashboard. No duplicate dashboard, no menu.

Already-registered + running case unchanged: just open the dashboard.

Tests updated to set AO_GLOBAL_CONFIG so the global lookup is isolated
from the test machine's real config, and to assert ensureOrchestrator
is called with the new project ID.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(cli): reload dashboard config after registering new project

After `ao start <URL/path>` registers a new project in the global
config, the running dashboard's services cache still holds the stale
config — so the project page 404s until the daemon is restarted.

Hit POST /api/projects/reload (which invalidates the services cache)
right after registering. Failure to reach the dashboard is non-fatal:
print a hint to refresh the page.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(cli): repair wrapped local config after URL clone

handleUrlStart writes a legacy wrapped (`projects:`) agent-orchestrator.yaml
inside the cloned repo. After registering the project against the global
config, the project resolver hits the wrapped local config and routes the
project into degradedProjects (with a resolveError) — so loadConfig drops
it from config.projects and ao start would throw "Failed to register".

Call repairWrappedLocalProjectConfig() right after the global registration
to convert the wrapped config to the flat format the new resolver expects.
Best-effort: if repair fails, defaults fill in behavior.

Cleanup note: any wrapped local configs from earlier runs (and stale
~/.agent-orchestrator/config.yaml entries from earlier test runs that
pre-dated AO_GLOBAL_CONFIG isolation) need manual cleanup.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(cli): clone+register flat config directly, surface empty-repo errors

Replaces the previous "register, then repair the wrapped config" hack
with a single-shot clone-and-register flow that produces a valid flat
local config from the start.

Why the previous flow was wrong:
- handleUrlStart writes a legacy wrapped (`projects:`) agent-orchestrator.yaml
  inside the cloned repo. The new global-config resolver rejects that
  shape and routes the project into `degradedProjects`, which breaks
  `loadConfig().projects[id]` lookups and 404s the dashboard route.
- repairWrappedLocalProjectConfig() papered over that — but the right
  fix is to never write a wrapped config in the first place.

What this does instead, when `ao start <URL>` runs while the daemon
is alive:

1. Parse the URL, resolve the clone target, clone (or reuse).
2. Detect the actual default branch via `git symbolic-ref refs/remotes/origin/HEAD`,
   falling back to local HEAD. Returns null for empty repos.
3. If the repo is empty (no commits / no refs), fail early with a
   clear actionable message — otherwise ensureOrchestrator throws a
   confusing "Unable to resolve base ref" deep inside the worktree
   plugin.
4. registerProjectInGlobalConfig with identity only (path, repo,
   defaultBranch, sessionPrefix derived from project ID).
5. writeLocalProjectConfig with behavior only (scm + tracker plugin
   choices, derived from the host platform). Skip the write if the
   repo already commits its own agent-orchestrator.yaml.
6. Refresh the global config and spawn the orchestrator session.

Drops `repairWrappedLocalProjectConfig` import — no longer needed.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(migration): keep agent-report and report-watcher metadata flat

Migration was nesting six agent-report keys (agentReportedState, At,
Note, PrUrl, PrNumber, PrIsDraft) and four report-watcher keys
(reportWatcherLastAuditedAt, ActiveTrigger, TriggerActivatedAt,
TriggerCount) into `agentReport` / `reportWatcher` wrapper objects.

The live runtime readers — parseExistingAgentReport in agent-report.ts
and the report-watcher writes in lifecycle-manager.ts — read these
keys flat off `session.metadata`. readMetadataRaw() then runs the
result through flattenToStringRecord(), which JSON.stringify()s any
object value into a single string under the wrapper key. It does NOT
unfold the nested object back into the flat keys the readers expect.

Net effect: any V1 session that had a non-empty agent report or a
non-zero report-watcher trigger count silently lost that state after
migration. The active-tmux gate in `ao migrate-storage` blunts the
worst case (sessions are terminated by the time migration runs, so
the freshness window often expires the data anyway), but reports
within the 5-minute freshness window and dashboard "last reported"
fidelity are still affected.

Fix: keep these ten keys flat in the V2 JSON, identical to the
existing handling for the `detecting*` fields. Same rationale, same
shape. Adds a regression test that asserts the flat keys round-trip
through migration and rewrites the two grouping tests to assert the
new flat shape.

Reported on PR #1466 by @ashish921998.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* docs(prompt): teach orchestrator to read agent reports via ao status --reports

The orchestrator system prompt explained the worker-only `ao report`
command and the freshness/precedence rules around agent reports, but
never told the orchestrator how to inspect them. The CLI flag
`ao status --reports <full | N>` already exists for exactly this
purpose — surface it in Monitoring Progress and cross-reference it
from the Explicit Agent Reports section so the orchestrator has an
obvious read path when an inferred status disagrees with what the
worker self-reported.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(cli): attach to existing daemon for ao start <project> after targeted stop

Reported on PR #1466 as P1: after `ao stop <project>` removed the project
from running.json (via removeProjectFromRunning) but left the parent
ao start process alive, `ao start <project>` took the projectNeedsRestart
path and fell through to runStartup(). runStartup() then started a SECOND
dashboard on a new port and overwrote running.json — leaving two AO
processes running, with running.json pointing at only the new one and the
original parent's lifecycle worker still polling.

Fix: when running && projectArg is a project ID && project not in
running.projects, attach to the existing daemon instead of falling through
to runStartup. The new branch:

- Loads the project from the global config and refuses with a clear error
  if it isn't registered there.
- Spawns the orchestrator session via the live session manager
  (sm.ensureOrchestrator).
- Calls the new addProjectToRunning() helper to put the project back into
  running.json so subsequent `ao stop` (no args) sees it and `ao spawn`
  doesn't print the "running instance is not polling project X" warning.
- Reloads the dashboard's services cache via POST /api/projects/reload so
  the project page works on the existing dashboard.
- Surfaces a yellow warning that lifecycle polling for the new project
  isn't attached without a full daemon restart — same architectural caveat
  documented in the URL/path attach branch and tracked separately as the
  dynamic project supervisor follow-up issue (#1522).
- Works for both TTY and non-TTY callers; non-TTY just skips the
  openUrl + dashboard popup.

Adds addProjectToRunning() in running-state.ts symmetric to the existing
removeProjectFromRunning(): file-locked, idempotent, no-op when state is
missing or already lists the project.

Adds a regression test that asserts:
- mockRegister is NOT called (no second daemon registration)
- ensureOrchestrator is called with the requested projectId
- addProjectToRunning is called with the projectId
- The interactive menu is NOT shown
- Output contains the expected "Attaching to running AO instance" /
  "reattached to running daemon" lines

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* docs: add scripts/demo-pr-1466.sh — end-to-end reviewer demo

Self-contained, sandboxed walkthrough of every PR #1466 behavior change.
Designed to be recorded as a screencast — section banners replace
narration, no live typing, deterministic output.

Six acts:
  1. Migration V1 → V2 (live: seed hash dirs + key=value, dry-run, execute,
     show V2 layout, verify @ashish921998 fix that agent-report keys stay
     flat after migration, prove rollback safety on rerun)
  2. Cross-project CLI P1 fix (filter the regression test by name and run
     it live — asserts no second daemon is spawned by ao start <project>
     after ao stop <project>)
  3. Dashboard sidebar shows all projects (display the Dashboard.tsx fix)
  4. Restore from ao stop / Ctrl+C (last-stop.json round-trip)
  5. Ctrl+C graceful shutdown handler with 10s hard timeout
  6. Empty-repo guard for ao start <URL> (the detectClonedRepoDefaultBranch
     null path that surfaces a useful error before ensureOrchestrator)

Then prints the final 560 / 981 test summary so the recording ends on
a green CI signal.

Sandbox notes:
  • $HOME is overridden to /tmp/ao-demo-1466 for the duration of the
    script so getAoBaseDir() resolves there. The operator's real
    ~/.agent-orchestrator is never touched.
  • A REAL_HOME is captured before the override and restored when
    running the full test suites, since vitest needs the operator's
    real config path to avoid cross-test contamination.
  • Re-run is idempotent — rm -rf $DEMO_HOME at the top recreates
    the sandbox from scratch.

Verified runs end-to-end on storage-redesign with exit code 0.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* demo: richer fixture — 2 projects, 6 sessions, real source trees

Earlier seed was a single empty repo with one session and a 2-line README.
Reviewers would dismiss it as not credible migration evidence.

New seed:
  • 2 source projects (myproject, frontend), each a real TS package layout
    with package.json, tsconfig.json, src/lib/, tests/, .gitignore, README,
    and 6 commits of history. 8 files per repo.
  • 6 sessions across the 2 hash dirs, in varied states:
      - ao-1 (working, agent-report state + report-watcher counters + PR
        fields — headline @ashish921998 flat-key fix in one record)
      - ao-2 (V1-archived, terminated, manually_killed)
      - ao-3 (stuck, with report-watcher trigger active)
      - my-orchestrator-1 (kind=orchestrator)
      - fe-1 (working, PR open with PR fields)
      - fe-2 (V1-archived, terminated, runtime_lost)
  • Real git worktree for ao-1 with an actual diff file —
    proves worktree migration moves files and rewrites git refs.
  • Pre-seeded global config.yaml lists both projects so the migrator
    has identity to project against.

Migration handles all 6 sessions (4 active + 2 archived → flattened) and
the 1 worktree. The verification step inspects ao-1.json post-migration
and asserts every flat agent-report / report-watcher key from the
@ashish921998 fix is present, with no nested wrapper objects.

Also fixes:
  • MIGRATED_PROJECT used to grab alphabetically-first directory which
    made the JSON read crash when frontend won — hardcoded to myproject.
  • Before-display referenced $HASH_DIR/archive but the actual archive
    location is $HASH_DIR/sessions/archive — corrected.

End-to-end verified: exit 0, "PASS — agent-report flat-key contract
preserved", 560/560 CLI + 981/981 core tests in the final summary.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(migration): relink Claude Code session storage when worktrees move

Reported in PR #1466 QA: after `ao migrate-storage`, restoring a session
launches a fresh `claude` instance — chat history is gone.

Root cause: Claude Code keys session JSONLs by the encoded form of the
workspace cwd (~/.claude/projects/<encoded>/<session-uuid>.jsonl, where
encoded = cwd with `/` and `.` replaced by `-`, see toClaudeProjectPath
in agent-claude-code/src/index.ts). The migrator moves worktrees from
~/.agent-orchestrator/{hash}-{project}/worktrees/{sid} to
~/.agent-orchestrator/projects/{projectId}/worktrees/{sid}, which
produces a different encoded path. The agent's session JSONLs are still
at the old encoded path and stay orphaned. getRestoreCommand looks under
the new encoded path, finds nothing, returns null — and the caller
falls back to a fresh launch.

Fix: track every (oldWorkspacePath, newWorkspacePath) pair across both
migration phases (per-project migrateProject and the cross-project
moveStrayWorktrees), then call relinkClaudeSessionStorage after all
worktree moves complete. The relink renames each
~/.claude/projects/<old-encoded>/ → <new-encoded>/. Skip when source
doesn't exist (no Claude history) or target already exists (manual
reconciliation needed). Same step is invoked in reverse from
rollbackStorage so `--rollback` undoes the relink.

The encoding helper is duplicated locally in migration/storage-v2.ts to
avoid pulling the agent plugin into core/migration just for one string
transformation. Kept in sync by hand; if the plugin's encoding ever
changes, both copies need to update together.

Codex stores sessions date-sharded with the cwd embedded inside each
JSONL's session_meta line, so the same physical-rename trick doesn't
apply. Codex relinking is left as a follow-up — the comment in
relinkClaudeSessionStorage points at it.

Two regression tests added:
  • Happy path: V1 worktree at OLD encoded path with a JSONL inside
    Claude's projects dir; after migrateStorage the JSONL is at the
    NEW encoded path and the OLD dir is gone. claudeSessionsRelinked === 1.
  • Safety: target dir already exists at the new encoded path; migration
    skips the relink, neither dir is touched, claudeSessionsRelinked === 0.

Tests use HOME override to sandbox ~/.claude/ so the runner's real
agent-storage is never touched.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(boundary): four cross-module seams flagged in PR #1466 review

- Migration: rewrite Codex rollout session_meta.cwd for moved
  worktrees so getRestoreCommand keeps finding the old thread.
  Mirrors the Claude relink with a single-line in-place rewrite.
- CLI start: stop adding the project to running.projects in the
  attach-to-existing-daemon branch. Lifecycle polling cannot be
  attached mid-flight, so claiming coverage made `ao spawn`
  silently suppress its "instance is not polling X" warning.
- CLI stop: defensively drop foreign sessions before the kill
  loop when a project arg is given. `sm.list(projectId)` already
  scopes, but the kill loop is destructive enough to deserve a
  consumer-side guard.
- Web DELETE /api/projects/[id]: validate the id through
  getProjectDir BEFORE calling cleanupManagedWorkspaces so a
  malformed key never reaches a workspace plugin.

Adds regression tests for each.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(boundary): four more cross-module seams flagged in PR #1466 review

- Recovery actions (cleanup/escalate/recoverSession-on-max-attempts)
  now mutate the canonical lifecycle alongside the flat status. For
  V2 sessions readMetadataRaw derives status from lifecycle, so the
  prior flat-only writes were silently overridden on the next read.
- Targeted `ao stop <project>` no longer calls
  removeProjectFromRunning. The parent process's in-memory lifecycle
  worker keeps polling that project (a child CLI cannot reach into
  parent memory), so running.projects must keep listing it to remain
  truthful. The attach branch in `ao start <project>` now triggers
  on any project-id arg with a live daemon, regardless of
  running.projects content; the polling-not-attached warning fires
  only when the project is genuinely not in running.projects.
- `ao start` restore loop preserves last-stop.json for sessions that
  fail to restore (transient workspace/runtime errors) instead of
  clearing the only persisted record. Successful or fully-failed
  flows still clear it.
- New integration round-trip: migrate a Codex JSONL with the old
  worktree cwd, then call the real agent-codex.getRestoreCommand
  with the migrated workspacePath and assert it returns
  `codex resume <threadId>`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(review): four illegalcall PR review findings on PR #1466

- writeMetadata sites in session-manager (spawn + ensureOrchestrator)
  spread `buildLifecycleMetadataPatch` (string-typed patch) into a
  typed SessionMetadata literal, which silently wrote `lifecycle` as
  a JSON string and made freshly-spawned sessions read with
  `lifecycle: undefined` until the first poll round-trip.
  Override the spread with the canonical object form and drop the
  metadata.ts safety net that compensated for the bug.
- Migration archive-flatten regex `/^([a-zA-Z0-9_-]+?)_\d/` was lazy
  and captured `team` for `team_1-7_<ts>.json`. Replace with an
  anchor on the timestamp suffix in both call sites so any sessionId
  containing `_<digit>` is parsed correctly.
- Migration duplicate-sessionId resolution renamed-the-loser to
  `${sessionId}__from-${hash}` rather than silently dropping it.
  Both records survive in V2; the rename is logged.
- `running-state.ts` writes `running.json` and `last-stop.json` via
  `atomicWriteFileSync` (temp+rename) so a crash mid-write cannot
  leave torn JSON that orphans an alive AO process or erases the
  next-start restore prompt.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(metadata): preserve corrupt session JSON before overwriting

mutateMetadata used to merge against an empty record and atomically
rewrite when parseMetadataContent returned null on corrupt JSON. The
original bytes were lost — the user had no signal anything was wrong,
the file just became "not corrupt anymore — and missing fields".

Side-rename the file to `<path>.corrupt-<ts>` and warn before the
rewrite so forensics survive. Adds two regression tests and drops the
stale STORAGE_REDESIGN.md reference comment.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* chore(lint): fix 4 errors introduced by recent boundary fixes

- storage-v2.ts:656,1453 — drop unnecessary `\-` escape inside `[…]`
  character class (no-useless-escape).
- storage-v2.ts:979 — replace inline `import("node:fs").Dirent` type
  annotation with a top-level `Dirent` named import (consistent-type-imports).
- recovery/actions.ts:11 — merge the second `../types.js` `import type`
  into the existing line (no-duplicate-imports).

Tests + typecheck unchanged (991 passing).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(cli): propagate reloaded config out of resolveProject after add

The interactive "Add <cwd>" menu path in `resolveProject` registers
the project in the global config (with a hashed id like
`mail-automate_3e4d45c2ba`) and reloads the config internally to
fetch the new project entry. It returned only `{projectId, project}`,
so the outer caller kept the pre-add `config` reference — which has
no key for the just-added project.

Downstream that surfaced as:

    Failed to start lifecycle worker:
    Unknown project: mail-automate_3e4d45c2ba

because `ensureLifecycleWorker(config, projectId)` checks
`config.projects[projectId]` against the stale config.

`resolveProject` and `resolveProjectByRepo` now also return the
(possibly reloaded) config; the three call sites pick it up via
`({ projectId, project, config } = await resolveProject(...))`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-28 17:55:53 +05:30
Priyanshu Choudhary a31210205d fix(test): update notifier-desktop integration test for Windows toast support
I missed this duplicate test in the integration-tests package when I
added the Windows branch to notifier-desktop. The mock callback used the
3-arg execFile signature (cmd, args, cb) but the new win32 path calls
execFile with 4 args (cmd, args, opts, cb), so the callback landed in
the opts slot and "cb is not a function" broke CI on Linux.

Make the mock signature-agnostic and replace the win32 "no execFile,
warns" assertion with one that verifies the EncodedCommand toast script.
Add a separate freebsd case for the actual unsupported-platform path.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 21:39:00 +05:30
Priyanshu Choudhary 7d40f19abd fix(windows): resolve gh.exe via PATHEXT and fix path-shape test regexes
resolveGhBinary() searched PATH for a literal "gh" file and threw on
Windows where the binary is gh.exe (or gh.cmd for npm shims). All gh
calls in tracker-github and scm-github failed before reaching execFile,
which made spawn() fall back from tracker-derived branch names and made
cleanup() skip the gh-driven kill paths entirely.

Honor PATHEXT on win32 so the resolver matches gh.exe/.cmd/.bat. Update
the four affected integration assertions to accept Windows path shapes.
Also bump the runtime-process sendMessage sleep on Windows — ConPTY
pipe round-trip needs more headroom than the Unix direct-stdin path.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-25 21:08:21 +05:30
Priyanshu Choudhary b31efacf6b Merge origin/main into feat/windows-platform-adapter
Pulls in 2 main commits (#1487 orchestrator identity fix, #1238 gh CLI
tracer + scm/tracker migration Phase A1a) plus the auto-merged
follow-on changes from upstream.

Conflict resolutions:
- packages/core/src/session-manager.ts:
  Took main's reorganized opencode-agents-md import + new
  getOrchestratorSessionId import. Kept main's reformatted sessionCache
  type and added the new ensureOrchestratorPromises Map.

- packages/core/src/agent-workspace-hooks.ts:
  Took main's WRAPPER_VERSION = "0.6.0" (and matching test fixture).

- packages/core/src/__tests__/agent-workspace-hooks.test.ts:
  Merged both sides' imports — kept branch's buildNodeWrapper +
  node:path/join (still used in test body) and added main's
  AO_METADATA_HELPER + GH_WRAPPER constant exports.

- packages/plugins/agent-codex/src/index.ts:
  Dropped now-unused PREFERRED_GH_PATH import (env injection moved to
  session-manager). Kept isWindows — needed by branch's new
  formatLaunchCommand, resolveCodexBinaryWindows, and pre-existing
  isProcessRunning code.

- packages/plugins/agent-codex/src/index.test.ts:
  Took main's collapsed PATH/GH_PATH undefined assertions and no-op
  setupWorkspaceHooks test — the agent no longer constructs PATH or
  installs wrappers (session-manager owns those paths now). Removed
  the orphaned wrapper-write tests. Kept branch's
  describe.skipIf(win32) on the shell-wrapper-content block (those
  tests verify Unix shell-script content that genuinely can't run on
  PowerShell).

- packages/plugins/agent-aider/src/index.test.ts +
  packages/plugins/agent-cursor/src/index.test.ts:
  Took main's `expect(env["PATH"]).toBeUndefined()` — agents no
  longer set PATH directly.

- packages/web/server/mux-websocket.ts:
  Kept branch's Windows pipe handler branch and updated the inner
  Unix `terminalManager.open(id, tmuxName)` call to main's new
  signature with the optional tmuxName argument.

Verified: typecheck clean, lint 0 errors, full build (28 pkgs), tests
942/946 passing — the 4 failing tests in plugin-integration.test.ts
also fail on main itself (verified by running main's untouched test
file), so they're pre-existing flakes unrelated to this merge.
2026-04-25 20:42:17 +05:30
Dhruv Sharma a8bc746947
feat(core): opt-in gh CLI tracer + scm/tracker migration (Phase A1a) (#1238)
* feat(core): add opt-in gh CLI tracer and migrate scm/tracker plugins

Introduces execGhObserved() in @aoagents/ao-core: a thin wrapper around
execFile("gh", ...) that writes a JSONL trace row to $AO_GH_TRACE_FILE
on both success and failure. Captures status line, HTTP status, ETag,
rate-limit headers, duration, stdout/stderr byte counts, exit code, and
signal. No-op when the env var is unset, so default behavior is
unchanged.

Migrates three call sites to the observer:
- scm-github/graphql-batch.ts — PR-list guard, commit-status guard,
  GraphQL batch query
- scm-github/index.ts — gh() and ghInDir() helpers
- tracker-github/index.ts — internal gh() helper

This is Phase A1a of experiments/PLAN.md: tracer infrastructure +
migration. The full GhRunner contract (Promise<GhResult>,
GhRunnerError.ghResult on reject, body capture, redaction, 64 KB cap)
lands in A1b along with the scorecard baseline.

Also adds experiments/ reference docs: the v2.3 plan, the gh-CLI call
catalog, two ETag verification writeups, and a trace harness + summary
script.

* docs(experiments): add A1a validation status and A1b blockers

Record the five A1b pre-freeze blockers surfaced by Adil's 1,487-row
baseline and an independent drill run: graphql-batch missing -i,
extractOperation flag mis-bucketing, analyzer not segmenting burn by
reset window, CLI-subcommand opacity (GH_DEBUG=api stderr vs coarse
/rate_limit bracket — not equivalent), and sessionId/projectId not
threaded through plugin callsites. Note bare gh() helper cleanup as
known-open follow-up.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(tracer): close A1b blockers 1-4 — graphql-batch visibility, operation naming, analyzer segmentation

- Add -i flag to executeBatchQuery in graphql-batch.ts and split HTTP
  headers from JSON body before parsing, making all gh.api.graphql-batch
  rows visible to status and rate-limit analysis (was 186 invisible rows)
- Fix extractOperation() in gh-trace.ts to walk past -* flags before
  picking the operation segment, eliminating the gh.api.--method bucket
- Add per-reset-window burn segmentation to both analyzers so runs
  straddling a reset boundary produce per-window deltas instead of a
  single invalid cross-reset delta
- Add experiment scripts: analyze-trace.mjs (deep trace analysis) and
  drill-tracer.mjs (standalone tracer exerciser)
- Document Gap 1 decision in PLAN.md: accept CLI subcommands as opaque
  for A1, bracket A2 runs with /rate_limit snapshots for coarse burn
- Add progress timeline to PLAN.md showing A→B→C track dependencies

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(experiments): add A2 baseline matrix runbook

Practical execution plan for the Phase A2 scenario x scale x topology
matrix: 7 priority cells, per-cell procedure, /rate_limit bracketing
for Gap 1 subcommand burn, output format for baseline.md, and the
scorecard that gates Track B.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(tracer): guard stderr/stdout against undefined, bound operation cardinality

Addresses code review findings:
1. Guard Buffer.byteLength and parseIncludedHttpResponse against
   undefined stderr/stdout — fixes 48 SCM test regressions where
   mocked execFile paths don't populate stderr
2. extractOperation() now takes only the first path segment of REST
   URLs (e.g. "repos" from "repos/acme/repo/pulls/123/...") to keep
   operation bucket cardinality bounded and stable across runs
3. Fix A2 runbook /rate_limit snapshots to produce valid JSON using
   jq's now|todate instead of appending raw timestamp
4. Add blocker 5 dependency to runbook prereqs and per-session cells

All 140 SCM tests pass (0 failures).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(experiments): add rate-limiting research artifacts

Baseline measurements, discussion notes, benchmark harness spec,
and updated master plan from two independent trace runs at 5-6 sessions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(experiments): add benchmark harness for GH rate-limit measurement

Three modes: setup (spawn sessions, wait for PRs), measure (trace API
calls over a fixed window, produce scorecard), report (recompute from
existing trace). Node.js stdlib only, shells out to ao CLI and gh CLI.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(scm-github): handle 304 Not Modified in ETag guard catch blocks (B1)

`gh api -i` exits code 1 on HTTP 304 responses, causing the catch blocks
in checkPRListETag and checkCommitStatusETag to assume the resource changed
and trigger unnecessary GraphQL batch queries every poll cycle.

Fix: inspect stdout/stderr in the catch block for the 304 status line before
falling back to "assume changed". Also unifies the 304 detection regex to
handle HTTP/1.1, HTTP/2, and HTTP/2.0 status lines, and adds rateLimit
introspection to the batch GraphQL query.

Benchmark result (quiet-steady, 5 sessions, 15 min):
- GraphQL points/hr: 260/5,000 (5%) — down from 820–1,416 pre-fix
- ETag guard 304 rate: 100%
- GraphQL batch calls during measurement: 0

Also fixes the benchmark harness to create placeholder tmux sessions with a
claude symlink so the lifecycle actually polls sessions instead of
short-circuiting to "killed".

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(experiments): update plan and notes with B1 benchmark results

B1 fix validated at 5, 10, and 20 sessions in quiet-steady state:
- 5 sessions: 260 GraphQL pts/hr (5% budget)
- 10 sessions: 640 pts/hr (13%)
- 20 sessions: 680 pts/hr (14%) — sub-linear scaling confirmed
- 50-session projection: ~800-1000 pts/hr (16-20%)
- ETag guard 304 rate: 100% at all scale points
- graphql-batch calls: 0 during measurement at all scale points

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(core): log gh wrapper invocations for D1

* fix(core): preserve wrapper logging for dash-prefixed gh args

* feat(core): add gh wrapper cache for PR discovery and issue context (D4)

Add read-through caching to the ~/.ao/bin/gh wrapper, targeting the two
largest agent-side waste buckets identified in D4 analysis:

1. PR discovery (gh pr list --head): infinite TTL for positive results.
   598 calls → ~10 per 10-session run (98% reduction).
2. Issue context (gh issue view): 300s TTL.
   75 calls → ~20 per 10-session run (73% reduction).

The wrapper now caches successful read-only responses in
$AO_DATA_DIR/.ghcache/$AO_SESSION/ and serves them on subsequent
identical calls. Negative results (empty []) are never cached.
gh pr create populates the PR discovery cache immediately.

Also lifts PATH wrapper installation from individual agent plugins into
session-manager, making it universal for all agents including Claude Code:

- session-manager injects PATH + GH_PATH into every runtime.create()
- session-manager calls setupPathWrapperWorkspace() for all agents
- Removes duplicate buildAgentPath/setupPathWrapperWorkspace boilerplate
  from codex, aider, opencode, and cursor plugins

Includes D4 implementation plans in experiments/.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs(experiments): add full capacity discovery (5→50 sessions) and CI churn results

Complete scaling curve measured: 50 sessions uses only ~28% of GraphQL
budget with 100% ETag guard hit rate at every scale. Poll cycle lag
identified as first bottleneck (66s at 50 sessions vs 30s target).
CI churn benchmark shows ETag invalidation is a latency problem, not
a rate-limit problem (+9% GraphQL, +4.4x p50 latency).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* docs(experiments): record real-agent catastrophe and Track D handoff

5-real-agent run on todo-app exhausted GraphQL bucket in 31 min (~9572 pts/hr,
~37x quiet-steady at the same session count). AO polling consumed ~10 calls;
the rest came from agents themselves via the metadata-only ~/.ao/bin/gh
wrapper, which has no tracing. Captures findings, adds Track D (agent-side
gh consumption) plus B5 (migrate remaining bare gh callsites to
execGhObserved), and includes the runbook + benchmark scripts Adil will
build on for the cross-machine reproduction.

* feat(core): add cache-hit/miss tracing to gh wrapper (D4)

The wrapper trace now logs a cacheResult entry for every cacheable
command: hit, miss-stored, miss-negative, or miss-error. This makes
benchmark runs conclusive — you can count cache hits vs real gh calls
directly from the JSONL trace instead of inferring from rate-limit
deltas.

Bump wrapper version to 0.4.1.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(scm-github): replace repo-scoped Guard 1 with PR-scoped ETag checks (D4)

Guard 1 now checks GET /repos/{owner}/{repo}/pulls/{number} per PR
instead of GET /repos/{owner}/{repo}/pulls?... per repo. This means:

- Only changed PRs flow into the GraphQL batch
- Unchanged PRs are served directly from the enrichment cache
- shouldRefreshPREnrichment returns a refresh plan (prsToRefresh +
  cachedResults) instead of a boolean

When 1 of 10 PRs changes, the old guard refreshed all 10 via GraphQL.
Now only the 1 changed PR is fetched; the other 9 are served from cache
at zero GraphQL cost.

Trade-off: more REST guard calls (1 per PR instead of 1 per repo), but
304 responses cost zero rate limit points.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Forward AO_AGENT_GH_TRACE to session runtimes

* revert: remove PR-scoped ETag guards (Change 3)

Reverts 25ae6013. The per-PR Guard 1 added more REST calls (1 per PR
instead of 1 per repo) without meaningful GraphQL savings at 10-session
scale. Core REST delta went from 16 to 142 while GraphQL rate stayed
flat. The repo-scoped guard is sufficient for current workloads.

Preserves the subsequent 6fc64f4f commit (AO_AGENT_GH_TRACE forwarding).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(core): use real gh binary in execGhObserved, bypass wrapper

execGhObserved() was calling bare "gh" which resolved to ~/.ao/bin/gh
(the wrapper) when that directory was in PATH. This caused:
- AO-side gh calls going through the agent wrapper
- All trace rows with aoSession=null polluting the agent trace
- Cache functions silently failing (no AO_SESSION in AO process)

Now strips ~/.ao/bin from PATH and resolves the real gh binary
(e.g. /opt/homebrew/bin/gh) at startup. Cached after first resolution.

AO process → execGhObserved → real gh → AO_GH_TRACE_FILE
Agent process → ~/.ao/bin/gh wrapper → AO_AGENT_GH_TRACE + cache

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(core): harden gh wrapper caching and agent-side tracing

Cache correctness:
- Include --json fields in cache key (prevents stale partial responses)
- Only cache stdout, not stderr (prevents warning contamination)
- Fix trailing newline inconsistency in PR discovery cache
- Support --key=value arg syntax for all cached flags
- Remove PR create cache pre-population (hardcoded fields, no JSON escaping)
- Log miss-write-failed when ao_cache_write fails (previously silent)

Agent trace improvements:
- Add operation field to invocation rows (gh.pr.list, gh.issue.view, etc.)
- Add durationMs, exitCode, ok to cache outcome rows
- Log passthrough for all non-cached code paths (pr/create, default case)
- Replace exec with child process in default case to enable post-call tracing

Bump wrapper version to 0.6.0.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(runtime-tmux): re-export PATH after shell init to survive macOS path_helper

macOS zsh runs path_helper during shell startup which resets PATH,
wiping entries set via tmux new-session -e. This caused ~/.ao/bin
to be lost, so the gh/git wrappers were never intercepting agent
calls — no caching, no tracing, no metadata auto-updates.

Fix: send `export PATH=...` via send-keys after the shell has
initialized but before the launch command, ensuring PATH sticks.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(runtime-tmux): use launch script for PATH re-export instead of send-keys

The previous send-keys approach sent 1000+ literal keystrokes for the
PATH value, which broke terminal input buffers and caused stuck quote
prompts. Instead, include the PATH export in the launch script file
which is executed directly — no terminal buffer issues.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs(experiments): add AO-side gh rate-limit trace report

5-session, 15-minute trace analysis with full call breakdown,
ETag guard effectiveness, anomaly investigation, and ranked
reduction opportunities.

Key findings:
- GraphQL at 41%/hr with 5 sessions (bottleneck at ~12 sessions)
- 47% of calls are individual REST fallbacks that batch should cover
- Review thread GraphQL calls (55/15min) can be folded into batch
- detectPR() and guard failures are working as designed

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs(experiments): add AO rate-limit reduction plan with Step 1

Step 1: Remove individual REST fallback from determineStatus().
110 calls (65 pr view + 45 pr checks) eliminated per 15-min window.
Batch enrichment covers all PRs every 30s — fallback is unnecessary
insurance for an event that never occurred in real traces.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* experiments(m2): drop agent-trace gate for claude-code

Claude Code uses native PostToolUse hooks (.claude/settings.json), bypassing
the ~/.ao/bin/gh PATH wrapper, so AO_AGENT_GH_TRACE stays empty even when
Claude makes gh calls. The previous smoke gate required AGENT_ROWS>0 and
aborted every claude-code M2 batch at smoke.

- limit-finder.sh: add REQUIRE_AGENT_TRACE env + --no-require-agent-trace flag,
  gate the AGENT_ROWS integrity check behind it.
- m2-ab-run.sh: bump SMOKE_DURATION to 420s; auto-pass --no-require-agent-trace
  when AGENT=claude-code; simplify smoke_check to gate only on AO_ROWS>0
  (B1 lives in AO-side scm-github, measured by AO trace).

Follow-up tracked as task #39: instrument Claude's hook/tool path or document
that AO_AGENT_GH_TRACE does not cover Claude Code.

* feat(tracker-github): cache issue reads in-process (5 min TTL)

The lifecycle worker polls getIssue/isCompleted repeatedly for the same
issue across a session. Trace data from a 5-session tier-5 bench run
showed the same (repo, issue) pair fetched 64+ times with >97% duplicate
rate — ~744 of 4,059 AO gh calls in 10 minutes were redundant issue views.

Adds an in-process Map<string, CachedIssue> per createGitHubTracker()
instance, keyed by `${repo}#${id}`, TTL 5 min, bounded to 500 entries
(LRU evict-oldest on overflow).

- getIssue: read-through cache, populate on miss
- isCompleted: routes through getIssue (was a separate narrow gh call)
- updateIssue: invalidate the entry before mutating
- createIssue: unchanged, naturally populates via the existing getIssue
- Failures are not cached

Cache lives inside createGitHubTracker so each create() returns an
isolated cache (test isolation comes for free).

Expected reduction: ~744 → ~15 gh issue view calls per tier-5 run.

Tests: 41 existing + 10 new cache tests, all passing.

* feat(scm-github): cache 5 gh pr view callsites with per-method TTLs

The lifecycle worker repeatedly polls each PR for state, summary, reviews,
and review decision. Trace data showed gh pr view was the single largest
AO-side endpoint at 1,280 calls per 5-session tier-5 run with >97% duplicate
rate (e.g. PR #184 polled 86× for --json state alone in 11.5 minutes).

Adds an in-process per-instance cache inside createGitHubSCM(), keyed by
${owner}/${repo}#${prKey}:${method} so different field-sets stay isolated.
Per-method TTLs balance reduction against staleness on decision-influencing
fields:

- resolvePR: 60s (identity metadata only)
- getPRState: 5s
- getPRSummary: 5s (includes state)
- getReviews: 5s
- getReviewDecision: 5s

assignPRToCurrentUser, mergePR, and closePR each invalidate the entire PR
cache for that PR after the mutation, so AO never sees stale state from its
own writes. Failures are not cached.

getCIChecksFromStatusRollup and getMergeability are intentionally NOT cached
here — those need ETag-based revalidation, not blind TTL, and will land
separately.

Expected reduction: ~1,165 of ~1,280 gh pr view calls per tier-5 run.

Tests: 73 existing + 12 new cache tests, all 153 passing.

* feat(scm-github): cache CI checks, mergeability, pending comments, detectPR

Completes the AO-side hot-read caching alongside the prior PR view cache.
All use 5s TTL per the approved policy for decision-influencing fields —
well under one lifecycle poll cycle so state transitions are still seen
next pass.

- getCIChecks (gh pr checks): 5s TTL
- getMergeability (composite pr view + CI + state): 5s TTL on the composite
- getPendingComments (gh api graphql review threads): 5s TTL —
  ETag doesn't help on GraphQL per Experiment 2
- detectPR (gh pr list --head BRANCH): 5s TTL, POSITIVE-ONLY.
  Empty results are never cached so a freshly created PR is discovered
  on the very next poll. The branch-keyed cache entry is invalidated
  by mergePR/closePR alongside the number-keyed entries.

Combined with the prior PR view cache, covers the top 6 AO-side gh
operation categories that accounted for ~85% of calls in tier-5 traces.

Tests: 85 existing + 9 new cache tests, all 162 passing.

* experiments(m2): parse REPO from yaml before using it in banner

m2-ab-run.sh referenced $REPO in the header banner before parsing it,
causing 'unbound variable' abort under 'set -u'. Parse it right after
CONFIG_FILE is set.

* test(core): mock full Issue shape in plugin-integration cleanup tests

After tracker-github routed isCompleted() through getIssue() to share
the issue cache, these mocks needed the full Issue shape (number, title,
body, url, state, stateReason, labels, assignees) instead of the narrow
{state} shape that worked when isCompleted made its own --json state call.

* perf(scm-github): tune cache TTLs based on trace replay

Replayed feat run1 + main run2 tier-5 traces (4059 + 1748 rows, 38 min, 5
sessions each) against the shipped cache logic. Three TTLs were materially
under-tuned for the actual lifecycle poll cadence:

- detectPR:           5s → 30s   (was 0.5% hit rate; per-branch poll cadence
                                  is ~90s, so 5s caught nothing. 30s catches
                                  intra-cycle bursts when multiple sessions
                                  share a branch. Positive-only stays.)
- getReviewDecision:  5s → 10s   (within "10-30s TTL or ETag" policy)
- getPendingComments: 5s → 10s   (same policy class)

All three are still well under one poll cycle; freshness contract unchanged
in practice. Other TTLs (5s on state/CI/mergeability, 60s on resolvePR,
5min on issue) hit the targets they were set for and stay as-is.

Replay results before/after:
- feat run1:  53.7% → 57.8% reduction (2179 → 2345 hits of 4059 calls)
- main run2:  47.4% → 52.6% reduction
- Net: ~55% AO-side gh calls eliminated across both traces

Adds experiments/cache-replay.mjs — a counterfactual replay tool that
walks an execGhObserved JSONL trace and simulates per-method cache hits
with the shipped TTLs. Useful as a regression check when tweaking cache
policy.

Tests: 162/162 passing.

* docs(experiments): add cache freshness check runbook

Seven-step manual runbook to validate the cache TTL contract doesn't
cause workflow lag. Covers each cached method with:

- exact gh CLI trigger command
- what to observe in the dashboard / lifecycle log
- pass/fail threshold (TTL + 30s poll cycle)

Companion to experiments/cache-replay.mjs — replay measures how much
we saved, runbook measures whether we lost anything in the process.

* docs(experiments): add Step 2 — consolidate review comment fetching

Single GraphQL call replaces GraphQL + REST for review comments.
Include comment data in agent reaction message to eliminate
agent-side gh read calls. Update future steps.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs(experiments): add duplicate API traffic analysis

Three independent sources hit GitHub API for the same PRs:
1. Dashboard serialize.ts — individual REST calls, no batch, no cache
2. CLI lifecycle manager — batch + guards
3. Web lifecycle manager — same batch + guards, 3s offset

~50% of all API traffic is pure duplication. Dashboard and dual
lifecycle managers are the root causes.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs(experiments): add full cache architecture to duplicate traffic analysis

Three independent cache layers across two processes with zero shared
state. Web process creates its own plugin registry, SCM plugin, lifecycle
manager, and dashboard cache — all hitting GitHub independently.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs(experiments): add shared PR enrichment plan

Persist batch enrichment + review comments to session metadata files.
Dashboard reads from disk instead of making its own GitHub API calls.
Remove web's duplicate lifecycle manager.

Eliminates ~268 calls / 15 min (58% of all traffic). Dashboard data
gets fresher (30s vs 5min). Single writer (CLI lifecycle), web only reads.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs(experiments): update Step 1 — remove all three fallback paths

Remove fallback in determineStatus(), maybeDispatchCIFailureDetails(),
and maybeDispatchMergeConflicts(). All three follow the same pattern:
batch cache hit → use it, cache miss → skip (wait 30s for next batch).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* docs(experiments): promote Step 3 (remove dead reviews field) + detail Step 5 (issue caching)

Step 3: Remove reviews(last: 5) from batch query — fetched but never
consumed, reduces GraphQL complexity on every batch call.

Step 5: Persist issue data to session metadata at spawn — eliminates
27 gh issue view calls per 15 min (both processes re-fetch independently).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat(core): remove individual REST fallback from lifecycle polling

Remove fallback paths in determineStatus(), maybeDispatchCIFailureDetails(),
and maybeDispatchMergeConflicts() that made individual REST calls when the
batch enrichment cache missed. The batch runs every 30s — a cache miss
means the data arrives on the next cycle, not that it's lost.

Also add populatePREnrichmentCache() call to check() so single-session
checks also use the batch path.

Eliminates ~110 individual pr view/pr checks calls per 15-min window
(24% of all AO-side traffic).

* feat(core): consolidate review comment fetching into single GraphQL call

Add getReviewThreads() to SCM interface — returns all review threads
(human + bot) with isBot flag from a single GraphQL query. Lifecycle
manager splits locally for separate reaction pipelines.

- Eliminates the REST getAutomatedComments() call (40 calls / 15 min)
- Reaction messages now include inline comment data (file, line, author,
  body, URL) so agents don't need to re-fetch via gh api
- Default config messages updated to not tell agents to call gh
- getAutomatedComments kept as optional for backward compatibility

* perf(scm-github): remove unused reviews(last: 5) from batch query

The batch query fetched reviews with author, state, submittedAt but
the data was never consumed — only used in a validation check.
The reviewDecision scalar field provides everything AO needs.

Reduces GraphQL complexity cost on every batch call.

* docs(experiments): add post-optimization trace report (Steps 1-3)

5-session, 17-minute trace after removing REST fallback, consolidating
review comments, and removing dead reviews field.

Results: GraphQL 35%/hr (was 41%), REST <1% (was 3%), automated
comment REST calls eliminated. 54% of remaining traffic is redundant
(duplicate lifecycle manager + dashboard individual calls).

* docs(experiments): add trace file gist link to post-optimization report

* feat(core,web): shared PR enrichment — dashboard reads from metadata

CLI lifecycle manager now persists batch enrichment data and review
comments to session metadata files (prEnrichment + prReviewComments
keys). The web dashboard reads from metadata instead of calling
GitHub API.

Changes:
- lifecycle-manager: add persistPREnrichmentToMetadata() after poll,
  write prReviewComments in maybeDispatchReviewBacklog()
- serialize: replace enrichSessionPR (6 API calls) with metadata read
- services: stop web lifecycle polling (keep for webhook checks)
- cache: remove prCache (no longer needed)
- routes: remove timeout wrappers and cacheOnly pattern

Eliminates ~237 calls / 15 min (54% of all AO-side traffic).
Dashboard data freshness improves from 5min to 30s.

* fix(web): remove unused beforeEach import in serialize test

* docs(experiments): add final trace report — 56% GraphQL reduction achieved

5-session, 24-min trace after all optimizations including shared
enrichment. GraphQL 905/hr (was 2,072), REST 5/hr (was 168).
Single lifecycle manager confirmed. Dashboard API calls eliminated.
Max sessions before budget exhaustion: ~27 (was ~12).

* docs(experiments): add REST budget breakdown to final report

* fix(core): use storageKey for getSessionsDir in persistPREnrichmentToMetadata

* fix(test): use OpenCodeSessionManager type in plugin-integration tests

* fix(test): update bugbot-comments and auto-cleanup tests for new review API

* fix(web): fix syntax error and missing import from rebase

* docs(experiments): add complete rate-limiting change log and update final report numbers

* fix(web): fix tmux session resolution for legacy wrapped storageKeys

* fix(web): pass tmuxName directly to terminal server instead of reverse-resolving

* perf(core): gate detectPR behind Guard 1 ETag — skip when PR list unchanged

* perf(core): always run Guard 1 for all repos, dedup issue views, include threadId in review messages

* feat(core): add Guard 3 (review ETag), enrich review data with summaries, dedup issue views, gate detectPR for all repos

* fix(web): reuse cached tmuxSessionId on re-open, add 15-session trace report and comparison docs

* perf(scm-github): reduce contexts to first:10, add -i to review GraphQL for rate limit tracing

* feat(core): merge CI details into transition, enrich merge conflict message, reduce batch contexts, add graphqlCost tracing

* chore(experiments): remove working artifacts, keep final reports and reference docs

* chore: remove experiments directory

* refactor: remove getAutomatedComments from SCM interface and all implementations

* fix: address all PR review comments

- gh-trace: make binary resolution async via fs.access (no event loop
  blocking, no shell injection), cache mkdir for trace writes, async
  fire-and-forget appendFile, document 10MB maxBuffer rationale
- lifecycle-manager: log detectPR failures via observer instead of
  silent catch, add getPRState fallback for terminal states
  (merged/closed) when batch enrichment cache misses
- scm-gitlab: implement getReviewThreads with bot+human threads and
  isBot flag, fixing silent feature regression after
  getAutomatedComments removal
- scm-github: clear reviewThreadsCache in invalidatePRCache, document
  first:11 CI checks cost budget
- runtime-tmux: use printf+JSON.stringify for PATH export to prevent
  shell injection from single quotes
- agent-workspace-hooks: add cache timestamp sanity check, include
  --repo in cache keys to prevent cross-repo collisions
- services: document dashboard dependency on CLI polling
- tests: update gh binary path assertions for resolved paths

* fix: address all 17 PR review comments

- gh-trace: use path.delimiter, cache-only-on-success, last HTTP status
  line, await writes with warn-once, redact secrets, gate JSON.parse
- agent-workspace-hooks: validate cache keys, redact wrapper trace args,
  sha256 cache keys to prevent collisions, 120s TTL ceiling
- session-manager: skip PATH wrappers for claude-code (native hooks)
- types: add deprecation JSDoc for getReviewThreads
- graphql-batch: clear Guard 3 in clearETagCache, re-read ETag on 304,
  switch Guard 2 to check-runs endpoint, drop per_page=1 from Guard 3
- Add gh-trace unit tests for extractOperation, redactArgs, parseHttp

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: adil <adil.business4064@gmail.com>
Co-authored-by: iamasx <adilshaikh4064@gmail.com>
2026-04-25 18:57:04 +05:30
Priyanshu Choudhary f0eec60dfb Merge origin/main into feat/windows-platform-adapter
Resolve tmux-utils.ts conflict: rewrite resolvePipePath to read
runtimeHandle.data.pipePath from on-disk session metadata instead of
recomputing a hash from AO_CONFIG_PATH. Mirrors the storageKey
disambiguation pattern upstream introduced for resolveTmuxSession in
PR #1488 (eca3001c). Recomputing the hash drifted from the runtime's
deriveStorageKey on Windows (raw backslash paths vs POSIX-normalized),
so the dashboard's named-pipe relay was connecting to the wrong path
and falling back to ENOENT while ao session attach worked fine.

Also normalize path separators in 6 upstream resolveTmuxSession tests
that hard-coded Unix forward slashes in their exists() mocks; on
Windows path.join produces backslashes and the assertions never
matched.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 16:55:30 +05:30
Priyanshu Choudhary 0dcbdb2673 fix(windows): plugin runtime gaps + ConPTY graceful shutdown
runtime-process:
- Reserve the per-instance processes-map slot before the platform split
  so the Windows ConPTY branch participates in duplicate-create detection
  and getMetrics/getAttachInfo bookkeeping. Previously, Windows returned
  a handle without storing it, so duplicate session IDs were silently
  accepted and getMetrics always reported 0 uptime.
- Add 500ms graceful-exit poll before SIGKILLing the pty-host on destroy
  so node-pty can dispose its ConPTY handle. Skipping this orphaned the
  conpty_console_list_agent helper and triggered Windows Error Reporting
  dialogs (0x800700e8) on real runs, not just tests.
- pty-host: install SIGTERM/SIGINT/SIGHUP/SIGBREAK/beforeExit handlers
  that drive the same shutdown sequence (kill pty, drop clients, close
  pipe, exit after 50ms grace), and route MSG_KILL_REQ through the same
  path. Previously MSG_KILL_REQ only called pty.kill() and left the host
  process lingering.
- Add windowsHide:true to the pty-host child spawn so node-pty's helper
  console window stays hidden on errors.

workspace-worktree: normalize paths to a comparable POSIX form
(backslash→slash, lowercase drive letter) when matching git worktree
list --porcelain output against project directories. git emits
forward-slash paths on Windows; path.join produces native backslashes
— the comparison failed and list() returned empty.

agent-opencode: guard tmux/ps usage with isWindows() in isProcessRunning
so process-runtime sessions on Windows take the PID-signal path instead
of attempting Unix-only commands.

cli/start: detect Windows local paths in isLocalPath (drive letter
prefix, UNC path, .\, ..\) so spawn arguments like C:\... aren't
mistaken for project names.

integration test: replace cat + /tmp with platform-native echo (findstr
"x*" on Windows, cat on Unix) and os.tmpdir(); the original used
Unix-only tooling and would never run on Windows.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 11:25:19 +05:30
yyovil b086908f60
add zsh completion support for ao (#1374)
* feat: add zsh completion for ao (#1371)

Add a generated zsh completion command and dynamic completion backend so ao can tab-complete projects and session IDs without relying on jq or brittle text parsing. Document standard zsh and Oh My Zsh install paths, and cover the new flow with CLI tests.

* fix(cli): address copilot completion review feedback for PR 1374

* fix completion and harden local workflow parsing

* Update packages/cli/src/lib/completion.ts

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

* fix completion regressions and agent-ci review feedback

---------

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-04-24 03:48:25 +05:30
Ashish Huddar f3ce113c4c
Add multi-project storage, resolution, and project settings support (#1343)
* feat: add content-addressed project storage keys

* Add per-project resolution and hardened project routing

* Fix storage-key test isolation

* feat(web): redesign Add Project modal with Finder-native layout

* feat: multi-project support with project sidebar, settings, and improved routing

Add per-project configuration in global-config, project-aware CLI commands
(start/spawn/open/session), workspace-worktree project resolution, redesigned
ProjectSidebar with settings modal, repair flow for degraded projects, project
detail page with loading state, reload API endpoint, and comprehensive tests.

* Fix legacy config storage keys and duplicate project flow

* Fix multi-project storage migration and collision handling

* Fix merge regressions in startup and config handling

* Externalize yaml and zod from the web server bundle

* Fix session prefix matching for hashed tmux names

* Fix multi-project migration regressions

* Ignore generated worktree files in ESLint

* Fallback reload config for local-only projects

* Speed up session refresh and redirect after kill

* Use fresh session lists for dashboard polling

* fix(web): remove unused direct terminal child state

* test(web): mock router in merge conflict actions coverage

* docs: call out filesystem browse rollout requirement

* Add portfolio tests and remove unused decomposer export
2026-04-21 17:45:55 +05:30
harshitsinghbhandari dd83a11503 fix: model missing activity evidence explicitly (#122)
Represent missing activity probes as first-class signal states so lifecycle inference only treats valid idle evidence as proof. This prevents false stuck transitions, keeps API/UI lifecycle truth aligned, and makes root monorepo verification deterministic by serializing recursive build and typecheck.
2026-04-17 19:28:56 +05:30
Harshit Singh Bhandari 42e9be13a5
Merge pull request #114 from harshitsinghbhandari/plan/stage1-lifecycle-foundation
Plan/stage1 lifecycle foundation
2026-04-16 22:09:41 +05:30
Dhruv Sharma 87fb598d87
Merge pull request #1126 from gautamtayal1/fix/spawn-ao-for-opencode
fix (opencode): use local AGENTS.md for OpenCode orchestrator instructions
2026-04-16 18:21:12 +05:30
harshitsinghbhandari 81d88c4412 fix(core): address lifecycle review feedback 2026-04-15 12:41:06 +05:30
harshitsinghbhandari d466118450 feat(core): add canonical lifecycle persistence foundation 2026-04-15 12:41:06 +05:30
Dhruv Sharma ad10dbf7aa
Merge pull request #1119 from harshitsinghbhandari/feat/1072
feat(power): prevent macOS idle sleep while AO is running
2026-04-14 17:26:58 +05:30
yyovil 9bed49d453 fix: scope node types to node packages 2026-04-13 18:25:21 +05:30
Dhruv Sharma 663c61e16d fix: merge main and address review findings for prompt-driven spawn
- Merge main to pick up @composio/ao-core → @aoagents/ao-core rename
  and decomposer removal (#1104)
- Fix @composio/ao-core import in prompt-spawn.test.ts → @aoagents/ao-core
- Remove unreachable dead code in CLI spawn (empty-string guard after || undefined)
- Update format.ts JSDoc to document 8-item fallback chain including userPrompt
- Add clarifying comment on validation/sanitization separation in spawn route
- Integrate userPrompt display into redesigned SessionCard footer

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-11 21:44:57 +05:30
Gautam Tayal 372c1be631 refactor(opencode): simplify AGENTS.md handling and remove unused code 2026-04-11 18:35:18 +05:30
Gautam Tayal fdf46653fc chore: add tests 2026-04-11 15:54:16 +05:30
Dhruv Sharma d908500791
Merge pull request #882 from DNX/feat/linear-spawn-branch-name
Honor Linear branchName as single source of truth for spawn worktree branches
2026-04-11 14:45:57 +05:30
harshitsinghbhandari 5e1414e7ad feat(power): prevent macOS idle sleep while AO is running (#1072)
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>
2026-04-11 00:11:45 +05:30
Dhruv Sharma e18f9bee33 fix(tests): fix prompt-spawn integration test import and type annotations
The test file imported from @composio/ao-core but main renamed packages
to @aoagents/ao-core. Also added Session type annotations to all find()
callbacks to satisfy strict typecheck.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-10 22:03:04 +05:30
Prateek 967e864f5a chore: rename @composio scope to @aoagents across all packages
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
2026-04-09 15:59:33 +00:00
Dhruv Sharma ea0a01326b fix(types): restore pinnedSummary field alongside new userPrompt
We accidentally replaced pinnedSummary with userPrompt instead of
adding alongside it. Both fields are needed.

Also add blue-green tests demonstrating the before/after delta for
the prompt-driven spawn feature (issue #974):
- packages/web/src/__tests__/prompt-spawn.test.ts (14 tests)
- packages/integration-tests/src/prompt-spawn.integration.test.ts (7 tests)
2026-04-09 18:43:55 +05:30
Denis Darii f92c5f6c9d Validate Linear branchName before spawn 2026-04-03 09:59:58 +02:00
Denis Darii 693ab290b4 Extend Linear integration test: getIssue returns non-empty branchName
Made-with: Cursor
2026-04-02 22:17:26 +02:00
harshitsinghbhandari dd21054f48 extend opencode discovery timeout, include integration tests in pnpm test 2026-03-29 16:44:53 +05:30
Ashish Huddar 4a842d4742 Fix ao launcher linking and mobile toast offset 2026-03-26 22:47:25 +05:30
Harsh Batheja 9794291319
fix: make opencode bootstrap exit before attach (#427)
* fix: make opencode bootstrap exit before attach

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* test: align opencode integration assertions with bootstrap resume flow

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>
2026-03-11 20:02:15 +05:30
Harsh Batheja 240d6423fb
fix: opencode lifecycle race hardening (#359)
* fix: opencode lifecycle race hardening

This hardens the OpenCode session lifecycle to prevent race conditions that create orphan sessions:

Root Cause:
- Concurrent spawnOrchestrator calls could both check for existing orchestrator, see none exists, and proceed to create runtime + write metadata simultaneously, leading to orphan sessions
- Fallback title discovery was sorted oldest-first instead of newest-first, causing wrong session selection when duplicates exist

Changes:
1. spawnOrchestrator: Add atomic session ID reservation before creating runtime/metadata
   - Uses reserveSessionId to prevent check-create race
   - If reservation fails, checks if existing session is alive and reuses under reuse strategy
   - Never creates duplicate/orphan runtime on reservation conflict

2. agent-opencode plugin: Improve session discovery
   - Title-based fallback now sorts by updated timestamp (newest first)   - Validates session IDs with ses_ prefix pattern
   - Handles numeric and string timestamps in sorting

Tests added:
- spawnOrchestrator reuses concurrent alive session
- spawnOrchestrator throws when session not in reusable state
- spawnOrchestrator never creates duplicate runtime on conflict
- Invalid session ID rejection tests
- Newest-first fallback sorting tests

* fix: opencode lifecycle race hardening

This hardens the OpenCode session lifecycle to prevent race conditions that create orphan sessions:

Requirements:
1) Exact session-id capture from opencode run --format json stream
2) Fallback title discovery for missing/parse fails
3) Atomic reservation before runtime/metadata write
4) Never create duplicate/orphan runtime on reservation conflict

5) Never persist invalid opencodeSessionId

Mandatory tests: preferred exact-id path, newest fallback selection, reservation conflict no duplicate runtime, invalid-id rejection

- Fixed lint error in agent-opencode plugin test file
- Fixed the failures: spawnOrchestrator reservation logic breaks existing orchestrator reuse behavior
- All core tests now pass except 1 failing in the plugin-integration test (which needs to be resolved separately)

* fix: lint error and skip failing test

- Rename unused buildContinueSessionCommand to _buildContinueSessionCommand
- Skip 'reuses archived OpenCode mapping' test - pre-existing bug where findOpenCodeSessionIds only checks latest archived metadata, not all versions

* fix: integration test expectations for --format json flag and2>&1

* fix: harden orchestrator session reservation and align opencode session tests

* fix(core): keep claim-pr ownership consolidation automatic

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* test(opencode): remove unused test scaffolding

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* fix(core): preserve in-progress orchestrator reservations

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* fix(opencode): escape discovery failure message

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* refactor(opencode): rename discovery option suffix

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* fix(opencode): keep fallback shell syntax valid

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* fix(core): enforce project pause in session manager

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* chore: trigger bugbot re-review

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* test: restore archived mapping coverage and strict mock fallback

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* fix(core): enforce project pause for orchestrator spawn

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* chore: retrigger bugbot pass

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

---------

Co-authored-by: Harsh <harsh@Ubuntu-24-Forrest.lan>
Co-authored-by: Harsh <harsh@example.com>
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-03-10 15:33:08 +05:30
Harsh Batheja 4edf19df32
feat: lifecycle manager, backlog auto-claim, task decomposition, and verification gate (#365)
* 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>
2026-03-10 12:31:25 +05:30
Harsh Batheja cf31dee0b8
fix: implement PR325 session capture fallback and spawn race hardening (#366)
* 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
2026-03-10 08:42:07 +05:30
prateek a99d37b1d0
feat(ao): add OpenClaw notifier plugin for AO escalations
Adds AO notifier-openclaw (webhook-first Phase 0), registry wiring, tests, docs, and BugBot fixes.
2026-03-09 18:24:30 +05:30
Harsh Batheja cc2031f0f6
fix: bugbot follow-ups from PR #315 (#357)
* 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
2026-03-08 12:50:54 +05:30
Harsh Batheja 4e2144d99e
feat: OpenCode session lifecycle and CLI controls (#315)
* feat: refine OpenCode session reuse strategy and cleanup

* fix: harden OpenCode session selection and lint errors

* refactor: centralize OpenCode reuse resolution flow

* fix: return 404 for missing session in message route

* feat: replace force remap with terminal reload control

* fix: protect project path from session kill cleanup

* fix: preserve fullscreen alignment without reload action

* fix: harden OpenCode session id handling and title reuse selection

* fix: show OpenCode reload control and remap before restart

* fix: preserve title-only OpenCode reuse with fallback mapping persistence

* fix: resolve remaining PR315 Bugbot findings

* fix: keep OpenCode discovery title-based without timestamp sorting

* fix: avoid enrichment race fallout in session listing

* fix: guard OpenCode discovery parse with array check

* fix: stabilize Linear comment integration check

* fix: harden OpenCode discovery and prompt option flow

* ux: clarify OpenCode terminal restart action

* fix: remap OpenCode session fresh on each restart

* fix: validate remap session ids before reuse

* fix: clean archived metadata only after purge

* docs: align OpenCode remap selection with title-based behavior

* fix: harden opencode cleanup and ignore local sisyphus state

* test: add timeout cleanup coverage for session enrichment

* fix: harden Linear integration helper against transient non-JSON errors

* fix: make linear integration assertions resilient to eventual consistency

* fix: remove unused fs import after rebase

* fix: address remaining Bugbot blockers for opencode session handling

* fix: avoid stale metadata overwrite during restore post-launch

* fix: forward subagent in orchestrator flows and defer reuse lookup

* fix: apply configured subagent fallback for session spawn

* fix: scope archived cleanup by project and delay archive restore write

* fix: normalize orchestrator strategy aliases in start display logic

* fix: centralize orchestrator strategy normalization in core

* fix: derive orchestrator reuse display from spawn result

* fix: keep cleanup results consistent across project-id collisions

* fix: namespace cleanup results when session IDs collide

* fix: harden GitHub issue stateReason fallback

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* fix: avoid false failing CI state mapping

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* fix: add tmux command timeouts

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* fix: bound session API enrichment latency

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* fix: repair scm-github merge resolution

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* fix: repair lifecycle-manager test merge

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* fix: delay archive metadata recreation until restore passes

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* test: restore claim-pr session mocks

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>

* fix: address review findings for OpenCode lifecycle PR

- Fix stripControlChars to preserve newlines for reload commands
- Add SessionNotFoundError and use instanceof checks in API routes
- Document orchestratorSessionStrategy in YAML example
- Validate existingSessionId with asValidOpenCodeSessionId()
- Extract inline Node script to buildSessionLookupScript helper
- Create OpenCodeSessionManager interface for remap capability
- Create OpenCodeAgentConfig type for agent-specific config
- Change default orchestratorSessionStrategy from delete to reuse

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: guard reused session display without metadata

* chore: add agent config files to .gitignore

Agent configuration files (CLAUDE.md, AGENTS.md, IMPROVEMENTS.md, etc.) are personal and project-specific. They should not be committed to the repository.

Changes:
- Remove CLAUDE.md from git tracking
- Add agent config files to .gitignore
- Create .gitignore-template for reference

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* chore: update gitignore for agent config folder structure

Reorganized agent configuration files:
- CLAUDE.md and AGENTS.md stay in root (agents read them there)
- Tracking files move to .opencode/ (IMPROVEMENTS.md, etc.)
- Optional Claude files in .claude/

Updated .gitignore to ignore folders instead of individual files.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: tighten opencode remap and session discovery safeguards

* fix: address Bugbot findings in session manager

* fix: scope opencode discovery to opencode sessions

* fix: restore concurrent listing and strict permission literals

* fix: throw SessionNotFoundError, parallelize list enrichment, fix permissions type

- Session manager now throws SessionNotFoundError instead of plain Error
  for missing sessions, so web API routes correctly return 404 (not 500)
- Parallelize session enrichment in list() — was sequential, causing O(N)
  latency for N sessions with subprocess enrichment
- Fix AgentLaunchConfig.permissions type to accept legacy "skip" value
  (AgentPermissionInput instead of AgentPermissionMode)
- Add happy-path and validation tests for /api/sessions/:id/message route
- Update all test mocks to use SessionNotFoundError

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: route ao send through session manager

* feat: add purge option to orchestrator stop

* fix: register opencode agent in web services

* test: align web API missing-session coverage

* fix: match notifier config by plugin name

* refactor: dedupe session lookup and tmux buffer send flow

* test: update send lifecycle wait expectation

* fix: harden send routing and cleanup purge controls

---------

Co-authored-by: Harsh <harsh@Ubuntu-24-Forrest.lan>
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Prateek <karnalprateek@gmail.com>
2026-03-08 09:55:44 +05:30
Prateek b865549ea9 fix(codex): harden gh wrapper resolution with explicit GH_PATH 2026-03-08 02:55:31 +05:30
Prateek cb2b386f99 test: align codex integration test with AgentPermissionMode 2026-03-07 21:01:32 +05:30
Prateek e81a8a4e27 test: add codex launch/env integration coverage 2026-03-07 20:37:36 +05:30
prateek a4a1a32e96
fix: auto-detect free port in ao start <url> config generation (#275)
* 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>
2026-03-03 15:03:20 +05:30
prateek 91dd7cc15f
fix: keep Claude Code interactive after initial prompt (#145)
* fix: keep Claude Code interactive after initial prompt

Claude Code's -p flag runs in one-shot mode (exits after responding),
which prevents follow-up messages via `ao send`. Instead, launch Claude
interactively and deliver the initial prompt post-launch via
runtime.sendMessage().

Adds `promptDelivery` property to the Agent interface so each agent
plugin can declare whether prompts should be inlined in the launch
command or sent after the agent starts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: make post-launch prompt delivery non-fatal and add test coverage

- Move sendMessage call outside the try/catch that destroys the session.
  A prompt delivery failure should not kill a running agent — user can
  retry with `ao send`.
- Add tests: no-prompt + post-launch agent, sendMessage failure resilience,
  5s delay verification, systemPrompt/systemPromptFile alongside omitted -p.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* test: add integration test for prompt delivery (proves bug and fix)

Two real-Claude integration tests that contrast:
1. `-p` mode: Claude exits after responding (the bug)
2. Interactive + sendMessage: Claude stays alive, follow-up works (the fix)

Runs in CI with ANTHROPIC_API_KEY. Skips when prerequisites missing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(test): skip interactive test without auth, improve TUI readiness detection

The integration test failed in CI because interactive Claude requires
full login auth (not just ANTHROPIC_API_KEY). Skip the interactive suite
when `claude auth status` reports not logged in.

Also fix local flakiness: replace blind 5s sleep with polling for
Claude's TUI prompt character (❯) before sending the first message,
and increase scrollback capture from 200 to 500 lines.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(test): skip interactive test in CI, fix TUI readiness detection

The interactive test ran in CI despite hasInteractiveAuth() — Claude
reports logged in when ANTHROPIC_API_KEY is set, but interactive mode
requires OAuth. Use `!process.env.CI` as the skip condition instead.

Also fix waitForTuiReady false positive: the OAuth screen's
"Paste code here if prompted >" matched the `>` regex. Now checks
the last non-empty line for Claude's specific ❯ prompt character,
and bails early if the OAuth/login screen is detected.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 18:11:06 +05:30
prateek 40c1906d41
feat(web): redesign dashboard, session detail, and orchestrator terminal (#125)
* docs: add design research artifacts — briefs, token reference, screenshots

Comprehensive design research package for the ao dashboard, session
detail page, and orchestrator terminal. Produced via competitive analysis
of 14 products (Linear, Vercel, Railway, Fly.io, Inngest, WandB, LangSmith,
Supabase, and more) + Playwright CSS extraction from live sites + full
codebase audit.

Artifacts:
- docs/design/design-brief.md            Main design brief (v2, Playwright-updated)
- docs/design/session-detail-design-brief.md   /sessions/[id] design spec
- docs/design/orchestrator-terminal-design-brief.md  Orchestrator page spec
- docs/design/token-reference.css        Drop-in CSS replacement for globals.css
- docs/design/competitive-analysis-raw.md  Raw research notes, all 14 sites
- docs/design/design-brief-v1.md         Original text-only brief (pre-Playwright)
- docs/design/README.md                  Index + research methods summary
- docs/design/screenshots/linear-homepage.png   Playwright-captured screenshot
- docs/design/screenshots/railway-homepage.png  Playwright-captured screenshot

Key findings:
- Linear CSS token values verified via Playwright (body bg #08090A, accent
  #7070FF, Berkeley Mono monospace, type scale, radius, transitions)
- Recommended palette: #0C0C11 base (blue-cast dark vs current GitHub #0d1117)
- Highest-impact change: load Inter Variable via next/font/google
- Orchestrator terminal needs visual differentiation (violet accent, status strip)
- token-reference.css is ready to drop into packages/web/src/app/globals.css

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(web): redesign dashboard, session detail, and orchestrator terminal

Implements a cohesive dense dark-mode design system across all three main views.

- New color token palette: #0c0c11 base, #141419 surface, #1c1c25 elevated
- Accent blue #5b7ef8, status semantics (ready/error/attention/working/idle/done)
- Violet accent #a371f7 reserved for orchestrator
- Inter Variable + JetBrains Mono loaded via next/font with CSS variables
- activity-pulse keyframe for live agent dots

- AttentionZone header: dot + label + flex divider + count pill + chevron
- Sessions laid out in responsive 1→2→3 column grid
- Solid green merge button (translateY hover), no confirm() dialog

- Breadcrumb nav: ← Agent Orchestrator / {session-id} [orchestrator badge]
- CSS 8×8px activity dot with pulse animation replaces emoji labels
- Merge-ready state: green-bordered banner with checkmark icon
- Orchestrator sessions show zone counts strip (merge/respond/review counts)

- xterm.js dark theme (#0a0a0f bg, #d4d4d8 fg, full 16-color ANSI palette)
- variant prop: "agent" (blue cursor) vs "orchestrator" (violet cursor)
- Dynamic height prop instead of fixed 600px; fullscreen toggle with SVG icons

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(web): strip rainbow stats, clean header, IBM Plex Sans typography

- Replace Inter with IBM Plex Sans (technical tool aesthetic, distinctive numerics)
- Replace 4-color big-number stats bar with a single compact inline status line
  in the header: "35 sessions · 1 working · 9 PRs" — no decorative colors
- Remove the two-tone "Agent (blue) Orchestrator (white)" title — just "Orchestrator"
- Remove ClientTimestamp (useless) — replaced by orchestrator nav link
- Zone headers: colored dot only (semantic), neutral uppercase label, plain count
  — removes the rainbow-colored label text that read as a widget template
- Add subtle radial gradient glow at top of page for depth

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(web): kanban layout, amber accent, full-width, bigger stats

- Switch accent from blue (#5b7ef8) to amber/gold (#d18616) throughout
- Replace grid layout with horizontal Kanban columns for active zones
  (merge, respond, review, pending, working), Done stays full-width below
- Remove max-w-[1100px] constraint — full viewport width
- Header stats numbers 20px bold (was 12px), orchestrator link is now a
  visible bordered button
- AttentionZone gains variant="column" for Kanban mode (compact header
  with count pill, vertical card stack)
- Update all hardcoded rgba(91,126,248,...) in SessionCard to amber

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(web): layout, alert sizing, column order, button feel

- Kanban column order: working→pending→review→respond→merge
  (left = in progress, right = ready to ship)
- Columns use flex-1 min-w-[200px] to fill available width
  instead of fixed 260px leaving half the page empty
- Alert badges: inline-flex wrapper prevents stretching to full
  row width when wrapping
- Terminal button: add bg-subtle fill so it reads as a button
- PR number (#91): remove opaque pill background, now plain
  amber text link — clearly a hyperlink
- Merge PR button: pt-0.5 spacer above the action area

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(web): don't cache rate-limited partial PR data

When GitHub rate limits fire, enrichSessionPR was caching the
bad partial data (0 additions, CI failing) for 60 seconds, causing
the dashboard to show incorrect data for the full TTL window.

- Skip cache write when majority of API calls failed
- Downgrade console.error → console.warn (this is handled/expected)

The next page refresh will retry live API calls, so data recovers
as soon as the rate limit window resets.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(web): graceful GitHub API rate limit handling in UI

When the GitHub plugin hits rate limits, the dashboard now:

- Shows a single amber banner: "GitHub API rate limited — PR data
  (CI status, review state, sizes) may be stale. Will retry on
  next refresh."
- Hides CI badge, review decision, and size pill on PR cards
  (they'd show wrong values: +0 -0 XS, CI failing)
- Shows a subtle "⚠ PR data rate limited" note on affected cards
  instead of misleading alert badges
- Skips CI/review/conflict-based attention zone classification
  for rate-limited PRs (prevents sessions moving to Review due
  to phantom "CI failing" from the fallback value)
- Doesn't cache partial rate-limited data so next refresh retries
  live API calls as soon as the rate limit window resets

What still works when rate limited:
- Session ID, title, branch, PR number/link
- Session activity status (working/spawning/etc.)
- Merge button if mergeability was already cached
- Restore/terminate/send actions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(web): dashboard redesign — glassmorphism, Kanban, rate-limit handling, perf fix

Design:
- Kanban layout: active zones as flex columns (working→pending→review→respond→merge),
  Done as full-width grid below
- GitHub dark color palette (main's tokens) with glassmorphic card surfaces
  (rgba bg + backdrop-blur) and subtle blue/violet body gradient
- Activity state shown as labeled pill (● active / ● idle etc.) instead of bare dot
- Session card: title on its own row, larger font, inline-flex alert badges (no stretch)
- PR number rendered as plain accent link, not a blue pill badge
- Terminal button has background fill to feel like a button
- Info circle icon replaces alarming warning triangle for rate-limit indicators
- "1 working" → "1 active" in header stats
- PR table constrained to max-w-[900px] and centered
- Orchestrator session no longer uses purple accent

Rate limiting:
- isPRRateLimited() helper; getAttentionLevel() skips PR classification when limited
- Rate-limited banner in Dashboard; suppressed CI/size/review badges in PRStatus
- SessionCard shows subtle "PR data rate limited" indicator; getAlerts() returns []
- serialize.ts: rate-limited enrichment results cached for 5 min (not 60s) to stop
  retrying 168 failing API calls every minute

Performance:
- page.tsx: 4s hard timeout on PR enrichment — serves stale data fast instead of
  blocking SSR for 75s under rate limiting
- cache.ts: TTLCache.set() accepts optional ttl override for per-entry control

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: suppress stale size/CI/review in PR table when rate limited

PRTableRow now shows "—" for size, CI, and review columns when GitHub
API is rate limited, matching the card view which already hides these.
Prevents misleading "+0 -0 XS" size and "needs review" labels from the
default fallback values.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: 3D card effect with depth shadow and top-edge shine

Cards now clearly pop against the dark background:
- Solid gradient bg (rgba(28,36,47) → rgba(18,23,31)) instead of
  near-invisible rgba(22,27,34,0.8) surface
- Layered box-shadow: contact shadow + diffuse depth + inset top highlight
  that simulates light hitting the card's top edge (the "shine")
- Hover: card lifts 2px with deeper shadow
- Merge-ready: green-tinted bg with green ambient glow + stronger lift on hover

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: restore text legibility inside session cards

The darker solid card gradient made muted/secondary text nearly
invisible — #484f58 (text-muted) had only ~2:1 contrast on the
new card bg. Override the color tokens locally within .session-card
to GitHub's established dark-mode legibility values:

  --color-text-muted:     #484f58 → #656d76  (3.8:1 on card bg)
  --color-text-secondary: #7d8590 → #8b949e  (6.2:1 on card bg)
  --color-text-tertiary:  #484f58 → #656d76

Scoped to .session-card so the rest of the UI is unchanged.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: address bugbot comments — fonts, review zone, ActivityDot, orchestrator btn

- layout.tsx: add IBM Plex Sans weight 700 (was missing, font-bold falling
  back to 600)
- DirectTerminal.tsx: use "IBM Plex Mono" instead of unloaded "JetBrains Mono"
- SessionDetail.tsx: add review zone to OrchestratorStatusStrip (was omitted,
  sessions with CI failures were invisible in the strip)
- ActivityDot.tsx: extract shared component, remove duplicate implementations
  in SessionCard.tsx and SessionDetail.tsx
- Dashboard.tsx: redesign orchestrator button with 3D glass style matching
  card aesthetic (blue-tinted bg, depth shadow, hover lift)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(web): lint — eqeqeq, duplicate import, unused var

- ActivityDot.tsx: != → !== (eqeqeq rule)
- PRStatus.tsx: merge duplicate @/lib/types imports into one
- SessionCard.tsx: remove unused activityIcon import

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(web): elevate session detail + orchestrator page design

- Nav: glass backdrop-blur effect with chevron back link
- Header: detail-card 3D treatment with left-border accent keyed to activity color
- Meta chips: bordered pill style with subtle bg instead of flat text
- Status tag: pill badge for status instead of plain text
- PR card: detail-card 3D treatment, border-color reflects PR state
- PR merged badge: purple pill instead of gray text
- Unresolved count: red pill badge in section header
- Blockers section: renamed "Issues" → "Blockers"
- Terminal section: colored bar indicator instead of plain label
- Orchestrator status strip: total agent count + per-zone colored pills
- globals.css: add .nav-glass and .detail-card classes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(web): fetchZoneCounts parses body.sessions, delayed 2s to avoid contention

The /api/sessions endpoint returns `{ sessions: [...] }` not a bare array.
fetchZoneCounts was treating the whole response object as an array, so
zone counts were always zero on the orchestrator detail page.

Also delays the initial fetchZoneCounts call by 2s so it doesn't contend
with the session fetch on page load (both hit /api/sessions which is slow
when GitHub enrichment is running).

Also includes: Playwright kill-Chrome-for-Testing tip in CLAUDE.md,
toned-down detail-card shadow in globals.css.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* perf+test(web): cache-first PR enrichment, skip exited sessions, fix component tests

Performance improvements:
- enrichSessionPR() now accepts cacheOnly option and returns boolean
- /api/sessions/[id]: serve from cache immediately, only block on first load
- /api/sessions: skip PR enrichment for EXITED sessions (no longer changing)
- cache: increase default TTL from 60s to 5 minutes

Test fixes (match redesigned SessionCard + AttentionZone):
- "restore session" (header) → "restore"; expanded panel still shows "restore session"
- "merge PR #N" → "Merge PR #N" (capital M)
- "CI status unknown" → "CI unknown"
- "ask to fix CI" / "ask to fix CI" → "ask to fix"
- "terminate session" → "terminate"
- Zone labels: RESPOND/WORKING/DONE → Respond/Working/Done (CSS uppercase is visual only)
- "working" zone no longer collapsed by default; collapse tests now use "done" zone

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(core): ActivityDetection with timestamp propagation

- Add ActivityDetection interface { state, timestamp? } to types.ts
- Agent getActivityState() returns ActivityDetection | null instead of
  ActivityState | null, allowing timestamp from JSONL mtime to propagate
- session-manager updates session.lastActivityAt when detected.timestamp
  is more recent — fixes "active 22h ago" showing stale timestamps
- Update all agent plugins (claude-code, aider, codex, opencode) to
  return ActivityDetection objects

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(web): dismissible rate limit banner + 60min rate-limit cache TTL

- Add X dismiss button to GitHub API rate limit banner in Dashboard.tsx
  so it can be closed during demos
- Extend rate-limited PR cache TTL from 5min to 60min — GitHub GraphQL
  rate limits reset hourly, no point retrying every 5 minutes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(web): address Cursor Bugbot review comments on PR #125

- Dashboard StatusLine: active sessions count now uses var(--color-status-working)
  (blue) instead of neutral text color, matching the design system semantics
- SessionCard: isReadyToMerge now guards against rate-limited state — a card
  with stale cached mergeability data won't show green merge-ready styling
- DirectTerminal: add `variant` to useEffect dependency array (was missing,
  causing stale cursor/selection colors if variant changed after mount)
- agent-aider: include `timestamp: chatMtime` in all ActivityDetection returns,
  matching the pattern used by agent-claude-code (enables accurate lastActivityAt)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(ci): resolve lint, typecheck, and test failures

Lint:
- Remove unused parseJsonlFile function (superseded by parseJsonlFileTail)
- Remove dead lastLogModified stat() call in getSessionInfo (field was
  removed from AgentSessionInfo but the filesystem read was left behind)

Typecheck + Tests (ActivityDetection):
- session-manager.test.ts: update mocks to return { state: "active" } /
  { state: "idle" } instead of bare strings — getActivityState() returns
  ActivityDetection | null, not ActivityState | null
- integration tests (aider, claude-code, codex, opencode): update imports
  from ActivityState → ActivityDetection, variable types, comparisons
  (activityState?.state !== "exited"), and assertions (?.state).toBe()

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: parseJsonlFileTail uses readFile for small files; enrich exited sessions with PRs

- parseJsonlFileTail now calls stat() then readFile() for files smaller than
  maxBytes, falling back to open()/handle.read() only for large files. This
  fixes the test infrastructure (which mocks readFile but not open) and also
  fixes a scope bug where `offset` was declared inside an inner try block but
  referenced outside both try blocks.
- Math.max(0, NaN) returns NaN not 0, so size must default to 0 when stat
  returns a mock without a size field: `const { size = 0 } = await stat(...)`.
- Update activity-detection.test.ts: getActivityState() now returns
  ActivityDetection objects, so tests use (await ...)?.state comparisons.
- Remove stale lastLogModified test (field was removed from AgentSessionInfo).
- Remove EXITED skip guard from api/sessions/route.ts: exited sessions can
  still have open, merge-ready PRs that need enrichment on the dashboard.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: comprehensive code review fixes — tests, timestamps, UI correctness

Address gaps identified in code review of the ActivityDetection PR:

Core / Session Manager:
- Add `timestamp` to all `{ state: "exited" }` returns in all 4 agent plugins
  (claude-code, aider, codex, opencode) using consistent `exitedAt = new Date()` pattern
- Add 2 new session-manager tests: timestamp propagation when detection timestamp
  is newer, and no-downgrade when detection timestamp is older
- Fix `parseJsonlFileTail` lint error: remove useless `= 0` initializer (value was
  always overwritten before use; catch block returns early)

Web package — tests:
- Fix 3 `api-routes.test.ts` failures: `sessionsGET()` needs a Request object since
  the route reads `request.url` for `?active=true` query param
- Fix `serialize.test.ts` rate-limit test: spy on `console.warn` (what the code uses)
  not `console.error`
- Add 5 `ActivityDot` component tests covering all activity states, unknown states,
  null activity, and dotOnly mode

Web package — UI correctness:
- Fix `relativeTime()` in SessionDetail to guard against invalid/empty ISO strings
- Fix timer Map leak: add `timersRef.current.clear()` in cleanup effect after forEach
- Add `encodeURIComponent` to sessionId in message fetch URL

Server — race condition fix:
- Guard `activeSessions.delete` in pty.onExit, ws.on("close"), and ws.on("error")
  against stale handlers deleting a newly-registered session with the same ID.
  Fixes flaky integration test where afterEach's pty.kill() fired asynchronously
  after the next test had already set up a new session with the same session ID.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(web): narrow PREnrichmentData types to eliminate unsafe casts in serialize

PREnrichmentData.ciStatus and .reviewDecision were typed as string,
requiring unsafe `as` casts when reading from cache into DashboardPR.
Narrow them to the same literal union types used by DashboardPR, making
the casts unnecessary. Also narrow ciChecks[].status to match CoreCICheck.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-20 18:43:57 +05:30
prateek 59c490a3af
fix: dashboard config discovery + CLI service layer refactoring (#70)
* fix: config discovery, activity detection, and metadata port storage

- findConfigFile() checks AO_CONFIG_PATH env var (resolved to absolute path)
- loadConfig() delegates to findConfigFile() for consistent validation
- Pure Node.js readLastJsonlEntry (no external tail binary), safe for
  multi-byte UTF-8 at chunk boundaries
- Added "ready" activity state to agent plugins
- Store dashboardPort, terminalWsPort, directTerminalWsPort in session
  metadata so ao stop targets the correct processes
- Zod schema port default aligned with TypeScript interface

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: dashboard config discovery + CLI service layer refactoring

- Config discovery via AO_CONFIG_PATH env var
- Auto port detection with PortManager
- Activity detection with ready state, pure Node.js readLastLine
- 5 CLI services: ConfigService, PortManager, DashboardManager, MetadataService, ProcessManager
- Store all service ports in metadata for ao stop
- Set NEXT_PUBLIC_ env vars for frontend terminal components
- Multi-byte UTF-8 safe readLastJsonlEntry
- Tests for all new services and utils

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: address bugbot review comments (port fallback + systemPrompt)

1. Align port fallback to 3000 everywhere (matching Zod schema default):
   - start.ts: config.port ?? 3000
   - dashboard.ts: config.port ?? 3000
   - types.ts JSDoc: "defaults to 3000"
   - orchestrator-prompt.ts: already correct at 3000

2. Add --append-system-prompt to Claude Code plugin's getLaunchCommand
   so orchestrator context is actually passed to the Claude agent.
   Previously systemPrompt was generated but silently dropped.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: remove dead ConfigService mock from status test

The vi.mock for ConfigService.js referenced a deleted module.
Config mocking is already handled by the @composio/ao-core mock.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: extract shared buildDashboardEnv to eliminate duplication

Dashboard env construction (AO_CONFIG_PATH, PORT, NEXT_PUBLIC_*) was
duplicated between start.ts and dashboard.ts. Extracted into
buildDashboardEnv() in web-dir.ts (already shared by both commands).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 17:08:48 +05:30
prateek 73957182f7
fix: activity detection — fix path encoding bug, add ready state (#71)
* fix: activity detection — fix path encoding, use tail -1 for JSONL

Two tightly coupled infrastructure fixes:

- Fix toClaudeProjectPath(): leading `/` becomes `-` (not stripped),
  matching Claude Code's actual project directory naming convention.
- Replace manual 4KB buffer read in readLastJsonlEntry() with
  `tail -1` + JSON.parse — handles any file size, any line length,
  and eliminates the truncated-line edge case entirely.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: add "ready" state, return null when unknown, remove dead code

Behavioral changes to activity detection:

- Add "ready" to ActivityState — separates "alive at prompt" from
  "idle/stale". Configurable via readyThresholdMs (default 5 min).
- Agent plugins return null when they can't determine activity
  (no workspace, no JSONL, no per-session tracking). Session manager
  preserves existing activity instead of overwriting with a guess.
- Remove isProcessing() from Agent interface — zero callers in
  production code, fully superseded by getActivityState().
- Remove extractLastMessageType() from claude-code — the field it
  populated (lastMessageType) was only consumed by the old inline
  CLI mapping, which is now replaced by plugin delegation.
- CLI status delegates to agent.getActivityState() (single source
  of truth) with metadata fallback when plugin returns null.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* test: comprehensive activity detection coverage

- activity-detection.test.ts: 42+ tests covering path encoding,
  getActivityState edge cases (exited/null/fallback), real Claude Code
  JSONL types, agent interface spec types, staleness thresholds,
  JSONL file selection, and realistic session sequences.
- status.test.ts: plugin delegation tests — verifies CLI uses
  agent.getActivityState() as single source of truth, passes
  readyThresholdMs from config, falls back to metadata on null/throw.
- Integration tests: updated type expectations for null returns from
  codex, opencode, and aider; added "ready" to valid state lists.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: flaky Linear integration test + missing ready label in SessionDetail

Linear API has eventual consistency — updateIssue state changes don't
propagate instantly. Poll with retries instead of asserting immediately.

Also adds "ready" entry to SessionDetail activityLabel map (was missing,
causing fallback to dim/unstyled rendering).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: pure Node.js readLastJsonlEntry, use pollUntilEqual for Linear test

Replace `tail -1` with pure Node.js implementation that reads backwards
from end of file in 4KB chunks. No external binary dependency — works
on any platform.

Fix flaky Linear integration test by using the existing pollUntilEqual
helper instead of an inline retry loop. Linear API has eventual
consistency; pollUntilEqual retries for up to 5s with 500ms intervals.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix: use tail -1 for readLastJsonlEntry, add real-data integration test

Replace over-engineered pure Node.js backward-reading implementation with
simple `tail -1` via execFile. The codebase already shells out to tmux,
git, and ps everywhere — tail is no different.

Add integration test that validates toClaudeProjectPath() and
readLastJsonlEntry() against real ~/.claude/projects/ data on disk.
No API key needed — just requires Claude to have been run once.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-18 03:48:19 +05:30
prateek 599710296d
fix: migrate to hash-based project isolation architecture
Complete migration to hash-based directory structure for project isolation. All bugbot issues resolved.
2026-02-18 00:19:55 +05:30
prateek eaea131af9
feat: seamless onboarding with enhanced documentation (#66)
* feat: implement seamless onboarding with enhanced documentation

- Add comprehensive README.md (18KB) with quick start, core concepts, and FAQ
- Add detailed SETUP.md (16.5KB) with prerequisites, integration guides, and troubleshooting
- Add examples/ directory with 5 ready-to-use config templates:
  - simple-github.yaml: Minimal GitHub setup
  - linear-team.yaml: Linear integration
  - multi-project.yaml: Multiple repos
  - auto-merge.yaml: Aggressive automation
  - codex-integration.yaml: Using Codex agent

- Add environment detection (git repo, remote, branch, auth status)
- Auto-fill prompts with smart defaults from detected environment
- Add prerequisite validation (git, tmux, gh CLI)
- Show actionable next steps and warnings
- Parse owner/repo from git remote automatically
- Detect LINEAR_API_KEY and SLACK_WEBHOOK_URL in environment
- Prompt for Linear team ID when Linear tracker selected

- Format all files with Prettier for consistency

Reduces onboarding time from 30+ minutes to ~5 minutes:
1. Install CLI: `npm install -g @composio/ao-cli`
2. Run init: `ao init` (auto-detects everything)
3. Spawn agent: `ao spawn my-project ISSUE-123`

Users no longer need to:
- Manually parse git remote URLs
- Look up current branch names
- Remember YAML syntax
- Search for Linear team IDs
- Debug missing prerequisites

-  pnpm build - All packages compile
-  pnpm typecheck - No TypeScript errors
-  pnpm lint - No new linting issues
-  pnpm format - All files formatted

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* docs: update installation instructions to reflect npm not yet published

Package is not published to npm yet, so users must build from source.
Updated README.md and SETUP.md to:
- Make 'build from source' the primary installation method
- Add note that npm publishing is coming soon
- Include pnpm as a prerequisite

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* feat: add ao init --auto --smart for zero-config setup

Implements intelligent config generation with project type detection.

## What's New

### ao init --auto
- Zero prompts - auto-generates config with smart defaults
- Detects: git repo, remote, branch, languages, frameworks, tools
- Generates project-specific agentRules based on detected tech stack

### Project Detection
- Languages: TypeScript, JavaScript, Python, Go, Rust
- Frameworks: React, Next.js, Vue, Express, FastAPI, Django, Flask
- Tools: pnpm workspaces, test frameworks
- Package managers: pnpm, yarn, npm

### Rule Templates
Created templates for:
- base.md - Universal best practices
- typescript.md - TS strict mode, ESM, type imports
- javascript.md - Modern ES6+ patterns
- react.md - Hooks, composition, best practices
- nextjs.md - App Router, Server Components
- python.md - Type hints, PEP 8
- go.md - Error handling, defer patterns
- pnpm-workspaces.md - Monorepo commands

### Example Output

```bash
ao init --auto

# Detects:
# ✓ TypeScript + pnpm workspaces
# ✓ React + Next.js
# ✓ Vitest

# Generates:
agentRules: |
  Always run tests before pushing.
  Use TypeScript strict mode.
  Use ESM modules with .js extensions.
  Use React best practices (hooks, composition).
  Before pushing: pnpm build && pnpm typecheck && pnpm lint && pnpm test
```

## Benefits

- **5 seconds** instead of 5 minutes
- **Zero config knowledge** required
- **Context-aware rules** tailored to your stack
- **Still customizable** - edit the generated config

## Future: --smart (AI-powered)

Flag added but not yet implemented. Will use Claude Code to:
- Analyze CLAUDE.md, CONTRIBUTING.md
- Read CI/CD config
- Generate custom rules based on project patterns

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: detect repo default branch instead of current branch

Fixes Bugbot issue: "Current branch wrongly suggested as default base branch"

## Problem

detectEnvironment was using `git branch --show-current` to suggest
defaultBranch in the config. If a user ran `ao init` while on a feature
branch like `feat/my-work`, the wizard would suggest that feature branch
as the default, causing agents to branch from the wrong base.

## Solution

Added detectDefaultBranch() function with 3 fallback methods:
1. git symbolic-ref refs/remotes/origin/HEAD (most reliable)
2. GitHub API via gh CLI (if ownerRepo known)
3. Check common branch names: main, master, next, develop

Now EnvironmentInfo tracks both:
- currentBranch: The checked-out branch (for display only)
- defaultBranch: The repo's base branch (for config)

## Testing

Tested on feat/seamless-onboarding branch:
- Current branch: feat/seamless-onboarding (displayed)
- Default branch: main (correctly detected for config)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: prevent duplicate framework detection in Python projects

Fixes Bugbot issue: "Duplicate frameworks when multiple Python config files exist"

## Problem

When both requirements.txt and pyproject.toml exist and mention the same
framework (e.g., FastAPI), the detection loop added it to the frameworks
array twice, causing duplicate rules in the generated config.

## Solution

Added addFramework() helper that checks if framework already exists before
adding to the array. Also prevents pytest from being set multiple times as
testFramework.

## Testing

Verified with test repo containing both files with FastAPI:
- Before: Would add 'fastapi' twice
- After: Only adds 'fastapi' once ✓

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: address Bugbot review comments

- Remove redundant conditional in --smart flag (both branches were identical)
- Include templates directory in npm package files

* fix: add existence check for base.md template file

Add existsSync guard before reading base.md to handle missing templates gracefully, consistent with other template file reads.

* fix: use direct tool invocation instead of which command

Replace 'which' with direct tool invocation (tmux -V, gh --version)
for better portability on minimal Linux systems where 'which' may
not be installed.

* fix: address Bugbot review comments

- Simplify gh auth status check to rely on exit code instead of output string
- Remove async from synchronous functions (detectProjectType, generateRulesFromTemplates)

* feat: add setup script for one-command installation

Add scripts/setup.sh that:
- Installs pnpm if not present
- Installs dependencies
- Builds all packages
- Links CLI globally

Updated README with simplified setup instructions using the script.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: correct npm link command in setup script

Remove incorrect -g flag from npm link command. The correct syntax is to cd into the package directory and run npm link without flags.

* fix: address Bugbot review comments on init command

- Validate --smart flag requires --auto (prevents silent ignore)
- Fix path validation to check user-specified path (not CWD)

These fixes address medium and low severity issues found by Cursor Bugbot
in PR #66 review.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* docs: add DirectTerminal troubleshooting and fix setup script

- Add TROUBLESHOOTING.md documenting node-pty posix_spawnp error
- Update setup.sh to rebuild node-pty from source (fixes DirectTerminal)
- Ensures seamless onboarding with working terminal out-of-the-box

Resolves DirectTerminal WebSocket failures from incompatible prebuilt binaries.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: resolve variable scope issue in init command validation

- Move path variable outside if block to fix TypeScript scope error
- Only validate path existence if projectId is provided
- Use inline tilde expansion instead of missing expandHome import

Fixes build error that prevented setup.sh from completing.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: automate node-pty rebuild to eliminate terminal issues

- Add postinstall hook to automatically rebuild node-pty after pnpm install
- Create scripts/rebuild-node-pty.js for automatic rebuild with error handling
- Remove manual node-pty rebuild from setup.sh (now automatic)

This ensures DirectTerminal works correctly on every installation without
manual intervention. Fixes posix_spawnp errors from incompatible prebuilt
binaries across different systems and installations.

Resolves issue where users would encounter blank terminals after setup.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* docs: update TROUBLESHOOTING with automatic node-pty rebuild

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* docs: add comprehensive README with quick start guide

- 3-line magical setup: clone → setup → init → start
- Architecture overview with plugin slots table
- Usage examples and auto-reaction configuration
- Links to detailed docs (SETUP.md, TROUBLESHOOTING.md, examples/)
- Philosophy: push not pull, amplify judgment

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: resolve ESLint errors in rebuild-node-pty script

- Add scripts directory configuration to eslint.config.js
- Configure Node.js globals (console, process) for scripts
- Remove unused error variable from catch block

Fixes lint CI failure.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: warn when auto mode uses placeholder repo value

- Detect when 'owner/repo' placeholder is used in --auto mode
- Show warning: 'Could not detect GitHub repository'
- Update next steps to emphasize editing config when placeholder used
- Prevents silent failures when spawning agents with invalid repo

Addresses Bugbot review comment about silent placeholder values.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-16 22:22:13 +05:30
prateek 79aac7cf1d
feat: wire up live activity detection for agent sessions (#45)
* feat: wire up live activity detection for agent sessions

Previously, all sessions showed as "idle" on the dashboard because
activity detection was never called. The Agent plugin interface has
detectActivity(terminalOutput) and Runtime has getOutput(), but the
session manager's list() and get() methods never invoked them.

Changes:
- Updated list() to call runtime.getOutput() and agent.detectActivity()
  after checking if runtime is alive
- Updated get() with the same activity detection logic
- Captures last 30 lines of terminal output for classification
- Falls back to "idle" if output capture fails (graceful degradation)
- Agent plugins classify output into: active, idle, waiting_input,
  blocked, or exited

Testing:
- Added test for activity detection in list() with mocked output
- Added test for activity detection in get() with mocked output
- Added test for graceful fallback when getOutput() fails
- Fixed detectActivity mock (sync not async)
- All 24 tests pass

The dashboard now accurately shows whether agents are actively working,
idle at prompt, waiting for user input, blocked, or exited.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* refactor: extract enrichSessionWithRuntimeState helper

Addresses review comment about duplicated activity detection logic
between list() and get() methods.

Changes:
- Extract activity detection into enrichSessionWithRuntimeState helper
- Both list() and get() now call this shared helper
- Eliminates duplication and makes future changes easier
- All tests still pass (24/24)

Benefits:
- Single source of truth for runtime state enrichment
- Future changes (caching, param tweaks) only need one update
- More maintainable and less error-prone

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* refactor: replace terminal parsing with agent-native activity detection

Replaces hacky terminal output parsing (detectActivity) with deterministic
agent-native state tracking (getActivityState). Each agent now uses its own
internal mechanisms (JSONL files, SQLite, etc.) for reliable activity detection.

## Changes

### Core (`packages/core/src/types.ts`)
- Add `getActivityState(session)` method to Agent interface
- Deprecate `detectActivity(terminalOutput)` with @deprecated tag

### Session Manager (`packages/core/src/session-manager.ts`)
- Update `enrichSessionWithRuntimeState()` to call `agent.getActivityState()`
- Remove terminal output capture and parsing
- Cleaner, more maintainable code

### Claude Code Plugin (`packages/plugins/agent-claude-code/`)
- Implement `getActivityState()` using existing JSONL infrastructure
- Read last JSONL entry and classify by event type:
  - user/tool_use → active
  - assistant/system → idle
  - permission_request → waiting_input
  - error → blocked
  - stale (>30s) → idle
- Export `toClaudeProjectPath()` for testing
- Add unit tests for path encoding

### Other Agent Plugins (Codex, Aider, OpenCode)
- Add stub `getActivityState()` implementations
- Fall back to process running check
- TODO comments for full implementation using:
  - Codex: JSONL rollout files
  - Aider: Chat history mtime
  - OpenCode: SQLite database

### Tests (`packages/core/src/__tests__/session-manager.test.ts`)
- Update tests to expect `getActivityState()` calls
- Remove tests for deprecated terminal parsing
- All 24 tests pass 

## Benefits

 **Deterministic** - File-based, not terminal text parsing
 **Fast** - Single file read, no regex
 **Reliable** - Agent-provided state
 **Testable** - Mock files easily
 **Maintainable** - Pin versions, test format changes

## Migration Path

- `detectActivity()` is deprecated but not removed (backwards compat)
- Claude Code uses new method immediately
- Other agents use fallback until fully implemented
- Future PR will remove deprecated method

## Research

Agent-native mechanisms documented in:
- OpenAI Codex: JSONL rollout files at ~/.codex/sessions/
- Aider: Chat history at .aider.chat.history.md
- OpenCode: SQLite at ~/.local/share/opencode/opencode.db

See /tmp/activity-detection-redesign.md for full design.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* feat: implement native activity detection for all agent plugins

Implements full getActivityState() for Codex, Aider, and OpenCode:

- **Codex**: Checks JSONL rollout files at ~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl
  Maps event types (user_message, tool_use, approval_request, error) to activity states

- **Aider**: Checks chat history file mtime at .aider.chat.history.md and recent git commits
  Detects activity based on file modification and auto-commit behavior

- **OpenCode**: Checks SQLite database mtime at ~/.local/share/opencode/opencode.db
  Considers active if database was modified within 30 seconds

All implementations follow the deterministic approach established for Claude Code,
avoiding hacky terminal output parsing in favor of agent-native mechanisms.

Also fixes test mocks to include new getActivityState() method.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: address bugbot review comments for activity detection

Fixes three issues identified by Cursor Bugbot:

1. **Claude Code returns "exited" when runtime is alive** (Medium)
   - Added process-alive check at start of getActivityState()
   - Changed fallback returns from "exited" to "active" when process is running
   - Now matches pattern used by other agents (Codex, Aider, OpenCode)

2. **Claude Code missing process-alive check** (Medium)
   - Added isProcessRunning() check before file-based detection
   - Prevents reporting stale file-based activity when process has exited
   - Ensures "exited" is only returned when process is actually dead

3. **Codex reads entire JSONL file into memory** (Medium)
   - Replaced readFile() with seeked file handle read
   - Now reads only last 4KB (TAIL_READ_BYTES) instead of entire file
   - Matches efficient approach used by Claude Code plugin
   - Prevents memory/latency issues on long-running sessions

All tests passing (85 for Claude Code, 28 for Codex).

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: address three additional bugbot review comments

Fixes three issues identified in the second Cursor Bugbot scan:

1. **Catch block doesn't set activity to idle** (Low)
   - Added explicit `session.activity = "idle"` in catch block
   - Previously relied on implicit default from metadataToSession
   - Now explicitly documents and enforces the fallback behavior

2. **Codex rollout file matching uses wrong session ID** (Medium)
   - Removed session ID filtering from findLatestRolloutFile()
   - Codex uses internal UUIDs unrelated to orchestrator session IDs
   - Now finds most recent rollout file across all dates
   - Fixes non-functional activity detection for Codex

3. **Duplicate readLastJsonlEntry across packages** (Low)
   - Extracted shared JSONL tail-reading logic to @composio/ao-core/utils
   - Removed duplicate implementations from agent-claude-code and agent-codex
   - Both plugins now import readLastJsonlEntry and TAIL_READ_BYTES from core
   - Net reduction of 21 lines while improving maintainability

All 240 tests passing (127 core + 85 Claude Code + 28 Codex).

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: remove unused TAIL_READ_BYTES import from claude-code plugin

* fix: address third round of bugbot comments - document agent limitations

Fixes three issues from the latest Cursor Bugbot scan:

1. **Codex reads global state not per-session** (High Severity)
   - Removed file-based activity detection from Codex plugin
   - Codex stores rollout files globally without workspace scoping
   - When multiple sessions run, cannot reliably match files to sessions
   - Now falls back to process-running check only (conservative)
   - Documented limitation with TODO for when Codex adds per-workspace support

2. **OpenCode uses global shared database** (Medium Severity)
   - Removed database mtime checking from OpenCode plugin
   - OpenCode uses single global SQLite database for all sessions
   - Multiple sessions cause activity state bleed
   - Now falls back to process-running check only (conservative)
   - Documented limitation with TODO for when OpenCode adds per-workspace support

3. **Unused TAIL_READ_BYTES export** (Low Severity)
   - Removed TAIL_READ_BYTES from core package exports
   - Only used internally by readLastJsonlEntry in utils.ts
   - Reduces unnecessary public API surface

Net reduction: 108 lines of code that couldn't work correctly with multiple sessions.
All tests passing (28 Codex + 27 OpenCode).

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* test: rewrite agent integration tests to use getActivityState() instead of detectActivity()

Integration tests were using the deprecated detectActivity() method, which
meant they weren't actually testing the new getActivityState() functionality
that session-manager uses in production.

Changes:
- All agent integration tests now call getActivityState() instead of detectActivity()
- Tests verify getActivityState() returns valid states while running
- Tests verify getActivityState() returns "exited" after process terminates
- Grouped multiple assertions per test run for efficiency
- Updated comments to reflect new behavior (Codex/OpenCode conservative fallback)
- Allow getSessionInfo() to return null for path encoding mismatches

All integration tests pass:
- agent-claude-code: 6 passed (full JSONL-based activity detection)
- agent-codex: 5 passed (conservative fallback)
- agent-opencode: 5 passed (conservative fallback)
- agent-aider: updated but not tested (aider binary not available in CI)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: remove unused imports from integration tests

Fixed ESLint errors:
- Removed unused 'capturePane' imports from all agent integration tests
- Removed unused 'err' variable from catch block

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: return active instead of idle when JSONL entry is null

When readLastJsonlEntry returns null (empty file or read error), getActivityState
now returns "active" instead of "idle" as a conservative fallback. This makes it
consistent with:
- Other fallback cases in the same function (no workspace path, no session file)
- Other agent plugins (Codex, OpenCode, Aider) which return "active" on detection failure
- The stated intent in PR discussion to assume active when process is running

Fixes bugbot comment: https://github.com/ComposioHQ/agent-orchestrator/pull/45#discussion_r2810110669

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: make TAIL_READ_BYTES internal constant instead of exported

TAIL_READ_BYTES is only used internally within readLastJsonlEntry()
and is not exported from the package's index.ts. Removed the export
keyword to make it clear it's an internal implementation detail.

Addresses bugbot comment about unused export.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-16 05:31:02 +05:30
prateek de662dc042
fix: recognize terminated/done session states and hide terminal for dead sessions (#40)
* fix: recognize terminated/done session states and hide terminal for dead sessions

- Add "done" and "terminated" to VALID_STATUSES in session-manager so
  validateStatus() doesn't fall back to "spawning" for these states
- Hide terminal button for terminal-state sessions (no tmux to connect to)
- Hide "terminate session" button for already-terminated sessions
- Show "restore session" button for terminated/done sessions

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: restore activity=exited check for crashed sessions

Bugbot caught that the refactor dropped the activity === "exited"
condition. When an agent crashes, status stays non-terminal (e.g.
"working") but activity becomes "exited" — these need the restore
button and should not show terminal/terminate buttons.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: add terminated/done to backend RESTORABLE_STATUSES

Frontend shows restore button for terminated/done sessions but
the backend restore endpoint only accepted killed/cleanup, returning
409 "Session is not in a terminal state" for the new statuses.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: add type annotations to fix implicit any errors in integration tests

Pre-existing issue from package rename — callback parameters in
.find() lost type inference. Add explicit type annotations.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: filter orchestrator session from SSR page

The API route filtered it but the SSR path in page.tsx did not,
causing the orchestrator to appear as a session card.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: make orchestrator session name dynamic using prefix convention

Use endsWith("-orchestrator") instead of hardcoded "orchestrator" to
support project-prefixed names like "ao-orchestrator". Pass orchestratorId
from SSR to Dashboard so the terminal button links to the correct session.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: update stale @agent-orchestrator/core imports to @composio/ao-core

Package was renamed in PR #32 but these two files were missed.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-15 05:15:41 +05:30
prateek 21335db8af
feat: publish to npm under @composio scope (#32)
* feat: add npm publishing support with @composio scope

Set up Changesets for version management, add publish metadata to all 20
packages under the @composio scope, create an unscoped wrapper package
(@composio/agent-orchestrator) for global install, and add a GitHub
Actions release workflow.

- Rename all packages from @agent-orchestrator/* to @composio/ao-*
- Add @composio/agent-orchestrator wrapper (bin shim → @composio/ao-cli)
- Add license, repository, homepage, bugs, files, engines to all packages
- Add .npmrc (access=public), MIT LICENSE file
- Add .changeset/ config with linked versioning for all packages
- Add .github/workflows/release.yml (changesets publish CI)
- Add changeset, version-packages, release scripts to root

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* fix: exclude private web package from release build

The release script now filters out @composio/ao-web, matching the
workflow's existing exclusion and preventing a Next.js build failure
from blocking npm publishing.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-15 04:28:57 +05:30