mirror of
https://github.com/jparkerweb/plan2code.git
synced 2026-09-17 16:22:23 -07:00
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:
@@ -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
@@ -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"
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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',
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user