diff --git a/.beads/daemon-error b/.beads/daemon-error
new file mode 100644
index 00000000..5d7768f8
--- /dev/null
+++ b/.beads/daemon-error
@@ -0,0 +1,16 @@
+
+LEGACY DATABASE DETECTED!
+
+This database was created before version 0.17.5 and lacks a repository fingerprint.
+To continue using this database, you must explicitly set its repository ID:
+
+ bd migrate --update-repo-id
+
+This ensures the database is bound to this repository and prevents accidental
+database sharing between different repositories.
+
+If this is a fresh clone, run:
+ rm -rf .beads && bd init
+
+Note: Auto-claiming legacy databases is intentionally disabled to prevent
+silent corruption when databases are copied between repositories.
diff --git a/.claude/cache/agents/scout/latest-output.md b/.claude/cache/agents/scout/latest-output.md
new file mode 100644
index 00000000..6d8d1308
--- /dev/null
+++ b/.claude/cache/agents/scout/latest-output.md
@@ -0,0 +1,543 @@
+# Codebase Report: ralph-tui Sessions and Plans Architecture
+Generated: 2026-01-14 12:30 PST
+
+## Summary
+
+Ralph TUI is an **AI Agent Loop Orchestrator** that automates AI coding agents (Claude Code, OpenCode) to work through task lists autonomously. It manages **one session at a time** with a single plan/tracker, but supports **pausing/resuming** and **switching between different trackers or epics** across sessions. Sessions are persisted to `.ralph-tui/session.json` for crash recovery and resume functionality.
+
+**Key Finding:** Ralph operates with **one active session at a time**, but you can have **multiple plans** (PRDs, epics) and switch between them across sessions.
+
+---
+
+## Project Structure
+
+```
+ralph-tui/
+├── src/
+│ ├── cli.tsx # CLI entry point, command routing
+│ ├── commands/ # CLI commands
+│ │ ├── run.tsx # Start new session
+│ │ ├── resume.tsx # Resume paused session
+│ │ ├── create-prd.tsx # Create PRD with AI
+│ │ ├── convert.ts # Convert PRD to tasks
+│ │ └── status.ts # Check session status
+│ ├── session/ # Session management
+│ │ ├── persistence.ts # Session file operations
+│ │ ├── lock.ts # Process locking
+│ │ ├── types.ts # Session types
+│ │ └── index.ts # Session API
+│ ├── engine/ # Execution loop
+│ │ ├── index.ts # ExecutionEngine class
+│ │ └── types.ts # Engine events, state
+│ ├── plugins/
+│ │ ├── agents/ # Agent plugins (Claude, OpenCode)
+│ │ └── trackers/ # Tracker plugins (JSON, Beads)
+│ │ ├── builtin/
+│ │ │ ├── json.ts # prd.json tracker
+│ │ │ ├── beads.ts # Beads CLI tracker
+│ │ │ └── beads-bv.ts # Beads + bv graph analysis
+│ ├── tui/ # Terminal UI
+│ │ └── components/
+│ │ ├── RunApp.js # Main TUI app
+│ │ └── EpicSelectionApp.js
+│ └── config/ # Configuration
+│ ├── schema.ts # Zod validation
+│ └── types.ts # Config types
+├── .ralph-tui/ # Session & runtime files (created on first run)
+│ ├── session.json # Active session state
+│ ├── session.lock # Process lock file
+│ ├── config.toml # Project config
+│ ├── iterations/ # Per-iteration logs
+│ └── progress.md # Cross-iteration context
+└── skills/ # Bundled Claude Code skills
+ ├── ralph-tui-prd/ # PRD creation skill
+ ├── ralph-tui-create-json/ # Convert PRD → prd.json
+ └── ralph-tui-create-beads/ # Convert PRD → Beads issues
+```
+
+---
+
+## Questions Answered
+
+### Q1: What is ralph-tui? What does it do?
+
+**Ralph TUI is an AI Agent Loop Orchestrator** that automates the cycle of:
+1. **Select Task** → picks highest-priority task from tracker
+2. **Build Prompt** → renders Handlebars template with task data
+3. **Execute Agent** → spawns AI agent (Claude Code / OpenCode)
+4. **Detect Completion** → checks for `COMPLETE` token
+5. **Update Tracker** → marks task complete, moves to next
+
+**Core Workflow:**
+```
+┌─────────────────────────────────────────────────────────────────┐
+│ AUTONOMOUS LOOP │
+│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
+│ │ 1. SELECT │────▶│ 2. BUILD │────▶│ 3. EXECUTE │ │
+│ │ TASK │ │ PROMPT │ │ AGENT │ │
+│ └──────────────┘ └──────────────┘ └──────────────┘ │
+│ ▲ │ │
+│ │ ▼ │
+│ ┌──────────────┐ ┌──────────────┐ │
+│ │ 5. NEXT │◀────────────────────────│ 4. DETECT │ │
+│ │ TASK │ │ COMPLETION │ │
+│ └──────────────┘ └──────────────┘ │
+└─────────────────────────────────────────────────────────────────┘
+```
+
+**Key Features:**
+- **Autonomous execution** - no manual copy/paste of tasks to AI
+- **Session persistence** - pause/resume, crash recovery
+- **Multiple trackers** - prd.json (file-based), Beads (git-backed)
+- **TUI visibility** - real-time agent output, task status, iteration history
+- **Error handling** - configurable retry/skip/abort strategies
+- **Cross-iteration context** - progress.md tracks what's been done
+
+---
+
+### Q2: How does it handle "sessions" and "plans"?
+
+#### Sessions
+
+**One active session at a time**, persisted to `.ralph-tui/session.json`:
+
+**File Location:** `/.ralph-tui/session.json`
+
+**Session State Structure:**
+```typescript
+interface PersistedSessionState {
+ version: 1; // Schema version
+ sessionId: string; // UUID for this session
+ status: SessionStatus; // running | paused | completed | failed | interrupted
+
+ // Timing
+ startedAt: string; // ISO 8601
+ updatedAt: string;
+ pausedAt?: string;
+
+ // Progress tracking
+ currentIteration: number; // 0-based internally, 1-based for display
+ maxIterations: number; // 0 = unlimited
+ tasksCompleted: number;
+
+ // Agent & tracker info
+ agentPlugin: string; // 'claude' | 'opencode'
+ model?: string; // e.g., 'opus', 'sonnet'
+ trackerState: TrackerStateSnapshot;
+
+ // Iteration history
+ iterations: PersistedIterationResult[];
+
+ // Crash recovery
+ activeTaskIds: string[]; // Tasks set to in_progress by this session
+ skippedTaskIds: string[]; // Tasks skipped due to errors
+
+ // UI state
+ subagentPanelVisible?: boolean; // Persist TUI panel state
+
+ cwd: string; // Working directory
+}
+```
+
+**Session Lifecycle:**
+```
+ralph-tui run
+ ↓
+[Check for .ralph-tui/session.json]
+ ↓
+┌─────────────────────────────────┐
+│ Existing session found? │
+├─────────────────────────────────┤
+│ YES: status = 'paused'? │
+│ → Prompt: Resume or New? │
+│ YES: status = 'running'? │
+│ → Stale session recovery │
+│ NO: Create new session │
+└─────────────────────────────────┘
+ ↓
+Create session.lock (PID-based)
+ ↓
+Initialize ExecutionEngine
+ ↓
+Run iteration loop
+ ↓
+Save state after each iteration
+ ↓
+On graceful exit:
+ - Reset activeTaskIds → 'open'
+ - Delete session.json
+ - Release lock
+```
+
+**Session Commands:**
+| Command | Effect |
+|---------|--------|
+| `ralph-tui run` | Start new session OR resume if paused |
+| `ralph-tui resume` | Resume paused session (explicit) |
+| `ralph-tui status` | Check session status without TUI |
+| `ralph-tui status --json` | JSON output for CI/scripts |
+
+**Session Status Flow:**
+```
+ start()
+ idle ────────────────────▶ running
+ ▲ │
+ │ pause()
+ │ ▼
+ │ pausing
+ │ │
+ │ (iteration completes)
+ │ ▼
+ └──────── resume() ─────── paused
+
+ stop()
+ running ────────────────▶ stopping ────▶ idle
+```
+
+#### Plans
+
+**Plans = Task Lists** from a tracker plugin. Ralph supports **three tracker types**:
+
+**1. JSON Tracker** (`prd.json`)
+- **File-based** - no external dependencies
+- **Single plan per file**
+- **Use case:** Quick start, simple projects
+
+**Structure:**
+```json
+{
+ "project": "My Project",
+ "description": "Project description",
+ "userStories": [
+ {
+ "id": "US-001",
+ "title": "Add login",
+ "description": "Implement user login",
+ "priority": 2,
+ "status": "open",
+ "dependsOn": [],
+ "passes": false
+ }
+ ]
+}
+```
+
+**2. Beads Tracker** (`bd` CLI)
+- **Git-backed** - issues in `.beads/beads.jsonl`
+- **Multiple epics** - hierarchical structure
+- **Use case:** Larger projects, git-synced workflows
+
+**3. Beads-BV Tracker** (`bd` + `bv`)
+- **Graph analysis** - PageRank, critical path, cycle detection
+- **Intelligent selection** - picks tasks with highest impact
+- **Use case:** Complex dependency graphs
+
+**Multiple Plans Pattern:**
+```bash
+# Plan 1: Feature A
+ralph-tui run --epic feature-a-epic
+ → Works on tasks under feature-a-epic
+ → Pause when done
+
+# Plan 2: Feature B (different session)
+ralph-tui run --epic feature-b-epic
+ → Works on tasks under feature-b-epic
+```
+
+**Each session tracks its plan:**
+```typescript
+trackerState: {
+ plugin: 'beads',
+ epicId: 'feature-a-epic', // Which plan we're executing
+ totalTasks: 12,
+ tasks: [ /* task snapshots */ ]
+}
+```
+
+---
+
+### Q3: Can you have multiple sessions or plans? Or only one at a time?
+
+**Answer:**
+- **Sessions:** **ONE at a time** (enforced by `.ralph-tui/session.lock`)
+- **Plans:** **MULTIPLE plans exist**, but **ONE active plan per session**
+
+**Lock Enforcement:**
+```typescript
+// File: src/session/lock.ts
+interface LockFile {
+ pid: number; // Process ID
+ sessionId: string; // UUID
+ acquiredAt: string; // ISO 8601
+ cwd: string;
+ hostname: string;
+}
+
+// Lock file: .ralph-tui/session.lock
+```
+
+**If you try to start a second session:**
+```bash
+$ ralph-tui run --epic another-epic
+Error: Another Ralph session is running (PID 12345)
+Use 'ralph-tui resume --force' to override a stale lock.
+```
+
+**Switching Plans Across Sessions:**
+```bash
+# Session 1: Work on Epic A
+$ ralph-tui run --epic epic-a
+ ... works on epic-a tasks ...
+ [Press 'p' to pause]
+
+# Session 2: Work on Epic B (new session)
+$ ralph-tui run --epic epic-b
+ ... works on epic-b tasks ...
+```
+
+**Dynamic Epic Switching (within session):**
+```bash
+# Start with epic-a
+$ ralph-tui run --epic epic-a
+
+# In TUI: Press 'l' to load different epic
+ → Shows epic selection UI
+ → Switch to epic-b mid-session
+ → Continues with epic-b tasks
+```
+
+**Implementation:**
+```typescript
+// File: src/plugins/trackers/types.ts
+interface TrackerPlugin {
+ // Set epic ID dynamically
+ setEpicId?(epicId: string): void;
+
+ // Get current epic ID
+ getEpicId?(): string;
+
+ // Get available epics
+ getEpics(): Promise;
+}
+```
+
+---
+
+## Conventions Discovered
+
+### Naming
+
+| Type | Convention | Example |
+|------|------------|---------|
+| Files | kebab-case | `session-persistence.ts` |
+| React Components | PascalCase | `RunApp.js`, `EpicSelectionApp.js` |
+| Interfaces | PascalCase | `PersistedSessionState`, `TrackerPlugin` |
+| Functions | camelCase | `loadPersistedSession()`, `hasPersistedSession()` |
+| Constants | UPPER_SNAKE_CASE | `SESSION_FILE`, `PROMISE_COMPLETE_PATTERN` |
+
+### Patterns
+
+| Pattern | Usage | Example |
+|---------|-------|---------|
+| **Plugin System** | Agents & trackers | `TrackerPlugin`, `AgentPlugin` interfaces |
+| **Event-Driven** | Engine emits events | `engine:started`, `iteration:completed` |
+| **State Machines** | Session & engine status | `idle → running → pausing → paused` |
+| **Builder Pattern** | Config construction | `buildConfig()`, `validateConfig()` |
+| **Repository Pattern** | Plugin registries | `AgentRegistry`, `TrackerRegistry` |
+| **Factory Pattern** | Plugin creation | `TrackerPluginFactory`, `AgentPluginFactory` |
+
+### Testing
+
+- **Test location:** Not visible in current structure (no `__tests__/` or `spec/`)
+- **Type checking:** `bun run typecheck` (no emit)
+- **Linting:** `bun run lint` (ESLint)
+
+---
+
+## Architecture Map
+
+### Data Flow
+
+```
+┌──────────────────────────────────────────────────────────────────┐
+│ USER LAYER │
+├──────────────────────────────────────────────────────────────────┤
+│ CLI (cli.tsx) │
+│ ├─ run [options] → Start new session │
+│ ├─ resume → Resume paused session │
+│ ├─ create-prd → Create PRD with AI │
+│ └─ status → Check session status │
+└───────────────┬──────────────────────────────────────────────────┘
+ │
+ ▼
+┌──────────────────────────────────────────────────────────────────┐
+│ COMMAND LAYER │
+├──────────────────────────────────────────────────────────────────┤
+│ commands/ │
+│ ├─ run.tsx → Parse args, load config, start TUI │
+│ ├─ resume.tsx → Load session, resume engine │
+│ └─ create-prd.tsx → AI chat → PRD → tasks │
+└───────────────┬──────────────────────────────────────────────────┘
+ │
+ ▼
+┌──────────────────────────────────────────────────────────────────┐
+│ SESSION LAYER │
+├──────────────────────────────────────────────────────────────────┤
+│ session/ │
+│ ├─ persistence.ts → CRUD for session.json │
+│ ├─ lock.ts → PID-based process locking │
+│ └─ index.ts → createSession(), resumeSession() │
+└───────────────┬──────────────────────────────────────────────────┘
+ │
+ ▼
+┌──────────────────────────────────────────────────────────────────┐
+│ ENGINE LAYER │
+├──────────────────────────────────────────────────────────────────┤
+│ engine/ │
+│ └─ index.ts (ExecutionEngine) │
+│ ├─ start() → Initialize, run loop │
+│ ├─ runIteration() → Select task, execute agent │
+│ ├─ pause() → Set pausing flag │
+│ ├─ resume() → Continue from paused │
+│ └─ stop() → Interrupt, cleanup │
+└───────────────┬──────────────────────────────────────────────────┘
+ │
+ ├──────────────────────┬───────────────────────────┐
+ ▼ ▼ ▼
+┌───────────────────────┐ ┌───────────────────────┐ ┌──────────────────┐
+│ TRACKER PLUGINS │ │ AGENT PLUGINS │ │ TUI LAYER │
+├───────────────────────┤ ├───────────────────────┤ ├──────────────────┤
+│ JSON Tracker │ │ Claude Agent │ │ RunApp (React) │
+│ → prd.json │ │ → claude CLI │ │ - Task list │
+│ │ │ │ │ - Agent output │
+│ Beads Tracker │ │ OpenCode Agent │ │ - Iteration │
+│ → bd CLI │ │ → opencode CLI │ │ history │
+│ │ │ │ │ - Dashboard │
+│ Beads-BV Tracker │ │ │ │ │
+│ → bd + bv CLI │ │ │ │ │
+└───────────────────────┘ └───────────────────────┘ └──────────────────┘
+```
+
+### Session State Machine
+
+```
+ start()
+ ┌─────┐ ──────────────▶ ┌─────────┐
+ │IDLE │ │ RUNNING │
+ └─────┘ ◀────────────── └─────────┘
+ stop() │
+ pause()
+ │
+ ▼
+ ┌─────────┐
+ │ PAUSING │
+ └─────────┘
+ │
+ (iteration completes)
+ │
+ ▼
+ resume() ┌─────────┐
+ ────────────▶ │ PAUSED │
+ └─────────┘
+ │
+ stop()
+ │
+ ▼
+ ┌──────────┐
+ │ STOPPING │
+ └──────────┘
+ │
+ ▼
+ ┌─────┐
+ │IDLE │
+ └─────┘
+```
+
+---
+
+## Key Files
+
+| File | Purpose | Entry Points |
+|------|---------|--------------|
+| `src/cli.tsx` | CLI entry, command routing | `main()` |
+| `src/commands/run.tsx` | Start session command | `executeRunCommand()` |
+| `src/commands/resume.tsx` | Resume session command | `executeResumeCommand()` |
+| `src/session/persistence.ts` | Session CRUD operations | `loadPersistedSession()`, `savePersistedSession()` |
+| `src/session/lock.ts` | Process locking | `acquireLock()`, `releaseLock()` |
+| `src/engine/index.ts` | Execution loop | `ExecutionEngine.start()` |
+| `src/plugins/trackers/builtin/json.ts` | prd.json tracker | `JsonTrackerPlugin` |
+| `src/plugins/trackers/builtin/beads.ts` | Beads tracker | `BeadsTrackerPlugin` |
+| `src/plugins/agents/builtin/claude.ts` | Claude agent | `ClaudeAgentPlugin` |
+| `src/tui/components/RunApp.js` | Main TUI component | `RunApp` (React) |
+
+---
+
+## Session vs Plan: Design Decision
+
+**Why one session at a time?**
+
+1. **Process Safety:** Single session.lock prevents race conditions
+2. **Resource Management:** One agent process at a time (no concurrent API calls)
+3. **TUI Clarity:** User focuses on one task stream
+4. **State Consistency:** Session.json always reflects single coherent state
+
+**Why multiple plans?**
+
+1. **Project Organization:** Different features/epics live independently
+2. **Prioritization:** Switch focus between urgent work without losing progress
+3. **Team Coordination:** Different epics can be worked on sequentially
+
+**The Trade-off:**
+
+Ralph sacrifices **concurrent execution** for **simplicity and safety**. You can't work on multiple epics simultaneously, but you can:
+- **Pause** epic-a mid-session
+- **Start new session** for urgent epic-b
+- **Resume** epic-a later
+
+This aligns with the **single-threaded nature of AI agents** and **human attention**.
+
+---
+
+## Open Questions
+
+1. **Can you run multiple ralph-tui instances in different project directories?**
+ - **Likely YES** - lock file is `.ralph-tui/session.lock` (project-local)
+ - Would need to verify no global locks exist
+
+2. **What happens if you edit prd.json while session is running?**
+ - Tasks are loaded at session start into `trackerState.tasks[]`
+ - Mid-session edits probably won't be picked up until next session
+ - Beads tracker may sync changes (needs testing)
+
+3. **Can you create a new prd.json for a different plan while a session is running?**
+ - Session holds lock only for `.ralph-tui/session.lock`
+ - Creating a new `prd-feature-b.json` should be safe
+ - But can't **run** it until current session ends
+
+---
+
+## Final Answer: Multiple Sessions or Plans?
+
+### Sessions
+**ONE at a time** - enforced by:
+- `.ralph-tui/session.lock` (PID-based)
+- Only one `ExecutionEngine` instance per project directory
+- Attempting second session → error or resume prompt
+
+### Plans
+**MULTIPLE plans supported** - but:
+- **ONE plan active per session** (via `epicId` or `prdPath`)
+- Can **switch plans** by:
+ - Pausing current session
+ - Starting new session with different `--epic` or `--prd`
+ - Pressing `l` in TUI to switch epics dynamically (for Beads tracker)
+
+**Analogy:**
+- **Session** = Your current coding session (one editor window)
+- **Plan** = Your to-do list (can have many, but only work one at a time)
+
+You can have 10 different PRDs or epics, but Ralph will only work on one at a time within a single session.
+
+---
+
+**Report complete.** Ralph TUI's architecture clearly enforces **one active session** but allows **multiple plans** that can be worked on sequentially.
diff --git a/.claude/tsc-cache/ef69acfb-24b1-42cd-809c-acf3c2abb82e/affected-repos.txt b/.claude/tsc-cache/ef69acfb-24b1-42cd-809c-acf3c2abb82e/affected-repos.txt
new file mode 100644
index 00000000..85de9cf9
--- /dev/null
+++ b/.claude/tsc-cache/ef69acfb-24b1-42cd-809c-acf3c2abb82e/affected-repos.txt
@@ -0,0 +1 @@
+src
diff --git a/.learnings/ERRORS.md b/.learnings/ERRORS.md
new file mode 100644
index 00000000..27546bff
--- /dev/null
+++ b/.learnings/ERRORS.md
@@ -0,0 +1,122 @@
+## [ERR-20260113-001] bd create
+
+**Logged**: 2026-01-13T00:00:00Z
+**Priority**: high
+**Status**: pending
+**Area**: docs
+
+### Summary
+bd create failed because no beads database exists for this repo
+
+### Error
+```
+Error: no beads database found
+
+Found JSONL file: /.beads/issues.jsonl
+This looks like a fresh clone or JSONL-only project.
+
+Options:
+ • Run 'bd init' to create database and import issues
+ • Use 'bd --no-db create' for JSONL-only mode
+ • Add 'no-db: true' to .beads/config.yaml for permanent JSONL-only mode
+```
+
+### Context
+- Command attempted: `bd create --title="Fix TUI mouse capture on exit" --type=bug --priority=1`
+- Repo has JSONL-only beads data under `.beads/`
+
+### Suggested Fix
+Use `bd --no-db create` for JSONL-only projects or run `bd init` first.
+
+### Metadata
+- Reproducible: yes
+- Related Files: .beads/issues.jsonl
+- See Also: none
+
+---
+## [ERR-20260113-002] bd --no-db create
+
+**Logged**: 2026-01-13T00:00:00Z
+**Priority**: high
+**Status**: pending
+**Area**: docs
+
+### Summary
+bd --no-db create failed due to mixed issue prefixes in .beads/issues.jsonl
+
+### Error
+```
+Error initializing --no-db mode: failed to detect prefix: issues have mixed prefixes, please set issue-prefix in .beads/config.yaml
+```
+
+### Context
+- Command attempted: `bd --no-db create --title="Fix TUI mouse capture on exit" --type=bug --priority=1`
+- Repo uses JSONL-only beads with mixed prefixes
+
+### Suggested Fix
+Set `issue-prefix` in `.beads/config.yaml` or run `bd init` to normalize.
+
+### Metadata
+- Reproducible: yes
+- Related Files: .beads/issues.jsonl, .beads/config.yaml
+- See Also: ERR-20260113-001
+
+---
+## [ERR-20260113-003] bun run typecheck
+
+**Logged**: 2026-01-13T00:00:00Z
+**Priority**: high
+**Status**: pending
+**Area**: config
+
+### Summary
+TypeScript typecheck failed because @types/node is missing
+
+### Error
+```
+error TS2688: Cannot find type definition file for 'node'.
+ The file is in the program because:
+ Entry point of type library 'node' specified in compilerOptions
+```
+
+### Context
+- Command attempted: `bun run typecheck`
+- Ran in
+
+### Suggested Fix
+Run `bun install` or ensure node type definitions are available in node_modules.
+
+### Metadata
+- Reproducible: yes
+- Related Files: tsconfig.json, package.json
+- See Also: none
+
+---
+## [ERR-20260113-004] bun run typecheck
+
+**Logged**: 2026-01-13T00:00:00Z
+**Priority**: high
+**Status**: pending
+**Area**: config
+
+### Summary
+TypeScript typecheck failed because node-notifier dependency was missing
+
+### Error
+```
+src/notifications.ts(8,22): error TS2307: Cannot find module 'node-notifier' or its corresponding type declarations.
+```
+
+### Context
+- Command attempted: `bun run typecheck`
+- After rebasing with upstream changes
+
+### Suggested Fix
+Run `bun install` to pull new dependencies from package.json.
+
+### Metadata
+- Reproducible: yes
+- Related Files: package.json, src/notifications.ts
+- See Also: ERR-20260113-003
+
+---
diff --git a/src/plugins/agents/builtin/ampcode.ts b/src/plugins/agents/builtin/ampcode.ts
new file mode 100644
index 00000000..a7478a6c
--- /dev/null
+++ b/src/plugins/agents/builtin/ampcode.ts
@@ -0,0 +1,428 @@
+/**
+ * ABOUTME: Ampcode agent plugin for the amp CLI.
+ * Integrates with Ampcode AI coding assistant for AI-assisted coding.
+ * Supports: execute mode, model selection, dangerously-allow-all mode,
+ * stream-json output, timeout, and graceful interruption.
+ */
+
+import { execFile } from 'node:child_process';
+import { BaseAgentPlugin, findCommandPath } from '../base.js';
+import type {
+ AgentPluginMeta,
+ AgentPluginFactory,
+ AgentFileContext,
+ AgentExecuteOptions,
+ AgentSetupQuestion,
+ AgentDetectResult,
+} from '../types.js';
+
+/**
+ * Represents a parsed JSONL message from Ampcode output.
+ * Ampcode emits Claude Code-compatible stream JSON format.
+ */
+export interface AmpcodeJsonlMessage {
+ /** The type of message */
+ type?: string;
+ /** Message content for text messages */
+ message?: string;
+ /** Tool use information if applicable */
+ tool?: {
+ name?: string;
+ input?: Record;
+ };
+ /** Result data for completion messages */
+ result?: unknown;
+ /** Session ID for conversation tracking */
+ sessionId?: string;
+ /** Raw parsed JSON for custom handling */
+ raw: Record;
+}
+
+/**
+ * Result of parsing a JSONL line.
+ */
+export type JsonlParseResult =
+ | { success: true; message: AmpcodeJsonlMessage }
+ | { success: false; raw: string; error: string };
+
+/**
+ * Ampcode agent plugin implementation.
+ * Uses the `amp` CLI to execute AI coding tasks.
+ *
+ * Key features:
+ * - Auto-detects amp binary using `which`
+ * - Executes in execute mode (-x) for non-interactive use
+ * - Supports --dangerously-allow-all for autonomous operation
+ * - Configurable mode selection (free, rush, smart)
+ * - Stream JSON output for structured responses
+ * - Timeout handling with graceful SIGINT before SIGTERM
+ * - Streaming stdout/stderr capture
+ */
+export class AmpcodeAgentPlugin extends BaseAgentPlugin {
+ readonly meta: AgentPluginMeta = {
+ id: 'ampcode',
+ name: 'Ampcode',
+ description: 'Ampcode AI coding assistant CLI',
+ version: '1.0.0',
+ author: 'Amp',
+ defaultCommand: 'amp',
+ supportsStreaming: true,
+ supportsInterrupt: true,
+ supportsFileContext: false, // amp doesn't have explicit file context flags
+ supportsSubagentTracing: true,
+ structuredOutputFormat: 'jsonl',
+ };
+
+ /** Output mode: text or stream-json */
+ private streamJson = false;
+
+ /** Agent mode: free, rush, smart */
+ private mode?: string;
+
+ /** Allow all tool executions without confirmation (default: false for security) */
+ private dangerouslyAllowAll = false;
+
+ /** Timeout in milliseconds (0 = no timeout) */
+ protected override defaultTimeout = 0;
+
+ override async initialize(config: Record): Promise {
+ await super.initialize(config);
+
+ if (typeof config.streamJson === 'boolean') {
+ this.streamJson = config.streamJson;
+ }
+
+ if (
+ typeof config.mode === 'string' &&
+ ['free', 'rush', 'smart'].includes(config.mode)
+ ) {
+ this.mode = config.mode;
+ }
+
+ if (typeof config.dangerouslyAllowAll === 'boolean') {
+ this.dangerouslyAllowAll = config.dangerouslyAllowAll;
+ }
+
+ if (typeof config.timeout === 'number' && config.timeout > 0) {
+ this.defaultTimeout = config.timeout;
+ }
+ }
+
+ /**
+ * Detect amp CLI availability.
+ */
+ override async detect(): Promise {
+ const command = this.commandPath ?? this.meta.defaultCommand;
+
+ const findResult = await findCommandPath(command);
+
+ if (!findResult.found) {
+ return {
+ available: false,
+ error: `Ampcode CLI not found in PATH. Install from: https://ampcode.com`,
+ };
+ }
+
+ // Verify the binary works by running --version
+ const versionResult = await this.runVersion(findResult.path);
+
+ if (!versionResult.success) {
+ return {
+ available: false,
+ executablePath: findResult.path,
+ error: versionResult.error,
+ };
+ }
+
+ return {
+ available: true,
+ version: versionResult.version,
+ executablePath: findResult.path,
+ };
+ }
+
+ /**
+ * Run --version to verify binary and extract version number
+ */
+ private runVersion(
+ command: string
+ ): Promise<{ success: boolean; version?: string; error?: string }> {
+ return new Promise((resolve) => {
+ let resolved = false;
+
+ // Timeout after 5 seconds
+ const timer = setTimeout(() => {
+ if (!resolved) {
+ resolved = true;
+ resolve({ success: false, error: 'Timeout waiting for --version' });
+ }
+ }, 5000);
+
+ execFile(command, ['--version'], (error, stdout, stderr) => {
+ clearTimeout(timer);
+ if (resolved) return;
+ resolved = true;
+
+ if (error) {
+ resolve({
+ success: false,
+ error: stderr || `Failed to execute: ${error.message}`,
+ });
+ return;
+ }
+
+ // Extract version from output
+ const versionMatch = stdout.match(/(\d+\.\d+\.\d+)/);
+ resolve({
+ success: true,
+ version: versionMatch?.[1],
+ });
+ });
+ });
+ }
+
+ override getSetupQuestions(): AgentSetupQuestion[] {
+ const baseQuestions = super.getSetupQuestions();
+ return [
+ ...baseQuestions,
+ {
+ id: 'mode',
+ prompt: 'Agent mode:',
+ type: 'select',
+ choices: [
+ { value: 'smart', label: 'Smart', description: 'Balanced mode (default)' },
+ { value: 'free', label: 'Free', description: 'More creative, less constrained' },
+ { value: 'rush', label: 'Rush', description: 'Faster, more direct responses' },
+ ],
+ default: 'smart',
+ required: false,
+ help: 'Controls the model, system prompt, and tool selection',
+ },
+ {
+ id: 'streamJson',
+ prompt: 'Use stream JSON output?',
+ type: 'boolean',
+ default: false,
+ required: false,
+ help: 'Output in Claude Code-compatible stream JSON format',
+ },
+ {
+ id: 'dangerouslyAllowAll',
+ prompt: '⚠️ Allow all tool executions without confirmation (autonomous mode)?',
+ type: 'boolean',
+ default: false,
+ required: false,
+ help: 'Enable --dangerously-allow-all for autonomous operation. Warning: This bypasses all safety prompts.',
+ },
+ ];
+ }
+
+ protected buildArgs(
+ _prompt: string,
+ _files?: AgentFileContext[],
+ options?: AgentExecuteOptions
+ ): string[] {
+ const args: string[] = [];
+
+ // Use execute mode for non-interactive
+ args.push('--execute');
+
+ // Add stream-json output if needed for subagent tracing
+ if (options?.subagentTracing || this.streamJson) {
+ args.push('--stream-json');
+ }
+
+ // Add mode if specified
+ if (this.mode) {
+ args.push('--mode', this.mode);
+ }
+
+ // Allow all tool executions for autonomous operation
+ if (this.dangerouslyAllowAll) {
+ args.push('--dangerously-allow-all');
+ }
+
+ // Disable notifications in execute mode
+ args.push('--no-notifications');
+
+ // NOTE: Prompt is NOT added here - it's passed via stdin
+
+ return args;
+ }
+
+ /**
+ * Provide the prompt via stdin instead of command args.
+ */
+ protected override getStdinInput(
+ prompt: string,
+ _files?: AgentFileContext[],
+ _options?: AgentExecuteOptions
+ ): string {
+ return prompt;
+ }
+
+ override async validateSetup(
+ answers: Record
+ ): Promise {
+ // Validate mode
+ const mode = answers.mode;
+ if (
+ mode !== undefined &&
+ mode !== '' &&
+ !['free', 'rush', 'smart'].includes(String(mode))
+ ) {
+ return 'Invalid mode. Must be one of: free, rush, smart';
+ }
+
+ return null;
+ }
+
+ /**
+ * Validate a model name for the Ampcode agent.
+ */
+ override validateModel(_model: string): string | null {
+ // Ampcode handles model selection via mode, not explicit model names
+ return null;
+ }
+
+ /**
+ * Parse a single line of JSONL output from Ampcode.
+ */
+ static parseJsonlLine(line: string): JsonlParseResult {
+ const trimmed = line.trim();
+
+ if (!trimmed) {
+ return { success: false, raw: line, error: 'Empty line' };
+ }
+
+ try {
+ const parsed = JSON.parse(trimmed) as Record;
+
+ const message: AmpcodeJsonlMessage = {
+ raw: parsed,
+ };
+
+ if (typeof parsed.type === 'string') {
+ message.type = parsed.type;
+ }
+ if (typeof parsed.message === 'string') {
+ message.message = parsed.message;
+ }
+ if (typeof parsed.sessionId === 'string') {
+ message.sessionId = parsed.sessionId;
+ }
+ if (parsed.result !== undefined) {
+ message.result = parsed.result;
+ }
+
+ if (parsed.tool && typeof parsed.tool === 'object') {
+ const toolObj = parsed.tool as Record;
+ message.tool = {
+ name: typeof toolObj.name === 'string' ? toolObj.name : undefined,
+ input:
+ toolObj.input && typeof toolObj.input === 'object'
+ ? (toolObj.input as Record)
+ : undefined,
+ };
+ }
+
+ return { success: true, message };
+ } catch (err) {
+ return {
+ success: false,
+ raw: line,
+ error: err instanceof Error ? err.message : 'Parse error',
+ };
+ }
+ }
+
+ /**
+ * Parse complete JSONL output from Ampcode.
+ */
+ static parseJsonlOutput(output: string): {
+ messages: AmpcodeJsonlMessage[];
+ fallback: string[];
+ } {
+ const messages: AmpcodeJsonlMessage[] = [];
+ const fallback: string[] = [];
+
+ const lines = output.split('\n');
+
+ for (const line of lines) {
+ const result = AmpcodeAgentPlugin.parseJsonlLine(line);
+ if (result.success) {
+ messages.push(result.message);
+ } else if (result.raw.trim()) {
+ fallback.push(result.raw);
+ }
+ }
+
+ return { messages, fallback };
+ }
+
+ /**
+ * Create a streaming JSONL parser.
+ */
+ static createStreamingJsonlParser(): {
+ push: (chunk: string) => JsonlParseResult[];
+ flush: () => JsonlParseResult[];
+ getState: () => { messages: AmpcodeJsonlMessage[]; fallback: string[] };
+ } {
+ let buffer = '';
+ const messages: AmpcodeJsonlMessage[] = [];
+ const fallback: string[] = [];
+
+ return {
+ push(chunk: string): JsonlParseResult[] {
+ buffer += chunk;
+ const results: JsonlParseResult[] = [];
+
+ let newlineIndex: number;
+ while ((newlineIndex = buffer.indexOf('\n')) !== -1) {
+ const line = buffer.slice(0, newlineIndex);
+ buffer = buffer.slice(newlineIndex + 1);
+
+ const result = AmpcodeAgentPlugin.parseJsonlLine(line);
+ results.push(result);
+
+ if (result.success) {
+ messages.push(result.message);
+ } else if (result.raw.trim()) {
+ fallback.push(result.raw);
+ }
+ }
+
+ return results;
+ },
+
+ flush(): JsonlParseResult[] {
+ if (!buffer.trim()) {
+ buffer = '';
+ return [];
+ }
+
+ const result = AmpcodeAgentPlugin.parseJsonlLine(buffer);
+ buffer = '';
+
+ if (result.success) {
+ messages.push(result.message);
+ } else if (result.raw.trim()) {
+ fallback.push(result.raw);
+ }
+
+ return [result];
+ },
+
+ getState(): { messages: AmpcodeJsonlMessage[]; fallback: string[] } {
+ return { messages, fallback };
+ },
+ };
+ }
+}
+
+/**
+ * Factory function for the Ampcode agent plugin.
+ */
+const createAmpcodeAgent: AgentPluginFactory = () => new AmpcodeAgentPlugin();
+
+export default createAmpcodeAgent;
diff --git a/src/plugins/agents/builtin/index.ts b/src/plugins/agents/builtin/index.ts
index b8d85bbd..2c4527e4 100644
--- a/src/plugins/agents/builtin/index.ts
+++ b/src/plugins/agents/builtin/index.ts
@@ -7,6 +7,7 @@ import { getAgentRegistry } from '../registry.js';
import createDroidAgent from '../droid/index.js';
import createClaudeAgent from './claude.js';
import createOpenCodeAgent from './opencode.js';
+import createAmpcodeAgent from './ampcode.js';
/**
* Register all built-in agent plugins with the registry.
@@ -18,12 +19,17 @@ export function registerBuiltinAgents(): void {
// Register built-in plugins
registry.registerBuiltin(createClaudeAgent);
registry.registerBuiltin(createOpenCodeAgent);
+ registry.registerBuiltin(createAmpcodeAgent);
registry.registerBuiltin(createDroidAgent);
}
// Export the factory functions for direct use
-export { createClaudeAgent, createOpenCodeAgent, createDroidAgent };
+export { createClaudeAgent, createOpenCodeAgent, createAmpcodeAgent, createDroidAgent };
// Export Claude JSONL parsing types and utilities
export type { ClaudeJsonlMessage, JsonlParseResult } from './claude.js';
export { ClaudeAgentPlugin } from './claude.js';
+
+// Export Ampcode JSONL parsing types and utilities
+export type { AmpcodeJsonlMessage } from './ampcode.js';
+export { AmpcodeAgentPlugin } from './ampcode.js';
diff --git a/thoughts/shared/handoffs/events/2026-01-14T21-39-32.258Z_ef69acfb.md b/thoughts/shared/handoffs/events/2026-01-14T21-39-32.258Z_ef69acfb.md
new file mode 100644
index 00000000..e0ab4f8e
--- /dev/null
+++ b/thoughts/shared/handoffs/events/2026-01-14T21-39-32.258Z_ef69acfb.md
@@ -0,0 +1,10 @@
+---
+ts: 2026-01-14T21:39:32.258Z
+agent: ef69acfb
+branch: main
+type: session_end
+reason: prompt_input_exit
+---
+
+## Session End
+Updated: 2026-01-14T21:39:32.258Z
diff --git a/thoughts/shared/handoffs/events/2026-01-15T03-23-16.669Z_91e50da5.md b/thoughts/shared/handoffs/events/2026-01-15T03-23-16.669Z_91e50da5.md
new file mode 100644
index 00000000..88b9d773
--- /dev/null
+++ b/thoughts/shared/handoffs/events/2026-01-15T03-23-16.669Z_91e50da5.md
@@ -0,0 +1,10 @@
+---
+ts: 2026-01-15T03:23:16.670Z
+agent: 91e50da5
+branch: main
+type: session_end
+reason: other
+---
+
+## Session End
+Updated: 2026-01-15T03:23:16.670Z