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
This commit is contained in:
2026-08-08 12:39:28 -07:00
parent 09aa565559
commit 161e424632
6 changed files with 232 additions and 144 deletions
+4 -2
View File
@@ -109,6 +109,7 @@ The scratchpad is managed by the LLM itself - after each task, the AI appends no
|-------|--------| |-------|--------|
| Claude Code | Supported | | Claude Code | Supported |
| GitHub Copilot CLI | Supported | | GitHub Copilot CLI | Supported |
| Devin CLI | Supported |
The loop uses your configured default model for each agent. The loop uses your configured default model for each agent.
@@ -119,7 +120,7 @@ $ plan2code-loop
╭──────────────────────────────────────╮ ╭──────────────────────────────────────╮
│ │ │ │
│ 🔮 Plany's Loop │ │ 🔮 Planny's Loop
│ Autonomous Implementation │ │ Autonomous Implementation │
│ │ │ │
╰──────────────────────────────────────╯ ╰──────────────────────────────────────╯
@@ -196,7 +197,8 @@ src/
├── cli.ts # Interactive prompts ├── cli.ts # Interactive prompts
├── agents/ # Agent implementations ├── agents/ # Agent implementations
│ ├── claude-code.ts │ ├── claude-code.ts
── copilot-cli.ts ── copilot-cli.ts
│ └── devin-cli.ts
├── prompt/ # Prompt building ├── prompt/ # Prompt building
│ ├── templates.ts │ ├── templates.ts
│ └── builder.ts │ └── builder.ts
+47 -46
View File
@@ -1,46 +1,47 @@
{ {
"name": "plan2code-loop", "name": "plan2code-loop",
"version": "1.6.2", "version": "1.6.2",
"description": "Plan2Code Loop - Autonomous spec-driven implementation CLI", "description": "Plan2Code Loop - Autonomous spec-driven implementation CLI",
"type": "module", "type": "module",
"main": "dist/index.js", "main": "dist/index.js",
"bin": { "bin": {
"plan2code-loop": "./dist/bin/plan2code-loop.js" "plan2code-loop": "./dist/bin/plan2code-loop.js"
}, },
"files": [ "files": [
"dist" "dist"
], ],
"engines": { "engines": {
"node": ">=18.0.0" "node": ">=18.0.0"
}, },
"keywords": [ "keywords": [
"cli", "cli",
"ai", "ai",
"agent", "agent",
"automation", "automation",
"claude", "claude",
"copilot", "copilot",
"spec-driven" "devin",
], "spec-driven"
"license": "MIT", ],
"scripts": { "license": "MIT",
"build": "tsup", "scripts": {
"dev": "tsup --watch", "build": "tsup",
"start": "node dist/bin/plan2code-loop.js", "dev": "tsup --watch",
"prepublishOnly": "npm run build" "start": "node dist/bin/plan2code-loop.js",
}, "prepublishOnly": "npm run build"
"dependencies": { },
"@inquirer/prompts": "^8.1.0", "dependencies": {
"chalk": "^5.6.2", "@inquirer/prompts": "^8.1.0",
"execa": "^9.6.1", "chalk": "^5.6.2",
"fs-extra": "^11.3.3", "execa": "^9.6.1",
"ora": "^9.0.0" "fs-extra": "^11.3.3",
}, "ora": "^9.0.0"
"devDependencies": { },
"@types/fs-extra": "^11.0.4", "devDependencies": {
"@types/node": "^25.0.3", "@types/fs-extra": "^11.0.4",
"tsup": "^8.5.1", "@types/node": "^25.0.3",
"typescript": "^5.9.3" "tsup": "^8.5.1",
} "typescript": "^5.9.3"
} }
}
+81
View File
@@ -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<AgentExecutionResult> {
// 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 <path>
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<boolean> {
// 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();
+22 -19
View File
@@ -1,19 +1,22 @@
export type { export type {
Agent, Agent,
AgentConfig, AgentConfig,
AgentExecutionOptions, AgentExecutionOptions,
AgentExecutionResult, AgentExecutionResult,
ModelOption, ModelOption,
} from './types.js'; } from './types.js';
export { agentRegistry } from './registry.js'; export { agentRegistry } from './registry.js';
export { claudeCodeAgent } from './claude-code.js'; export { claudeCodeAgent } from './claude-code.js';
export { copilotCliAgent } from './copilot-cli.js'; export { copilotCliAgent } from './copilot-cli.js';
export { devinCliAgent } from './devin-cli.js';
// Register all agents
import { agentRegistry } from './registry.js'; // Register all agents
import { claudeCodeAgent } from './claude-code.js'; import { agentRegistry } from './registry.js';
import { copilotCliAgent } from './copilot-cli.js'; import { claudeCodeAgent } from './claude-code.js';
import { copilotCliAgent } from './copilot-cli.js';
agentRegistry.register(claudeCodeAgent); import { devinCliAgent } from './devin-cli.js';
agentRegistry.register(copilotCliAgent);
agentRegistry.register(claudeCodeAgent);
agentRegistry.register(copilotCliAgent);
agentRegistry.register(devinCliAgent);
+43 -42
View File
@@ -1,42 +1,43 @@
export interface ModelOption { export interface ModelOption {
value: string; value: string;
label: string; label: string;
} }
export interface AgentConfig { export interface AgentConfig {
name: string; name: string;
displayName: string; displayName: string;
command: string; command: string;
models: ModelOption[]; models: ModelOption[];
defaultModel: string; defaultModel: string;
flags: { flags: {
prompt: string; prompt: string;
model: string; model: string;
skipPermissions: string; skipPermissions: string;
silent?: string; silent?: string;
}; promptFile?: string;
} };
}
export interface AgentExecutionOptions {
prompt: string; export interface AgentExecutionOptions {
model: string; prompt: string;
timeout: number; // milliseconds model: string;
verbose: boolean; timeout: number; // milliseconds
cwd: string; verbose: boolean;
signal?: AbortSignal; // For cancellation cwd: string;
} signal?: AbortSignal; // For cancellation
}
export interface AgentExecutionResult {
stdout: string; export interface AgentExecutionResult {
stderr: string; stdout: string;
exitCode: number; stderr: string;
timedOut: boolean; exitCode: number;
cancelled: boolean; timedOut: boolean;
duration: number; // milliseconds cancelled: boolean;
} duration: number; // milliseconds
}
export interface Agent {
config: AgentConfig; export interface Agent {
execute(options: AgentExecutionOptions): Promise<AgentExecutionResult>; config: AgentConfig;
isAvailable(): Promise<boolean>; execute(options: AgentExecutionOptions): Promise<AgentExecutionResult>;
} isAvailable(): Promise<boolean>;
}
+35 -35
View File
@@ -1,35 +1,35 @@
export type LoopMode = 'task' | 'phase'; export type LoopMode = 'task' | 'phase';
export interface SessionConfig { export interface SessionConfig {
agent: string; // "claude-code" | "copilot-cli" agent: string; // "claude-code" | "copilot-cli" | "devin-cli"
model: string; // Selected model model: string; // Selected model
maxIterations: number; // 5-50 maxIterations: number; // 5-50
timeout: number; // Base timeout in minutes per iteration attempt timeout: number; // Base timeout in minutes per iteration attempt
maxRetries: number; // Max retry attempts per iteration (timeout increments by 30s each retry) maxRetries: number; // Max retry attempts per iteration (timeout increments by 30s each retry)
verbose: boolean; verbose: boolean;
specPath: string; // Path to the spec directory specPath: string; // Path to the spec directory
startedAt: string; // ISO timestamp startedAt: string; // ISO timestamp
currentIteration: number; currentIteration: number;
jiraTicketId?: string; // JIRA ticket ID for commit messages jiraTicketId?: string; // JIRA ticket ID for commit messages
loopMode: LoopMode; // "task" = one task per loop, "phase" = one phase per loop loopMode: LoopMode; // "task" = one task per loop, "phase" = one phase per loop
} }
export interface IterationLogEntry { export interface IterationLogEntry {
iteration: number; iteration: number;
timestamp: string; // ISO timestamp timestamp: string; // ISO timestamp
duration: number; // milliseconds duration: number; // milliseconds
exitCode: number; exitCode: number;
status: 'running' | 'completed' | 'error' | 'timeout' | 'interrupted' | 'blocked'; status: 'running' | 'completed' | 'error' | 'timeout' | 'interrupted' | 'blocked';
completionMarker?: string; completionMarker?: string;
} }
export type SessionState = 'new' | 'continue' | 'changed'; export type SessionState = 'new' | 'continue' | 'changed';
export const DEFAULT_CONFIG: Partial<SessionConfig> = { export const DEFAULT_CONFIG: Partial<SessionConfig> = {
maxIterations: 100, maxIterations: 100,
timeout: 3, timeout: 3,
maxRetries: 5, maxRetries: 5,
verbose: false, verbose: false,
currentIteration: 0, currentIteration: 0,
loopMode: 'task', loopMode: 'task',
}; };