Commit Graph

4 Commits

Author SHA1 Message Date
prateek 8e5b23e6a0
feat: auto-update session metadata via Claude Code hooks (#34)
* feat: auto-update session metadata via Claude Code hooks

Implements INT-1355: Auto-update session metadata when agent creates PR or switches branch.

## What Changed

- **agent-claude-code plugin**: Added `postLaunchSetup` method that writes a Claude Code hook to `.claude/settings.json` and `.claude/metadata-updater.sh` in each workspace
- **session-manager**: Added `AO_DATA_DIR` environment variable to agent sessions
- **Hook script**: Monitors Bash commands and automatically updates metadata on:
  - `gh pr create` → extracts PR URL, sets `pr=<url>` and `status=pr_open`
  - `git checkout -b` / `git switch -c` → extracts branch name, sets `branch=<name>`
  - `git checkout` / `git switch` (existing branches) → updates branch name for feature branches
  - `gh pr merge` → sets `status=merged`

## How It Works

1. When a session is spawned, `postLaunchSetup` runs after the agent launches
2. Creates `.claude/settings.json` with a PostToolUse hook for Bash commands
3. Writes the metadata updater script to `.claude/metadata-updater.sh`
4. The hook fires after every Bash command execution
5. Parses command and output to detect git/gh operations
6. Updates the session metadata file directly using atomic file operations

## Benefits

- Dashboard shows correct PR associations immediately without manual intervention
- Branch tracking stays in sync with agent actions
- Orchestrator has accurate session state for automation and notifications

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

* fix: correct hook payload field name and JSON parsing

Fixes bugbot issues from PR #34:

1. Changed tool_output to tool_response - Claude Code hooks provide
   output in the tool_response field, not tool_output. This fixes PR
   URL extraction from gh pr create commands.

2. Replaced over-escaped sed patterns with cut - The fallback JSON
   parser now uses 'cut -d\" -f4' instead of sed with capture groups,
   which is simpler and avoids escaping issues.

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

* fix: escape special characters in metadata values

Fixes bugbot issue: metadata updates break on special branch names.

The update_metadata_key function now escapes sed special characters
(& | / \) in values before using them in sed replacement. This prevents
branch names or PR URLs containing these characters from breaking the
metadata update.

Example: branch name "feature/foo&bar" now correctly updates metadata
instead of causing sed to interpret "&" as a backreference.

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

* fix: only update metadata on successful commands

Fixes bugbot issue: metadata updates ignore command failure.

The hook now checks the exit_code field from the hook payload and only
updates metadata if the command succeeded (exit code 0). This prevents
updating metadata when git/gh commands fail.

Example: if 'gh pr create' fails due to auth issues, the metadata won't
be incorrectly updated with a pr= line.

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

* fix: use correct timeout units for Claude Code hook

Fixes bugbot issue: hook timeout uses wrong units (High Severity).

Claude Code hook timeouts are in milliseconds, not seconds. Changed
timeout from 5 to 5000 (5 seconds) to give the metadata updater script
enough time to parse tool output and update metadata.

Note: The hook command path is properly handled by JSON.stringify
and does not need additional escaping as Claude Code handles command
execution correctly.

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

* fix: PR URL regex and stale hook path issues

Fixes 2 bugbot issues:

1. PR URL extraction regex never matches (High Severity): Changed
   'github\.com' to 'github[.]com' because in single-quoted bash
   strings, backslashes are literal. The [.] syntax correctly matches
   a literal dot in grep regex.

2. Shared Claude settings keep stale hook path (Medium Severity):
   Changed hook detection to always update the command path to the
   current workspace, not just check for existence. This handles cases
   where .claude settings are shared/symlinked across workspaces.

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

---------

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-15 02:29:31 +05:30
prateek 06b5ec3267
feat: implement CLI with all commands (init, status, spawn, session, send, review-check, dashboard, open) (#6)
* feat: implement CLI with all commands (init, status, spawn, session, send, review-check, dashboard, open)

Implement the full `ao` CLI using Commander.js with 9 commands:

- ao init: Interactive setup wizard that creates agent-orchestrator.yaml
- ao status: Colored terminal table of all sessions (branch, activity, PR, CI)
- ao spawn <project> [issue]: Spawn single agent session (worktree + tmux + agent)
- ao batch-spawn <project> <issues...>: Batch spawn with duplicate detection
- ao session ls|kill|cleanup: Session management subcommands
- ao send <session> <message>: Smart message delivery with busy detection/retry
- ao review-check [project]: Scan PRs for review comments, trigger agent fixes
- ao dashboard: Start the Next.js web dashboard
- ao open [target]: Open session(s) in terminal tabs

Shared libraries:
- lib/shell.ts: Shell command helpers (tmux, git, gh wrappers)
- lib/metadata.ts: Flat metadata file read/write (backwards-compatible format)
- lib/format.ts: Terminal formatting (colors, tables, banners)

All commands code against core interfaces and flat metadata files,
matching the behavior of the reference bash scripts in scripts/.

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

* fix: address PR review — extract shared helpers, fix bugs, add tests

- Extract getTmuxSessions/getTmuxActivity to lib/shell.ts (was duplicated in 5 files)
- Fix readMetadata unsafe cast: return Partial<SessionMetadata> instead
- Fix dashboard webDir path: resolve from package root, not dist/
- Fix dashboard spawn error handling: add child.on("error")
- Fix getNextSessionNumber: escape regex metacharacters in prefix
- Fix fallback worktree creation: use existing branch name, not detached HEAD
- Fix post-create hooks: run before agent launch, not after
- Fix open --new-window: pass option through to openInTerminal
- Fix cleanup dry-run: separate summary message for dry-run mode
- Add Claude session introspection to status command (TTY→PID→CWD→JSONL)
- Add vitest test suite: 72 tests covering commands and lib utilities

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

* fix: address all PR review issues, add lint/format compliance

Review fixes:
- Extract getTmuxSessions/getTmuxActivity to lib/shell.ts (was duped in 5 files)
- Fix readMetadata: return Partial<SessionMetadata> instead of unsafe cast
- Fix dashboard webDir: use createRequire + __dirname for ESM compat
- Fix dashboard spawn: add child.on("error") handler
- Fix getNextSessionNumber: escape regex metacharacters in prefix
- Fix fallback worktree: use existing branch name, not detached HEAD
- Fix post-create hooks: run before agent launch, not after
- Fix open --new-window: pass option through to openInTerminal
- Fix cleanup dry-run: separate summary message for dry-run mode
- Add Claude session introspection to status (TTY→PID→CWD→JSONL)
- Add port validation in init wizard
- Add clarifying comment for tmux C-u send-keys

Lint/format:
- Add no-console override for CLI package in eslint config
- Fix duplicate imports (node:fs, @agent-orchestrator/core)
- Fix unused variable in send test
- Apply prettier formatting across all files

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

* fix: address codex review — fd leak, input validation, security hardening

- Wrap openSync/readSync in try/finally to prevent fd leak on error
- Add timeout NaN/<=0 guard in send command
- Add port validation (1-65535) in dashboard command
- Sanitize issueId for git-ref-safe branch names
- Add --project validation in session ls/cleanup
- Fix batch-spawn same-run duplicate detection
- Replace shell interpolation with safe ps + JS filtering
- Wrap temp file usage in try/finally for cleanup on error
- Read only tail of JSONL files to avoid large file loads

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

* fix: address remaining PR review comments

- Use `=== null` instead of falsy check for git worktree result (spawn.ts)
- Wrap spinner in try/catch so it stops on error (spawn.ts)
- Use exact match instead of substring for issue duplicate detection (metadata.ts)
- Check git worktree remove result before printing success (session.ts)
- Skip banner/headers/footer when --json is passed (status.ts)
- Add error handler on browser-open spawn (dashboard.ts)

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

* fix: address bugbot review — interrupt before send, error resilience, worktree validation

- review-check: send C-c + C-u to interrupt busy agent and clear input before
  delivering fix prompt, matching the reference bash script behavior
- review-check: wrap per-session send in try/catch so one failure doesn't abort
  remaining sessions
- spawn: check worktree creation return value and throw on failure instead of
  continuing with non-existent worktree path
- Update test mock for detached worktree to return success

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

* fix: address 4 bugbot findings — status type, TTY match, retry safety, comment counting

- spawn: write status "spawning" (not "starting") to match SessionStatus type
- status: use column-based exact TTY match to prevent pts/1 matching pts/10
- send: use null-safe tmux() helper in retry loop instead of throwing exec()
- review-check: use GraphQL reviewThreads to count only unresolved comments,
  preventing fix prompts on PRs with all threads resolved

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

* fix: GraphQL variable passing and env var name sanitization

- review-check: use -F flags for GraphQL variables instead of string
  interpolation to prevent injection via repo config values
- spawn: sanitize env var name by replacing non-alphanumeric chars with
  underscores (my-app → MY_APP_SESSION, not MY-APP_SESSION)

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

* fix: add -l flag to tmux send-keys and remove dead activityIndicator code

- Add literal (-l) flag to send-keys so user text isn't interpreted as
  tmux key names (e.g. "Enter", "Escape")
- Remove unused activityIndicator function from format.ts and its tests

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

* refactor: move agent-specific logic from CLI into agent plugins

detectActivity, introspect, and getLaunchCommand now live in the agent
plugins (claude-code, codex, aider) instead of being hardcoded in the
CLI commands. The Agent interface's detectActivity takes a plain string
of terminal output instead of a Session object, making plugins trivially
unit-testable.

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

* fix: use -f for string GraphQL vars and -l flag for tmux send-keys

Addresses two bugbot findings in review-check.ts:
1. Use -f (string) instead of -F (typed) for owner/name GraphQL
   variables to prevent numeric coercion of repo names.
2. Use -l (literal) flag with send-keys and separate Enter call,
   matching the established tmux safety pattern in send.ts.

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

* fix: guard empty terminal output in lifecycle-manager and deduplicate send.ts

- lifecycle-manager: skip detectActivity when terminal output is empty
  to prevent false "killed" state when runtime probe returns no data
- send.ts: collapse identical isBusy/isProcessing into single isActive
- tests: use non-empty mock terminal output so detectActivity is exercised

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

* fix: validate message before side effects, remove dead updateMetadataField

- send.ts: move message validation before idle-wait loop and C-u clear
  so running `ao send session` with no message exits immediately without
  touching the tmux session
- metadata.ts: remove exported updateMetadataField (unused in production)
- metadata.test.ts: remove corresponding tests

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

* fix: add -l flag and separate Enter for spawn prompt send-keys

Use literal flag (-l) when sending the initial prompt via tmux
send-keys in spawn.ts, and send Enter as a separate call. This
matches the pattern used in send.ts and review-check.ts and prevents
tmux from interpreting special characters in the issue ID.

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

* fix: strict session prefix matching and validate project before use

- session-utils: add matchesPrefix() using regex ^prefix-\d+$ to prevent
  cross-project misattribution with overlapping prefixes (e.g. "app" vs
  "app-v2")
- Replace all startsWith(`${prefix}-`) calls across status.ts,
  review-check.ts, session.ts, and open.ts with matchesPrefix()
- status.ts, review-check.ts: move project validation before the
  projects map construction so invalid project IDs exit before any
  access to undefined config

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

* fix: deduplicate escapeRegex and add -l flag to spawn send-keys

- Export escapeRegex from session-utils.ts, remove duplicate from spawn.ts
- Add -l (literal) flag to tmux send-keys for postCreate hooks and
  launch command, with Enter sent separately
- Prevents tmux from interpreting key names in user config or agent
  commands

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

* fix: update detectActivity to sync string signature across all plugins

After rebase on main, all agent plugins and their tests still had the
old async detectActivity(session: Session) signature. Updated to the
new sync detectActivity(terminalOutput: string): ActivityState across:
- agent-claude-code, agent-codex, agent-aider, agent-opencode plugins
- All plugin unit tests (test terminal output patterns, not process state)
- All integration tests (capture tmux pane output before calling)
- status.ts: use getSessionInfo() instead of nonexistent introspect()

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

* fix: integration tests expect idle (not exited) from detectActivity after process exit

detectActivity is a pure terminal-text classifier that returns "idle" for
empty/shell-prompt output. Process exit detection is handled by isProcessRunning.

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

* fix: check last-line prompt before historical activity patterns in detectActivity

Reorder classifyTerminalOutput to check the last line for a prompt
character before scanning the full buffer for activity indicators like
"Reading" or "Thinking". This prevents historical output in the capture
buffer from causing false "active" results when the agent is idle.

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

* fix: update pnpm-lock.yaml for web dashboard dependencies

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

* fix: preserve state on getOutput failure, check permission prompts before activity indicators

- lifecycle-manager: remove .catch(() => "") from getOutput so failures
  reach the outer catch block that preserves stuck/needs_input states
- classifyTerminalOutput: check waiting_input prompts against bottom of
  buffer before full-buffer active indicators, preventing historical
  "Thinking"/"Reading" text from overriding current permission prompts

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

* fix: detect agent exit via isProcessRunning, remove redundant active checks

- lifecycle-manager: replace dead "exited"/"blocked" branches with
  isProcessRunning check when detectActivity returns "idle" — correctly
  detects agent process exit inside a still-alive tmux session
- classifyTerminalOutput: remove redundant explicit active-indicator
  checks that returned the same "active" as the unconditional default

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

* fix: wrap killSession in try-catch so cleanup loop continues on failure

One session's kill failure (e.g. archiveMetadata fs error) no longer
aborts the entire cleanup loop, matching the pattern used by batch-spawn
and review-check.

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

* test: add comprehensive unit and integration tests for CLI

Unit tests (6 new files, 61 tests):
- session-utils: escapeRegex, matchesPrefix, findProjectForSession
- plugins: getAgent, getAgentByName lookup and error cases
- shell: exec, execSilent, tmux, git, gh, getTmuxSessions, getTmuxActivity
- review-check: GraphQL review checking, dry-run, fix prompt delivery
- open: target resolution (all/project/session), fallback, --new-window
- init: rejects existing config file

Integration tests (2 new files, 8 tests):
- spawn-send-kill: real tmux session create, send-keys, kill, graceful re-kill
- session-ls: multi-session listing, per-session capture, activity timestamps

CLI vitest config updated with explicit thread pool parallelization.
Total CLI tests: 68 → 129. Total integration tests: 20 → 28.

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

* fix: address 5 bugbot findings — symlink type, timeout cleanup, file validation, repo format

- spawn.ts: pass `type` param to symlinkSync based on lstat (dir vs file)
- spawn.ts: document TOCTOU gap in getNextSessionNumber (tmux rejects dupes)
- dashboard.ts: store browser timeout and clear it on child exit
- send.ts: wrap file read in try-catch with user-friendly error
- review-check.ts: validate owner/name split before GraphQL query

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 16:14:27 +05:30
prateek 4b6d62d142
feat: agent plugins, OpenCode plugin, integration tests, CI (#5)
* feat: implement agent plugins (claude-code, codex, aider)

Implement the Agent interface from @agent-orchestrator/core for three
AI coding tools, enabling the orchestrator to launch, detect activity,
and introspect agent sessions.

Claude Code plugin (full implementation):
- JSONL session introspection (summary, session ID, cost, last message type)
- Activity detection via terminal output pattern matching
- Process detection via tmux pane TTY → ps lookup chain
- Launch command generation with permissions/model/prompt support

Codex and Aider plugins (functional stubs):
- Launch command generation with tool-specific flags
- Process detection via TTY lookup
- Stub introspection (no JSONL support yet)

Closes INT-1329

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

* fix: address PR review — shell escaping, activity detection, tests

- Replace JSON.stringify with POSIX shell escaping (single quotes) for
  all agent launch commands to prevent command injection
- Remove `unset CLAUDECODE &&` shell syntax from getLaunchCommand (breaks
  execFile); moved to getEnvironment as empty string
- Fix detectActivity returning "exited" for empty pane when process is
  confirmed alive — now returns "idle"
- Tighten BLOCKED_PATTERNS to specific error framing (^Error:, ENOENT:,
  APIError:, etc.) to avoid false positives on code output
- Check blocked patterns before input patterns to prevent "EACCES:
  permission denied" matching INPUT_PATTERNS on the word "permission"
- Remove pgrep fallback in findClaudeProcess (could match wrong session)
- Fix extractCost double-counting: prefer costUSD, fall back to
  estimatedCostUsd only when costUSD is absent
- Trim ACTIVE_PATTERNS to stable indicators only
- Add comprehensive vitest tests (127 tests across 3 plugins)

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

* fix: address Codex review + lint fixes

- Use `ps -eo pid,tty,args` instead of `comm` for process detection so
  CLI wrappers (node, python) are correctly identified
- Coerce PID from handle.data with Number() + isFinite() to handle
  string-serialized PIDs
- Fix token double-counting: prefer `usage` object, only fall back to
  flat `inputTokens`/`outputTokens` when `usage` is absent
- Add `--` before Codex prompt to prevent prompts starting with `-`
  from being parsed as CLI flags
- Check blocked patterns before input patterns so "EACCES: permission
  denied" isn't misclassified as waiting_input
- Fix lint: merge duplicate imports, strict equality, remove non-null
  assertions
- Format with prettier

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

* fix: multi-pane tmux, EPERM handling, strict process matching

Codex review iteration 1 fixes:
- Iterate all tmux pane TTYs instead of only the first — prevents
  false "exited" in multi-pane sessions
- Handle EPERM from process.kill(pid, 0) as "process exists" — EPERM
  means the process is alive but we lack permission to signal it
- Use word-boundary regex for process name matching — prevents false
  positives on similar names like "claude-code" or paths containing
  the substring
- Add tests for EPERM handling, multi-pane detection, and strict
  name matching (134 tests total)

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

* fix: exclude next-env.d.ts from eslint

Auto-generated Next.js type file triggers triple-slash-reference rule.

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

* fix: address Bugbot review — JSON.parse guard, detectActivity pattern priority

- Guard parseJsonlFile against non-object JSON values (null, arrays,
  primitives) that would cause TypeError in downstream extractors
- Split null output (non-tmux → "active") from empty output (tmux → "idle")
  so non-tmux runtimes don't falsely report idle
- Reorder pattern matching: idle before blocked so stale errors in the
  tmux scrollback don't mask an idle prompt

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

* refactor: extract shellEscape to @agent-orchestrator/core

Move the duplicated POSIX shell-escaping utility from all three agent
plugins into packages/core/src/utils.ts and re-export from the package
entry point. Plugins now import { shellEscape } from the shared package.

Addresses Bugbot review comment on PR #5.

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

* fix: add execFileAsync timeouts, use satisfies for plugin exports

- Add { timeout: 30_000 } to all execFileAsync calls (tmux, ps) per
  CLAUDE.md convention to prevent hanging on stuck processes
- Replace `const plugin: PluginModule<Agent>` with inline
  `export default { ... } satisfies PluginModule<Agent>` to preserve
  narrow literal types on manifest fields

Addresses Bugbot review comments on PR #5.

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

* refactor: rename introspect → getSessionInfo, detectActivity uses JSONL

- Rename Agent.introspect() → getSessionInfo(), AgentIntrospection → AgentSessionInfo
- Claude Code detectActivity: replace tmux screen-scraping with JSONL-based
  detection (file mtime + last entry type)
- Remove terminal pattern constants (ACTIVE_PATTERNS, IDLE_PATTERNS, etc.)
- Update all tests: JSONL-based activity tests, renamed introspect blocks
- waiting_input/blocked states deferred to hooks-based implementation (#16)

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

* feat: add OpenCode agent plugin, integration tests, and CI workflow

- Add agent-opencode plugin with getLaunchCommand, detectActivity,
  isProcessRunning, getSessionInfo (27 unit tests)
- Add integration test suite for all 4 agents (claude-code, codex,
  aider, opencode) using real binaries in tmux sessions
- Add GitHub Actions CI workflow for integration tests
- Add test:integration script to root package.json

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

* fix: update core test mocks — introspect → getSessionInfo + isProcessing

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

* fix: address Bugbot review — Windows paths, bytesRead, hardcoded paths

- toClaudeProjectPath: handle Windows backslashes and drive letters
- readLastJsonlEntry: use bytesRead to avoid null character corruption
- Remove duplicate next-env.d.ts eslint ignore entry
- Remove hardcoded developer-specific binary paths from integration tests
- Add isProcessing limitation comments with issue references (#17-19)

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 11:28:42 +05:30
Prateek 5058c409d5 feat: scaffold TypeScript monorepo with all plugin interfaces
Phase 0 complete. Establishes:
- pnpm workspace with 18 packages (core, cli, web, 15 plugins)
- Complete type definitions in packages/core/src/types.ts defining
  all 8 plugin slot interfaces (Runtime, Agent, Workspace, Tracker,
  SCM, Notifier, Terminal) + core service interfaces
- YAML config loader with Zod validation and sensible defaults
- Plugin registry with built-in discovery
- CLAUDE.md with conventions for spawned agents

All agents can now branch from main and implement their assigned
packages against the interfaces defined in types.ts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 17:02:42 +05:30