From 161e424632b269babf6f339761af33e2b456a38d Mon Sep 17 00:00:00 2001 From: Justin Parker Date: Sat, 8 Aug 2026 12:39:28 -0700 Subject: [PATCH] Add Devin CLI as a selectable agent backend in plan2code-loop Upstream replaced GitHub Copilot CLI with Devin; Plan2Code keeps both, so Claude Code, Copilot CLI and Devin are all selectable and existing Copilot selections keep working. AI Assisted --- plan2code-loop/README.md | 6 +- plan2code-loop/package.json | 93 +++++++++++++------------- plan2code-loop/src/agents/devin-cli.ts | 81 ++++++++++++++++++++++ plan2code-loop/src/agents/index.ts | 41 ++++++------ plan2code-loop/src/agents/types.ts | 85 +++++++++++------------ plan2code-loop/src/state/config.ts | 70 +++++++++---------- 6 files changed, 232 insertions(+), 144 deletions(-) create mode 100644 plan2code-loop/src/agents/devin-cli.ts diff --git a/plan2code-loop/README.md b/plan2code-loop/README.md index 1e632d1..0b4567d 100644 --- a/plan2code-loop/README.md +++ b/plan2code-loop/README.md @@ -109,6 +109,7 @@ The scratchpad is managed by the LLM itself - after each task, the AI appends no |-------|--------| | Claude Code | Supported | | GitHub Copilot CLI | Supported | +| Devin CLI | Supported | The loop uses your configured default model for each agent. @@ -119,7 +120,7 @@ $ plan2code-loop ╭──────────────────────────────────────╮ │ │ - │ 🔮 Plany's Loop │ + │ 🔮 Planny's Loop │ │ Autonomous Implementation │ │ │ ╰──────────────────────────────────────╯ @@ -196,7 +197,8 @@ src/ ├── cli.ts # Interactive prompts ├── agents/ # Agent implementations │ ├── claude-code.ts -│ └── copilot-cli.ts +│ ├── copilot-cli.ts +│ └── devin-cli.ts ├── prompt/ # Prompt building │ ├── templates.ts │ └── builder.ts diff --git a/plan2code-loop/package.json b/plan2code-loop/package.json index 87e6fcb..e271a44 100644 --- a/plan2code-loop/package.json +++ b/plan2code-loop/package.json @@ -1,46 +1,47 @@ -{ - "name": "plan2code-loop", - "version": "1.6.2", - "description": "Plan2Code Loop - Autonomous spec-driven implementation CLI", - "type": "module", - "main": "dist/index.js", - "bin": { - "plan2code-loop": "./dist/bin/plan2code-loop.js" - }, - "files": [ - "dist" - ], - "engines": { - "node": ">=18.0.0" - }, - "keywords": [ - "cli", - "ai", - "agent", - "automation", - "claude", - "copilot", - "spec-driven" - ], - "license": "MIT", - "scripts": { - "build": "tsup", - "dev": "tsup --watch", - "start": "node dist/bin/plan2code-loop.js", - "prepublishOnly": "npm run build" - }, - "dependencies": { - "@inquirer/prompts": "^8.1.0", - "chalk": "^5.6.2", - "execa": "^9.6.1", - "fs-extra": "^11.3.3", - "ora": "^9.0.0" - }, - "devDependencies": { - "@types/fs-extra": "^11.0.4", - "@types/node": "^25.0.3", - "tsup": "^8.5.1", - "typescript": "^5.9.3" - } -} - +{ + "name": "plan2code-loop", + "version": "1.6.2", + "description": "Plan2Code Loop - Autonomous spec-driven implementation CLI", + "type": "module", + "main": "dist/index.js", + "bin": { + "plan2code-loop": "./dist/bin/plan2code-loop.js" + }, + "files": [ + "dist" + ], + "engines": { + "node": ">=18.0.0" + }, + "keywords": [ + "cli", + "ai", + "agent", + "automation", + "claude", + "copilot", + "devin", + "spec-driven" + ], + "license": "MIT", + "scripts": { + "build": "tsup", + "dev": "tsup --watch", + "start": "node dist/bin/plan2code-loop.js", + "prepublishOnly": "npm run build" + }, + "dependencies": { + "@inquirer/prompts": "^8.1.0", + "chalk": "^5.6.2", + "execa": "^9.6.1", + "fs-extra": "^11.3.3", + "ora": "^9.0.0" + }, + "devDependencies": { + "@types/fs-extra": "^11.0.4", + "@types/node": "^25.0.3", + "tsup": "^8.5.1", + "typescript": "^5.9.3" + } +} + diff --git a/plan2code-loop/src/agents/devin-cli.ts b/plan2code-loop/src/agents/devin-cli.ts new file mode 100644 index 0000000..a55d48d --- /dev/null +++ b/plan2code-loop/src/agents/devin-cli.ts @@ -0,0 +1,81 @@ +import type { Agent, AgentConfig, AgentExecutionOptions, AgentExecutionResult } from './types.js'; +import { executeCommand } from '../utils/process.js'; +import { writeFileSync, unlinkSync } from 'fs'; +import { join } from 'path'; +import { tmpdir } from 'os'; + +const devinCliConfig: AgentConfig = { + name: 'devin-cli', + displayName: 'Devin CLI', + command: 'devin', + models: [ + { value: 'default', label: 'Default (use Devin config)' }, + ], + defaultModel: 'default', + flags: { + prompt: '--print', + promptFile: '--prompt-file', + model: '--model', + skipPermissions: '--permission-mode', + }, +}; + +class DevinCliAgent implements Agent { + readonly config = devinCliConfig; + + async execute(options: AgentExecutionOptions): Promise { + // Devin CLI takes the prompt via --prompt-file rather than stdin + const tempFile = join(tmpdir(), `plan2code-prompt-${Date.now()}.txt`); + writeFileSync(tempFile, options.prompt, 'utf-8'); + + try { + const args: string[] = [ + this.config.flags.prompt, // --print for non-interactive mode + this.config.flags.promptFile!, tempFile, // --prompt-file + this.config.flags.skipPermissions, 'dangerous', // --permission-mode dangerous (auto-approve all tools) + ]; + + // Only add --model if not using default + if (options.model && options.model !== 'default') { + args.push(this.config.flags.model, options.model); + } + + const result = await executeCommand({ + command: this.config.command, + args, + cwd: options.cwd, + timeout: options.timeout, + signal: options.signal, + }); + + return { + stdout: result.stdout, + stderr: result.stderr, + exitCode: result.exitCode, + timedOut: result.timedOut, + cancelled: result.cancelled, + duration: result.duration, + }; + } finally { + // Clean up temp file + try { + unlinkSync(tempFile); + } catch { + // Ignore cleanup errors + } + } + } + + async isAvailable(): Promise { + // Run devin --version to verify it's actually installed and working + const result = await executeCommand({ + command: this.config.command, + args: ['--version'], + cwd: process.cwd(), + timeout: 5000, + }); + return result.exitCode === 0; + } +} + +export const devinCliAgent = new DevinCliAgent(); diff --git a/plan2code-loop/src/agents/index.ts b/plan2code-loop/src/agents/index.ts index 0cdea32..beec6f4 100644 --- a/plan2code-loop/src/agents/index.ts +++ b/plan2code-loop/src/agents/index.ts @@ -1,19 +1,22 @@ -export type { - Agent, - AgentConfig, - AgentExecutionOptions, - AgentExecutionResult, - ModelOption, -} from './types.js'; - -export { agentRegistry } from './registry.js'; -export { claudeCodeAgent } from './claude-code.js'; -export { copilotCliAgent } from './copilot-cli.js'; - -// Register all agents -import { agentRegistry } from './registry.js'; -import { claudeCodeAgent } from './claude-code.js'; -import { copilotCliAgent } from './copilot-cli.js'; - -agentRegistry.register(claudeCodeAgent); -agentRegistry.register(copilotCliAgent); +export type { + Agent, + AgentConfig, + AgentExecutionOptions, + AgentExecutionResult, + ModelOption, +} from './types.js'; + +export { agentRegistry } from './registry.js'; +export { claudeCodeAgent } from './claude-code.js'; +export { copilotCliAgent } from './copilot-cli.js'; +export { devinCliAgent } from './devin-cli.js'; + +// Register all agents +import { agentRegistry } from './registry.js'; +import { claudeCodeAgent } from './claude-code.js'; +import { copilotCliAgent } from './copilot-cli.js'; +import { devinCliAgent } from './devin-cli.js'; + +agentRegistry.register(claudeCodeAgent); +agentRegistry.register(copilotCliAgent); +agentRegistry.register(devinCliAgent); diff --git a/plan2code-loop/src/agents/types.ts b/plan2code-loop/src/agents/types.ts index 94937b2..6f60132 100644 --- a/plan2code-loop/src/agents/types.ts +++ b/plan2code-loop/src/agents/types.ts @@ -1,42 +1,43 @@ -export interface ModelOption { - value: string; - label: string; -} - -export interface AgentConfig { - name: string; - displayName: string; - command: string; - models: ModelOption[]; - defaultModel: string; - flags: { - prompt: string; - model: string; - skipPermissions: string; - silent?: string; - }; -} - -export interface AgentExecutionOptions { - prompt: string; - model: string; - timeout: number; // milliseconds - verbose: boolean; - cwd: string; - signal?: AbortSignal; // For cancellation -} - -export interface AgentExecutionResult { - stdout: string; - stderr: string; - exitCode: number; - timedOut: boolean; - cancelled: boolean; - duration: number; // milliseconds -} - -export interface Agent { - config: AgentConfig; - execute(options: AgentExecutionOptions): Promise; - isAvailable(): Promise; -} +export interface ModelOption { + value: string; + label: string; +} + +export interface AgentConfig { + name: string; + displayName: string; + command: string; + models: ModelOption[]; + defaultModel: string; + flags: { + prompt: string; + model: string; + skipPermissions: string; + silent?: string; + promptFile?: string; + }; +} + +export interface AgentExecutionOptions { + prompt: string; + model: string; + timeout: number; // milliseconds + verbose: boolean; + cwd: string; + signal?: AbortSignal; // For cancellation +} + +export interface AgentExecutionResult { + stdout: string; + stderr: string; + exitCode: number; + timedOut: boolean; + cancelled: boolean; + duration: number; // milliseconds +} + +export interface Agent { + config: AgentConfig; + execute(options: AgentExecutionOptions): Promise; + isAvailable(): Promise; +} diff --git a/plan2code-loop/src/state/config.ts b/plan2code-loop/src/state/config.ts index ca93e79..ed43c20 100644 --- a/plan2code-loop/src/state/config.ts +++ b/plan2code-loop/src/state/config.ts @@ -1,35 +1,35 @@ -export type LoopMode = 'task' | 'phase'; - -export interface SessionConfig { - agent: string; // "claude-code" | "copilot-cli" - model: string; // Selected model - maxIterations: number; // 5-50 - timeout: number; // Base timeout in minutes per iteration attempt - maxRetries: number; // Max retry attempts per iteration (timeout increments by 30s each retry) - verbose: boolean; - specPath: string; // Path to the spec directory - startedAt: string; // ISO timestamp - currentIteration: number; - jiraTicketId?: string; // JIRA ticket ID for commit messages - loopMode: LoopMode; // "task" = one task per loop, "phase" = one phase per loop -} - -export interface IterationLogEntry { - iteration: number; - timestamp: string; // ISO timestamp - duration: number; // milliseconds - exitCode: number; - status: 'running' | 'completed' | 'error' | 'timeout' | 'interrupted' | 'blocked'; - completionMarker?: string; -} - -export type SessionState = 'new' | 'continue' | 'changed'; - -export const DEFAULT_CONFIG: Partial = { - maxIterations: 100, - timeout: 3, - maxRetries: 5, - verbose: false, - currentIteration: 0, - loopMode: 'task', -}; +export type LoopMode = 'task' | 'phase'; + +export interface SessionConfig { + agent: string; // "claude-code" | "copilot-cli" | "devin-cli" + model: string; // Selected model + maxIterations: number; // 5-50 + timeout: number; // Base timeout in minutes per iteration attempt + maxRetries: number; // Max retry attempts per iteration (timeout increments by 30s each retry) + verbose: boolean; + specPath: string; // Path to the spec directory + startedAt: string; // ISO timestamp + currentIteration: number; + jiraTicketId?: string; // JIRA ticket ID for commit messages + loopMode: LoopMode; // "task" = one task per loop, "phase" = one phase per loop +} + +export interface IterationLogEntry { + iteration: number; + timestamp: string; // ISO timestamp + duration: number; // milliseconds + exitCode: number; + status: 'running' | 'completed' | 'error' | 'timeout' | 'interrupted' | 'blocked'; + completionMarker?: string; +} + +export type SessionState = 'new' | 'continue' | 'changed'; + +export const DEFAULT_CONFIG: Partial = { + maxIterations: 100, + timeout: 3, + maxRetries: 5, + verbose: false, + currentIteration: 0, + loopMode: 'task', +};