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