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:
@@ -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 {
|
||||
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);
|
||||
|
||||
@@ -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<AgentExecutionResult>;
|
||||
isAvailable(): Promise<boolean>;
|
||||
}
|
||||
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<AgentExecutionResult>;
|
||||
isAvailable(): Promise<boolean>;
|
||||
}
|
||||
|
||||
@@ -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<SessionConfig> = {
|
||||
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<SessionConfig> = {
|
||||
maxIterations: 100,
|
||||
timeout: 3,
|
||||
maxRetries: 5,
|
||||
verbose: false,
|
||||
currentIteration: 0,
|
||||
loopMode: 'task',
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user