This commit is contained in:
2026-02-18 15:16:11 -08:00
parent 6f98f6bd7f
commit b7b2595fe1
23 changed files with 331 additions and 776 deletions
+5 -5
View File
@@ -23,11 +23,11 @@ async function selectSpec(cwd: string = process.cwd()): Promise<string | null> {
logger.info('');
logger.info('To get started:');
logger.info('');
logger.info('1. Create a spec using `smarsh2code-1--plan`');
logger.info('1. Create a spec using `plan2code-1--plan`');
logger.info(' command in our AI Agent');
logger.info('');
logger.info('2. Come back here and run `smarsh2code-loop`');
logger.info(' as an alternative to `smarsh2code-3--implement`');
logger.info('2. Come back here and run `plan2code-loop`');
logger.info(' as an alternative to `plan2code-3--implement`');
logger.info('');
return null;
}
@@ -67,8 +67,6 @@ async function selectSpec(cwd: string = process.cwd()): Promise<string | null> {
async function selectAgent(): Promise<string> {
const allAgents = agentRegistry.getAll();
const agentName = await select({
message: 'Select AI agent:',
choices: allAgents.map((agent) => ({
@@ -108,6 +106,7 @@ async function selectMaxIterations(): Promise<number> {
return max;
}
/**
* Select loop mode: one task per loop or one phase per loop
*/
@@ -126,6 +125,7 @@ async function selectLoopMode(): Promise<LoopMode> {
],
default: 'task',
});
return mode;
}
+61 -53
View File
@@ -142,12 +142,14 @@ export class Controller {
logger.info(`Please ensure the "${this.agent.config.command}" command is installed and available in your PATH.`);
throw new Error(`Agent "${this.agent.config.displayName}" is not available. Please install it and try again.`);
}
// Ensure git repo and .gitignore are set up before any iterations
const gitReady = await ensureGitRepo(process.cwd());
if (!gitReady) {
throw new Error('Failed to initialize a git repository in the current working directory. Cannot start Plan2Code Loop.');
}
ensureGitignore(process.cwd());
const loopModeLabel = (this.config.loopMode || 'task') === 'phase' ? 'One phase per loop' : 'One task per loop';
logger.header('Starting Plan2Code Loop');
logger.info(`Agent: ${this.agent.config.displayName}`);
@@ -215,9 +217,11 @@ export class Controller {
// Branch completion handling based on loop mode
const isPhaseMode = (this.config.loopMode || 'task') === 'phase';
if (isPhaseMode) {
// Phase mode: parse ALL completion markers from output
const allCompletions = checkForAllCompletions(result.stdout + result.stderr);
// Determine status
let status: IterationLogEntry['status'] = 'running';
if (allCompletions.tasks.length > 0 || allCompletions.loopComplete || allCompletions.phaseComplete) {
@@ -229,10 +233,12 @@ export class Controller {
} else if (result.exitCode !== 0) {
status = 'error';
}
// Log iteration with count of tasks
const markerSummary = allCompletions.tasks.map(t => `${t.marker}: ${t.taskId}`).join(', ');
const logEntry = this.createLogEntry(result, status, markerSummary || undefined);
await this.stateManager.appendIterationLog(logEntry);
// Display each completed task
const duration = Math.round(result.duration / 1000);
for (const task of allCompletions.tasks) {
@@ -259,6 +265,7 @@ export class Controller {
logger.warning(blockInfo);
}
}
// Show phase-level summary
if (allCompletions.tasks.length > 0) {
const completedCount = allCompletions.tasks.filter(t => t.marker === 'TASK_COMPLETE').length;
@@ -273,6 +280,7 @@ export class Controller {
} else {
logger.iteration(iterNum, this.config.maxIterations, `completed in ${duration}s`);
}
// Verbose output
if (this.config.verbose) {
console.log();
@@ -287,6 +295,7 @@ export class Controller {
} else if (result.exitCode !== 0 && result.stderr.trim()) {
logger.error(` ${result.stderr.trim().split('\n')[0]}`);
}
// Handle LOOP_COMPLETE
if (allCompletions.loopComplete) {
this.onLoopComplete?.();
@@ -302,42 +311,41 @@ export class Controller {
}
} else {
// Task mode (default): existing single-marker logic
const completion = checkForCompletion(result.stdout + result.stderr);
const completion = checkForCompletion(result.stdout + result.stderr);
// Determine status
let status: IterationLogEntry['status'] = 'running';
if (completion.completed) {
if (completion.marker === 'TASK_BLOCKED') {
status = 'blocked';
} else {
status = 'completed';
// Determine status
let status: IterationLogEntry['status'] = 'running';
if (completion.completed) {
if (completion.marker === 'TASK_BLOCKED') {
status = 'blocked';
} else {
status = 'completed';
}
} else if (result.timedOut) {
status = 'timeout';
} else if (result.exitCode !== 0) {
status = 'error';
}
} else if (result.timedOut) {
status = 'timeout';
} else if (result.exitCode !== 0) {
status = 'error';
}
// Log iteration
const logEntry = this.createLogEntry(result, status, completion.marker);
await this.stateManager.appendIterationLog(logEntry);
// Log iteration
const logEntry = this.createLogEntry(result, status, completion.marker);
await this.stateManager.appendIterationLog(logEntry);
// Display iteration result with task info from completion marker
this.displayIterationResult(result, iterNum, completion);
// Display iteration result with task info from completion marker
this.displayIterationResult(result, iterNum, completion);
// Handle completion markers
if (completion.completed) {
const taskDisplay = this.formatTaskDisplay(completion);
// Handle completion markers
if (completion.completed) {
const taskDisplay = this.formatTaskDisplay(completion);
if (completion.marker === 'TASK_COMPLETE') {
this.tasksCompleted++;
await this.onTaskComplete?.({
marker: completion.marker,
taskId: completion.taskId,
taskName: completion.taskName,
});
logger.success(taskDisplay ? `Completed: ${taskDisplay}` : 'Task completed!');
if (completion.marker === 'TASK_COMPLETE') {
this.tasksCompleted++;
await this.onTaskComplete?.({
marker: completion.marker,
taskId: completion.taskId,
taskName: completion.taskName,
});
logger.success(taskDisplay ? `Completed: ${taskDisplay}` : 'Task completed!');
} else if (completion.marker === 'PREREQ_COMPLETE' || completion.marker === 'PREREQ_ASSUMED') {
this.prereqsCompleted++;
await this.onTaskComplete?.({
@@ -350,29 +358,29 @@ export class Controller {
: completion.taskId ? `Prereq ${completion.taskId}` : 'Prerequisite';
const verb = completion.marker === 'PREREQ_COMPLETE' ? 'Verified' : 'Assumed';
logger.success(`${verb}: ${prereqDisplay}`);
} else if (completion.marker === 'TASK_BLOCKED') {
const blockInfo = completion.taskId
? `Task ${completion.taskId} blocked: ${completion.reason || 'Unknown reason'}`
: `Task blocked: ${completion.reason || 'Unknown reason'}`;
logger.warning(blockInfo);
} else if (completion.marker === 'LOOP_COMPLETE') {
// Commit any final changes before completing
this.tasksCompleted++;
await this.onTaskComplete?.({
marker: completion.marker,
taskId: completion.taskId,
taskName: completion.taskName || 'Final implementation complete',
});
this.onLoopComplete?.();
logger.success('All tasks complete!');
return {
completed: true,
iterations: iterNum,
finalMarker: 'LOOP_COMPLETE',
exitReason: 'all_complete',
tasksCompleted: this.tasksCompleted,
} else if (completion.marker === 'TASK_BLOCKED') {
const blockInfo = completion.taskId
? `Task ${completion.taskId} blocked: ${completion.reason || 'Unknown reason'}`
: `Task blocked: ${completion.reason || 'Unknown reason'}`;
logger.warning(blockInfo);
} else if (completion.marker === 'LOOP_COMPLETE') {
// Commit any final changes before completing
this.tasksCompleted++;
await this.onTaskComplete?.({
marker: completion.marker,
taskId: completion.taskId,
taskName: completion.taskName || 'Final implementation complete',
});
this.onLoopComplete?.();
logger.success('All tasks complete!');
return {
completed: true,
iterations: iterNum,
finalMarker: 'LOOP_COMPLETE',
exitReason: 'all_complete',
tasksCompleted: this.tasksCompleted,
prereqsCompleted: this.prereqsCompleted,
};
};
}
}
}
@@ -394,7 +402,7 @@ export class Controller {
? checkForAllCompletions(result.stdout + result.stderr).tasks.length > 0
: checkForCompletion(result.stdout + result.stderr).completed;
if (!hasCompletions) {
logger.warning(`Iteration ${iterNum} exited with code ${result.exitCode}, continuing...`);
logger.warning(`Iteration ${iterNum} exited with code ${result.exitCode}, continuing...`);
}
}
+3 -1
View File
@@ -1,4 +1,4 @@
export const LOOP_PROMPT_TEMPLATE = `# PLAN-LOOP: Autonomous Task Implementation
export const LOOP_PROMPT_TEMPLATE = `# PLAN2CODE-LOOP: Autonomous Task Implementation
## CRITICAL CONSTRAINT
**IMPLEMENT EXACTLY ONE TASK PER ITERATION.**
@@ -83,7 +83,9 @@ If key patterns or learnings were discovered, update \`./AGENTS.md\` if it exist
Remember: ONE TASK ONLY. Find it, implement it, mark it done, output TASK_COMPLETE with the task ID and description, then stop.
`;
export const LOOP_PROMPT_TEMPLATE_PHASE = `# PLAN2CODE-LOOP: Autonomous Phase Implementation
## CRITICAL CONSTRAINT
**IMPLEMENT ALL REMAINING TASKS IN THE CURRENT PHASE.**
Find the first incomplete phase, then implement every remaining task in that phase before stopping.
+1
View File
@@ -1,4 +1,5 @@
export type LoopMode = 'task' | 'phase';
export interface SessionConfig {
agent: string; // "claude-code" | "copilot-cli"
model: string; // Selected model
+10
View File
@@ -54,6 +54,7 @@ export function checkForCompletion(output: string): CompletionCheckResult {
taskName: prereqMatch[2].trim(),
};
}
// Check for PREREQ_ASSUMED with prereq info
// Format: PREREQ_ASSUMED: P2.1 - Design approval (cannot verify)
const assumedMatch = output.match(/PREREQ_ASSUMED[:\[\s]+([^\]:\-\s]+)[\]:\-\s]+(.+?)(?:\n|$)/i);
@@ -65,6 +66,7 @@ export function checkForCompletion(output: string): CompletionCheckResult {
taskName: assumedMatch[2].trim(),
};
}
// Check for TASK_BLOCKED with task info and reason
// Format: TASK_BLOCKED: 1.1 - Reason why blocked
const blockedWithTaskMatch = output.match(/TASK_BLOCKED[:\[\s]+(\d+\.\d+)[\]:\-\s]+(.+?)(?:\n|$)/i);
@@ -94,6 +96,7 @@ export function checkForCompletion(output: string): CompletionCheckResult {
return { completed: false };
}
/**
* Extract ALL completion markers from output (for phase mode).
* Returns an array of all TASK_COMPLETE/TASK_BLOCKED markers found,
@@ -107,12 +110,15 @@ export function checkForAllCompletions(output: string): {
const tasks: CompletionCheckResult[] = [];
let loopComplete = false;
let phaseComplete = false;
if (output.includes('LOOP_COMPLETE')) {
loopComplete = true;
}
if (output.includes('PHASE_COMPLETE')) {
phaseComplete = true;
}
// Find all TASK_COMPLETE markers with task info
// Format: TASK_COMPLETE: 1.1 - Task description
const completeRegex = /TASK_COMPLETE[:\[\s]+(\d+\.\d+)[\]:\-\s]+(.+?)(?:\n|$)/gi;
@@ -125,6 +131,7 @@ export function checkForAllCompletions(output: string): {
taskName: match[2].trim(),
});
}
// Find all TASK_BLOCKED markers with task info
const blockedRegex = /TASK_BLOCKED[:\[\s]+(\d+\.\d+)[\]:\-\s]+(.+?)(?:\n|$)/gi;
while ((match = blockedRegex.exec(output)) !== null) {
@@ -135,6 +142,7 @@ export function checkForAllCompletions(output: string): {
reason: match[2].trim(),
});
}
// Find PREREQ_COMPLETE markers
const prereqRegex = /PREREQ_COMPLETE[:\[\s]+([^\]:\-\s]+)[\]:\-\s]+(.+?)(?:\n|$)/gi;
while ((match = prereqRegex.exec(output)) !== null) {
@@ -145,6 +153,7 @@ export function checkForAllCompletions(output: string): {
taskName: match[2].trim(),
});
}
// Find PREREQ_ASSUMED markers
const assumedRegex = /PREREQ_ASSUMED[:\[\s]+([^\]:\-\s]+)[\]:\-\s]+(.+?)(?:\n|$)/gi;
while ((match = assumedRegex.exec(output)) !== null) {
@@ -155,5 +164,6 @@ export function checkForAllCompletions(output: string): {
taskName: match[2].trim(),
});
}
return { tasks, loopComplete, phaseComplete };
}
+1
View File
@@ -1,6 +1,7 @@
export { logger, MASCOT, type Logger } from './logger.js';
export {
checkForCompletion,
checkForAllCompletions,
type CompletionMarker,
type CompletionCheckResult
} from './completion.js';