Commit Graph

24 Commits

Author SHA1 Message Date
Ashish Huddar 9ca3c1fcd7
fix: force launcher relink during update (#1594)
* fix: force launcher relink during update (#1591)

* fix: address launcher refresh review feedback (#1591)

* fix: improve launcher refresh diagnostics (#1591)
2026-05-01 17:09:58 +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
yyovil 7581af9a65
fix(cli): avoid missing repo scripts on global installs (#1252) (#1277)
* fix(cli): avoid missing repo scripts on global installs

* refactor(cli): moved ao-update.sh script into assets.

Entire-Checkpoint: b91ccadead1c

* Fix packaged doctor and update fallback

* Harden install detection and script runner

* fix(cli): align ao script launchers with packages/ao

---------

Co-authored-by: AO Bot <ao-bot@composio.dev>
2026-04-26 16:47:49 +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
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
Prateek 4f2616674f fix: address bugbot comments — missed renames in preflight, tests, and shell scripts 2026-04-09 16:00:31 +00:00
Denis Darii f92c5f6c9d Validate Linear branchName before spawn 2026-04-03 09:59:58 +02:00
Denis Darii 2c39076a52 Align claude-batch-spawn prompts: use AO-prepared branch, drop conflicting git hints
Made-with: Cursor
2026-04-02 22:17:29 +02:00
Ashish Huddar 4a842d4742 Fix ao launcher linking and mobile toast offset 2026-03-26 22:47:25 +05:30
suraj-markup 596913aa34 Fix review issues: busy-wait loops, lock race, import hygiene, typed errors
- Replace CPU-burning spin loops in running-state.ts with async setTimeout
  and jittered backoff; make all exports async
- Fix TOCTOU race in acquireLock by extracting tryAcquire helper with
  retry loop instead of force-remove-and-retry-once
- Hoist dynamic imports in addProjectToConfig/choice-2 to static imports
- Add try/finally around readline in detectAgentRuntime
- Validate session prefix uniqueness in "Start new orchestrator" menu
- Add ConfigNotFoundError class to @composio/ao-core, replace fragile
  string matching in ao start with instanceof check
- Fix setup.sh to recommend `ao start` instead of deprecated `ao init`
- Fix start-all.ts: resolve next binary with fallback, wait for children
  on SIGTERM instead of immediate process.exit

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 17:33:06 +05:30
suraj-markup 14fa726551 Publish ao-web on npm, harden error handling, deduplicate code
- Publish @composio/ao-web dashboard as npm package (removed private flag,
  added files field, production entry point, node-pty made optional)
- CLI auto-detects dev vs production mode for dashboard startup
- Use local next binary instead of npx in production start-all.ts
- findWebDir() throws with install-specific guidance instead of returning
  broken path
- Fix CI-silent failure: setup.sh and ao-update.sh exit 1 on non-interactive
  npm link failure
- Deduplicate detectDefaultBranch into shared cli/lib/git-utils.ts
- Add EACCES permission guidance to README.md and SETUP.md
- Move resolveProjectIdForSessionId to @composio/ao-core
- Update design doc with problems #8-13, changes #9-12, known limitations
  section, and expanded test plan

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 19:01:49 +05:30
suraj-markup 90fe0e09f8 Add contextual next-step hints with directory guidance
Every command now prints what to run next and from where:
- setup.sh → tells user to cd to project dir and run ao init
- ao init → tells user to run ao start (with directory context)
- ao add-project → tells user to run ao start and ao spawn
- ao start → tells user to run ao spawn <project> <issue>

Eliminates confusion about running commands in the wrong directory
(e.g., ao init inside agent-orchestrator instead of the project repo).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 22:07:58 +05:30
suraj-markup 672ee0e0c5 feat: reduce onboarding friction with npm link fix and ao add-project command
- Fix npm link EACCES permission error in setup.sh, ao-update.sh, and ao-doctor.sh
  by auto-retrying with sudo when interactive
- Add `ao add-project <path>` command that auto-detects git remote, default branch,
  project type, and generates agent rules — appends to existing config in one step

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 17:55:42 +05:30
Harsh Batheja 3d518aed2f
feat: add doctor and update maintenance tooling (#437)
* feat: add ao doctor maintenance checks

* feat: add ao update refresh script

* feat: add shared CLI script runner

* feat: add doctor and update CLI commands

* docs: document doctor and update commands

* fix: harden maintenance safety checks

* fix: harden ao update behavior

* fix: strip inline comments from doctor config values
2026-03-12 20:59:22 +05:30
suraj_markup 7aa883c217
Merge pull request #254 from suraj-markup/feat/try-pr-script
feat(scripts): add try-pr.sh for testing PR worktrees locally
2026-03-03 01:18:06 +05:30
suraj-markup 6603685a2f feat(scripts): add try-pr.sh for testing PR worktrees locally
Adds scripts/try-pr.sh — a helper to switch the global 'ao' command to
any session's worktree for manual testing, then restore back to main.

Usage:
  bash scripts/try-pr.sh <session-id>            # CLI/core/plugins only
  bash scripts/try-pr.sh <session-id> --with-web # also builds + starts dashboard
  bash scripts/try-pr.sh --restore               # switch back to main

Also fixes isPortAvailable() to use a connect-based probe instead of
bind-based. The old approach had false positives on macOS when Next.js
listens on :: (IPv6 wildcard) — binding 127.0.0.1 succeeded even though
the port was taken. A TCP connect correctly detects any listener
regardless of which address it's bound to.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-02 03:19:49 +05:30
suraj-markup 7128a9d469 fix: handle corepack permission errors in setup.sh
Skip pnpm install if already available. When it's not, try corepack
first and fall back to npm install -g pnpm if corepack fails (e.g.
non-root user in Docker can't write to /usr/local/bin).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 20:56:15 +05:30
suraj-markup 34cf42616f fix: skip interactive prompts in non-interactive environments
Guard `read` prompts behind `[ -t 0 ]` check so setup.sh works in
CI/Docker without hanging or failing on stdin. Soft warnings still
print but skip the interactive offer to fix.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 20:52:36 +05:30
suraj-markup e40275b53c fix(setup): improve setup.sh with prereq validation and add ao start/spawn pre-flight checks
- Rewrite setup.sh with hard stops for Node<20 and git<2.25, soft warns
  with interactive fix for tmux/gh-auth/claude, corepack-based pnpm
  install, and PATH verification after npm link
- Add pre-flight checks to `ao start`: verify dashboard port is free and
  packages are built before proceeding
- Add pre-flight checks to `ao spawn`: verify tmux is installed and gh
  is authenticated (when using github tracker) before session creation
- Extract shared preflight utilities into packages/cli/src/lib/preflight.ts

Closes #245

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 19:39:29 +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 5fe476774d
fix: clean Next.js build artifacts in setup script (#68)
Add cleanup step to remove packages/web/.next directory before building.
This prevents "Cannot find module" errors from stale webpack artifacts
that can occur after rebasing or switching branches.

The cleanup step ensures a clean build every time the setup script runs.

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-17 00:24:04 +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 1601ad75c0 feat: add agent-orchestrator (ao) as a self-hosting project
- Create claude-ao-session: worktree-based session manager for ao
  (Linear tickets, GitHub PRs, AO_SESSION env var)
- Add ao|agent-orchestrator case to all shared scripts:
  claude-batch-spawn, claude-spawn, claude-status, claude-open-all,
  claude-review-check, claude-bugbot-fix
- Rewrite CLAUDE.orchestrator.md with full self-hosting instructions:
  commands reference, workflows, architecture, tips
- Session prefix: ao, metadata: ~/.ao-sessions/, worktrees: ~/.worktrees/ao/

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-13 15:44:17 +05:30
Prateek 0273e8f3d0 feat: initial commit with orchestrator scripts and dev instructions
Import all 18 production orchestrator scripts from ~/. These are direct
copies with hardcoded paths — generalization is the next step. Includes
CLAUDE.local.md with project overview, architecture notes, and roadmap.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-13 15:25:31 +05:30