mirror of
https://github.com/jparkerweb/plan2code.git
synced 2026-07-21 18:33:22 -07:00
v1.5.4
This commit is contained in:
+35
-24
@@ -1,7 +1,7 @@
|
||||
import path from 'path';
|
||||
import { confirm, input, select } from '@inquirer/prompts';
|
||||
import { agentRegistry } from './agents/index.js';
|
||||
import { StateManager, type SessionConfig } from './state/index.js';
|
||||
import { StateManager, type SessionConfig, type LoopMode } from './state/index.js';
|
||||
import { detectSpecDirectories, getSpecProgress } from './spec/utils.js';
|
||||
import { logger } from './utils/index.js';
|
||||
|
||||
@@ -22,9 +22,13 @@ async function selectSpec(cwd: string = process.cwd()): Promise<string | null> {
|
||||
logger.info('Expected: specs/<feature>/overview.md');
|
||||
logger.info('');
|
||||
logger.info('To get started:');
|
||||
logger.info('1. Create a spec directory: mkdir -p specs/my-feature');
|
||||
logger.info('2. Create overview.md with phases listed as checkboxes');
|
||||
logger.info('3. Create phase-1.md, phase-2.md, etc. with task checkboxes');
|
||||
logger.info('');
|
||||
logger.info('1. Create a spec using `smarsh2code-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('');
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -61,31 +65,13 @@ async function selectSpec(cwd: string = process.cwd()): Promise<string | null> {
|
||||
* Select AI agent
|
||||
*/
|
||||
async function selectAgent(): Promise<string> {
|
||||
const availableAgents = await agentRegistry.getAvailable();
|
||||
const allAgents = agentRegistry.getAll();
|
||||
|
||||
if (availableAgents.length === 0) {
|
||||
logger.error('No AI agents detected on your system!');
|
||||
console.log();
|
||||
logger.info('Please install at least one of the following:');
|
||||
console.log();
|
||||
logger.bold('Claude Code:');
|
||||
logger.dim(' npm install -g @anthropic-ai/claude-code');
|
||||
console.log();
|
||||
logger.bold('GitHub Copilot CLI:');
|
||||
logger.dim(' gh extension install github/gh-copilot');
|
||||
console.log();
|
||||
throw new Error('No agents available. Please install an AI agent and try again.');
|
||||
}
|
||||
|
||||
if (availableAgents.length === 1) {
|
||||
const agent = availableAgents[0];
|
||||
logger.info(`Using ${agent.config.displayName} (only available agent)`);
|
||||
return agent.config.name;
|
||||
}
|
||||
|
||||
const agentName = await select({
|
||||
message: 'Select AI agent:',
|
||||
choices: availableAgents.map((agent) => ({
|
||||
choices: allAgents.map((agent) => ({
|
||||
name: agent.config.displayName,
|
||||
value: agent.config.name,
|
||||
})),
|
||||
@@ -122,6 +108,26 @@ async function selectMaxIterations(): Promise<number> {
|
||||
|
||||
return max;
|
||||
}
|
||||
/**
|
||||
* Select loop mode: one task per loop or one phase per loop
|
||||
*/
|
||||
async function selectLoopMode(): Promise<LoopMode> {
|
||||
const mode = await select<LoopMode>({
|
||||
message: 'Tasks per loop iteration:',
|
||||
choices: [
|
||||
{
|
||||
name: 'One task per loop (default)',
|
||||
value: 'task' as LoopMode,
|
||||
},
|
||||
{
|
||||
name: 'One phase per loop (related tasks together)',
|
||||
value: 'phase' as LoopMode,
|
||||
},
|
||||
],
|
||||
default: 'task',
|
||||
});
|
||||
return mode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle existing session - returns action to take
|
||||
@@ -195,6 +201,9 @@ export async function setupSession(
|
||||
if (sessionAction === 'continue') {
|
||||
const existingConfig = await stateManager.readConfig();
|
||||
if (existingConfig) {
|
||||
const agent = await selectAgent();
|
||||
existingConfig.agent = agent;
|
||||
await stateManager.writeConfig(existingConfig);
|
||||
logger.info('Resuming previous session...');
|
||||
return { config: existingConfig, isResume: true };
|
||||
}
|
||||
@@ -207,6 +216,7 @@ export async function setupSession(
|
||||
// Collect new session configuration
|
||||
const jiraTicketId = await promptJiraTicketId();
|
||||
const agent = await selectAgent();
|
||||
const loopMode = await selectLoopMode();
|
||||
const maxIterations = await selectMaxIterations();
|
||||
|
||||
const config: SessionConfig = {
|
||||
@@ -219,6 +229,7 @@ export async function setupSession(
|
||||
startedAt: new Date().toISOString(),
|
||||
currentIteration: 0,
|
||||
jiraTicketId,
|
||||
loopMode,
|
||||
};
|
||||
|
||||
// Initialize session
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { agentRegistry, type Agent, type AgentExecutionResult } from './agents/index.js';
|
||||
import { StateManager, type SessionConfig, type IterationLogEntry } from './state/index.js';
|
||||
import { buildLoopPrompt } from './prompt/index.js';
|
||||
import { checkForCompletion, logger, type CompletionCheckResult } from './utils/index.js';
|
||||
import { checkForCompletion, checkForAllCompletions, logger, ensureGitRepo, ensureGitignore, type CompletionCheckResult } from './utils/index.js';
|
||||
|
||||
export interface TaskCompleteInfo {
|
||||
marker: string;
|
||||
@@ -23,6 +23,7 @@ export interface LoopResult {
|
||||
finalMarker?: string;
|
||||
exitReason: 'all_complete' | 'max_iterations' | 'interrupted' | 'error';
|
||||
tasksCompleted: number;
|
||||
prereqsCompleted: number;
|
||||
error?: Error;
|
||||
}
|
||||
|
||||
@@ -36,6 +37,7 @@ export class Controller {
|
||||
private interrupted = false;
|
||||
private abortController: AbortController | null = null;
|
||||
private tasksCompleted = 0;
|
||||
private prereqsCompleted = 0;
|
||||
|
||||
constructor(options: ControllerOptions) {
|
||||
this.config = options.config;
|
||||
@@ -57,6 +59,8 @@ export class Controller {
|
||||
iteration: this.config.currentIteration + 1,
|
||||
maxIterations: this.config.maxIterations,
|
||||
stateManager: this.stateManager,
|
||||
loopMode: this.config.loopMode || 'task',
|
||||
jiraTicketId: this.config.jiraTicketId,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -131,10 +135,25 @@ export class Controller {
|
||||
}
|
||||
|
||||
async run(): Promise<LoopResult> {
|
||||
// Pre-flight: verify agent CLI is available
|
||||
const isAvailable = await this.agent.isAvailable();
|
||||
if (!isAvailable) {
|
||||
logger.error(`"${this.agent.config.displayName}" is not available!`);
|
||||
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}`);
|
||||
logger.info(`Model: ${this.config.model}`);
|
||||
logger.info(`Spec: ${this.config.specPath}`);
|
||||
logger.info(`Loop mode: ${loopModeLabel}`);
|
||||
logger.info(`Max iterations: ${this.config.maxIterations}`);
|
||||
console.log();
|
||||
|
||||
@@ -145,6 +164,7 @@ export class Controller {
|
||||
iterations: this.config.currentIteration,
|
||||
exitReason: 'interrupted',
|
||||
tasksCompleted: this.tasksCompleted,
|
||||
prereqsCompleted: this.prereqsCompleted,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -157,13 +177,13 @@ export class Controller {
|
||||
// Build prompt - simple, just spec path and iteration info
|
||||
const prompt = await this.buildPrompt();
|
||||
|
||||
const spinner = logger.spinner('Waiting for agent response');
|
||||
const spinner = logger.spinner('Waiting for AI Agent response (please be patient)');
|
||||
const startTime = Date.now();
|
||||
|
||||
// Update spinner with elapsed time every second
|
||||
const elapsedInterval = setInterval(() => {
|
||||
const elapsed = Math.round((Date.now() - startTime) / 1000);
|
||||
spinner.text = `Waiting for agent response (${elapsed}s)`;
|
||||
spinner.text = `Waiting for AI Agent response (please be patient) ... (${elapsed}s)`;
|
||||
}, 1000);
|
||||
|
||||
try {
|
||||
@@ -189,10 +209,99 @@ export class Controller {
|
||||
iterations: this.config.currentIteration,
|
||||
exitReason: 'interrupted',
|
||||
tasksCompleted: this.tasksCompleted,
|
||||
prereqsCompleted: this.prereqsCompleted,
|
||||
};
|
||||
}
|
||||
|
||||
// Check for completion markers in output (now includes task info)
|
||||
// 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) {
|
||||
const hasBlocked = allCompletions.tasks.some(t => t.marker === 'TASK_BLOCKED');
|
||||
const hasCompleted = allCompletions.tasks.some(t => t.marker === 'TASK_COMPLETE' || t.marker === 'PREREQ_COMPLETE' || t.marker === 'PREREQ_ASSUMED');
|
||||
status = hasCompleted || allCompletions.phaseComplete || allCompletions.loopComplete ? 'completed' : hasBlocked ? 'blocked' : 'running';
|
||||
} else if (result.timedOut) {
|
||||
status = 'timeout';
|
||||
} 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) {
|
||||
if (task.marker === 'TASK_COMPLETE') {
|
||||
this.tasksCompleted++;
|
||||
const taskDisplay = this.formatTaskDisplay(task);
|
||||
logger.success(taskDisplay ? `Completed: ${taskDisplay}` : 'Task completed!');
|
||||
} else if (task.marker === 'PREREQ_COMPLETE') {
|
||||
this.prereqsCompleted++;
|
||||
const prereqDisplay = task.taskId && task.taskName
|
||||
? `Prereq ${task.taskId}: ${task.taskName}`
|
||||
: task.taskId ? `Prereq ${task.taskId}` : 'Prerequisite';
|
||||
logger.success(`Verified: ${prereqDisplay}`);
|
||||
} else if (task.marker === 'PREREQ_ASSUMED') {
|
||||
this.prereqsCompleted++;
|
||||
const prereqDisplay = task.taskId && task.taskName
|
||||
? `Prereq ${task.taskId}: ${task.taskName}`
|
||||
: task.taskId ? `Prereq ${task.taskId}` : 'Prerequisite';
|
||||
logger.success(`Assumed: ${prereqDisplay}`);
|
||||
} else if (task.marker === 'TASK_BLOCKED') {
|
||||
const blockInfo = task.taskId
|
||||
? `Task ${task.taskId} blocked: ${task.reason || 'Unknown reason'}`
|
||||
: `Task blocked: ${task.reason || 'Unknown reason'}`;
|
||||
logger.warning(blockInfo);
|
||||
}
|
||||
}
|
||||
// Show phase-level summary
|
||||
if (allCompletions.tasks.length > 0) {
|
||||
const completedCount = allCompletions.tasks.filter(t => t.marker === 'TASK_COMPLETE').length;
|
||||
const prereqCount = allCompletions.tasks.filter(t => t.marker === 'PREREQ_COMPLETE' || t.marker === 'PREREQ_ASSUMED').length;
|
||||
const blockedCount = allCompletions.tasks.filter(t => t.marker === 'TASK_BLOCKED').length;
|
||||
const parts: string[] = [];
|
||||
if (completedCount > 0) parts.push(`${completedCount} task(s) completed`);
|
||||
if (prereqCount > 0) parts.push(`${prereqCount} prereq(s) verified`);
|
||||
if (blockedCount > 0) parts.push(`${blockedCount} blocked`);
|
||||
logger.iteration(iterNum, this.config.maxIterations,
|
||||
`Phase done: ${parts.join(', ')} (${duration}s)`);
|
||||
} else {
|
||||
logger.iteration(iterNum, this.config.maxIterations, `completed in ${duration}s`);
|
||||
}
|
||||
// Verbose output
|
||||
if (this.config.verbose) {
|
||||
console.log();
|
||||
logger.dim('--- Agent Output ---');
|
||||
console.log(result.stdout);
|
||||
if (result.stderr) {
|
||||
logger.dim('--- Agent Stderr ---');
|
||||
console.log(result.stderr);
|
||||
}
|
||||
logger.dim('--- End Output ---');
|
||||
console.log();
|
||||
} else if (result.exitCode !== 0 && result.stderr.trim()) {
|
||||
logger.error(` ${result.stderr.trim().split('\n')[0]}`);
|
||||
}
|
||||
// Handle LOOP_COMPLETE
|
||||
if (allCompletions.loopComplete) {
|
||||
this.onLoopComplete?.();
|
||||
logger.success('All tasks complete!');
|
||||
return {
|
||||
completed: true,
|
||||
iterations: iterNum,
|
||||
finalMarker: 'LOOP_COMPLETE',
|
||||
exitReason: 'all_complete',
|
||||
tasksCompleted: this.tasksCompleted,
|
||||
prereqsCompleted: this.prereqsCompleted,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
// Task mode (default): existing single-marker logic
|
||||
const completion = checkForCompletion(result.stdout + result.stderr);
|
||||
|
||||
// Determine status
|
||||
@@ -216,7 +325,6 @@ export class Controller {
|
||||
// Display iteration result with task info from completion marker
|
||||
this.displayIterationResult(result, iterNum, completion);
|
||||
|
||||
// Note: Scratchpad updates are now handled by the LLM directly
|
||||
|
||||
// Handle completion markers
|
||||
if (completion.completed) {
|
||||
@@ -230,6 +338,18 @@ export class Controller {
|
||||
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?.({
|
||||
marker: completion.marker,
|
||||
taskId: completion.taskId,
|
||||
taskName: completion.taskName,
|
||||
});
|
||||
const prereqDisplay = completion.taskId && completion.taskName
|
||||
? `Prereq ${completion.taskId}: ${completion.taskName}`
|
||||
: 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'}`
|
||||
@@ -251,7 +371,9 @@ export class Controller {
|
||||
finalMarker: 'LOOP_COMPLETE',
|
||||
exitReason: 'all_complete',
|
||||
tasksCompleted: this.tasksCompleted,
|
||||
prereqsCompleted: this.prereqsCompleted,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -266,8 +388,14 @@ export class Controller {
|
||||
}
|
||||
|
||||
// Handle error (but continue - LLM might recover)
|
||||
if (result.exitCode !== 0 && !result.timedOut && !completion.completed) {
|
||||
if (result.exitCode !== 0 && !result.timedOut) {
|
||||
// In phase mode, check if any tasks completed despite error exit code
|
||||
const hasCompletions = isPhaseMode
|
||||
? 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...`);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
@@ -290,6 +418,7 @@ export class Controller {
|
||||
iterations: this.config.currentIteration,
|
||||
exitReason: 'error',
|
||||
tasksCompleted: this.tasksCompleted,
|
||||
prereqsCompleted: this.prereqsCompleted,
|
||||
error: error instanceof Error ? error : new Error(String(error)),
|
||||
};
|
||||
}
|
||||
@@ -302,6 +431,7 @@ export class Controller {
|
||||
iterations: this.config.currentIteration,
|
||||
exitReason: 'max_iterations',
|
||||
tasksCompleted: this.tasksCompleted,
|
||||
prereqsCompleted: this.prereqsCompleted,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -58,6 +58,9 @@ export async function run(): Promise<LoopResult | null> {
|
||||
logger.header('Session Summary');
|
||||
logger.info(`Total iterations: ${loopResult.iterations}`);
|
||||
logger.info(`Tasks completed: ${loopResult.tasksCompleted}`);
|
||||
if (loopResult.prereqsCompleted > 0) {
|
||||
logger.info(`Prerequisites verified: ${loopResult.prereqsCompleted}`);
|
||||
}
|
||||
logger.info(`Exit reason: ${loopResult.exitReason}`);
|
||||
if (loopResult.finalMarker) {
|
||||
logger.info(`Completion marker: ${loopResult.finalMarker}`);
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
import type { StateManager } from '../state/index.js';
|
||||
import { LOOP_PROMPT_TEMPLATE } from './templates.js';
|
||||
import type { StateManager, LoopMode } from '../state/index.js';
|
||||
import { LOOP_PROMPT_TEMPLATE, LOOP_PROMPT_TEMPLATE_PHASE } from './templates.js';
|
||||
|
||||
export interface PromptContext {
|
||||
specPath: string;
|
||||
iteration: number;
|
||||
maxIterations: number;
|
||||
stateManager: StateManager;
|
||||
loopMode: LoopMode;
|
||||
jiraTicketId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the prompt for the AI agent
|
||||
* Simple: just pass spec path and let the LLM discover the next task
|
||||
* Selects template based on loop mode (task vs phase)
|
||||
*/
|
||||
export async function buildLoopPrompt(context: PromptContext): Promise<string> {
|
||||
const { specPath, iteration, maxIterations, stateManager } = context;
|
||||
const { specPath, iteration, maxIterations, stateManager, loopMode, jiraTicketId } = context;
|
||||
|
||||
// Read scratchpad content for session continuity (LLM writes to this)
|
||||
const scratchpadContent = await stateManager.readScratchpad();
|
||||
@@ -21,13 +23,17 @@ export async function buildLoopPrompt(context: PromptContext): Promise<string> {
|
||||
// Project root is where plan2code-loop was invoked from
|
||||
const projectRoot = process.cwd();
|
||||
|
||||
// Simple template substitution
|
||||
const prompt = LOOP_PROMPT_TEMPLATE
|
||||
// Select template based on loop mode
|
||||
const template = loopMode === 'phase' ? LOOP_PROMPT_TEMPLATE_PHASE : LOOP_PROMPT_TEMPLATE;
|
||||
|
||||
// Template substitution
|
||||
const prompt = template
|
||||
.replace(/{{projectRoot}}/g, projectRoot)
|
||||
.replace(/{{specPath}}/g, specPath)
|
||||
.replace(/{{iteration}}/g, iteration.toString())
|
||||
.replace(/{{maxIterations}}/g, maxIterations.toString())
|
||||
.replace(/{{scratchpadContent}}/g, scratchpadContent || '(First iteration - no previous progress)');
|
||||
.replace(/{{scratchpadContent}}/g, scratchpadContent || '(First iteration - no previous progress)')
|
||||
.replace(/{{jiraTicketId}}/g, jiraTicketId || '');
|
||||
|
||||
return prompt;
|
||||
}
|
||||
|
||||
@@ -3,4 +3,4 @@ export {
|
||||
type PromptContext,
|
||||
} from './builder.js';
|
||||
|
||||
export { LOOP_PROMPT_TEMPLATE } from './templates.js';
|
||||
export { LOOP_PROMPT_TEMPLATE, LOOP_PROMPT_TEMPLATE_PHASE } from './templates.js';
|
||||
|
||||
@@ -83,3 +83,102 @@ 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.
|
||||
Complete each task fully before moving to the next task within the phase.
|
||||
## Project Information
|
||||
- **Project Root:** \`{{projectRoot}}\`
|
||||
- **Spec Location:** \`{{specPath}}\`
|
||||
- Read \`AGENTS.md\` for project-specific guidance if available
|
||||
|
||||
## IMPORTANT: File Locations
|
||||
- Write ALL code files relative to the **project root** (\`{{projectRoot}}\`)
|
||||
- The spec directory (\`{{specPath}}\`) is for documentation ONLY - never write code there
|
||||
- Example: Create \`{{projectRoot}}/src/index.ts\`, NOT \`{{specPath}}/src/index.ts\`
|
||||
|
||||
## Iteration
|
||||
{{iteration}} of {{maxIterations}}
|
||||
|
||||
## Phase Discovery Process
|
||||
1. Read \`{{specPath}}/overview.md\` to see all phases
|
||||
2. Find the FIRST phase with an unchecked checkbox (\`- [ ]\` or \`- [/]\`)
|
||||
3. Read that phase's file (e.g., \`phase-1.md\`)
|
||||
4. Check the \`## Prerequisites\` section FIRST
|
||||
5. Complete ALL unchecked prerequisites (\`- [ ]\`) first, in order
|
||||
- Verify/complete each, then mark \`[x]\` or \`[?]\`
|
||||
6. Once ALL prerequisites are complete, implement ALL unchecked tasks in order
|
||||
7. Continue until every task in the phase is marked \`[x]\`
|
||||
|
||||
## Checkbox States
|
||||
- \`[ ]\` = incomplete/pending
|
||||
- \`[x]\` = complete (skip)
|
||||
- \`[?]\` = assumed complete, couldn't verify (skip)
|
||||
- \`[!]\` = blocked (skip, note in scratchpad)
|
||||
|
||||
## Implementation Steps (repeat for EACH task in the phase)
|
||||
1. Read and understand the task
|
||||
2. Implement it completely
|
||||
3. Validate it works (run tests if applicable and double-check code)
|
||||
4. Mark that task's checkbox as \`[x]\` in the phase file
|
||||
5. **Create a git commit** for this task (see Git Policy below)
|
||||
6. Output a TASK_COMPLETE marker for this task
|
||||
7. Move to the next unchecked task in the same phase
|
||||
8. When ALL tasks in the phase are done, mark the phase \`[x]\` in overview.md
|
||||
|
||||
## Git Policy
|
||||
**YOU are responsible for creating git commits after each task.** The orchestration system does NOT handle commits in phase mode.
|
||||
|
||||
After completing each task:
|
||||
\`\`\`bash
|
||||
git add -A
|
||||
git commit -m "<commit message>"
|
||||
\`\`\`
|
||||
|
||||
**Commit message format:**
|
||||
- With JIRA ticket: Use \`-m "Task X.Y: description" -m "{{jiraTicketId}}" -m "AI Assisted"\` (three \`-m\` flags)
|
||||
- Without JIRA ticket: \`-m "Task X.Y: description" -m "AI Assisted"\` (two \`-m\` flags)
|
||||
- ALWAYS include the "AI Assisted" footer as the final \`-m\` flag
|
||||
|
||||
Replace X.Y with the actual task ID and description with a concise summary of what was implemented.
|
||||
|
||||
## Completion Markers (REQUIRED FORMAT)
|
||||
Output one of these **after each task** you complete:
|
||||
|
||||
**PREREQ_COMPLETE: [prereq_id] - [description]**
|
||||
Example: \`PREREQ_COMPLETE: P1.1 - Verified Phase 1 complete\`
|
||||
|
||||
**PREREQ_ASSUMED: [prereq_id] - [description]**
|
||||
Example: \`PREREQ_ASSUMED: P2.1 - Design approval (cannot verify)\`
|
||||
|
||||
**TASK_COMPLETE: [task_id] - [task_description]**
|
||||
Example: \`TASK_COMPLETE: 1.1 - Initialize project structure\`
|
||||
|
||||
**TASK_BLOCKED: [task_id] - [reason]**
|
||||
Example: \`TASK_BLOCKED: 2.3 - Missing API credentials\`
|
||||
If a task is blocked, skip it and continue to the next task.
|
||||
|
||||
After ALL tasks in the phase are complete (or blocked), output:
|
||||
**PHASE_COMPLETE** - if only this phase is done
|
||||
**LOOP_COMPLETE** - if ALL phases in overview.md are now marked complete
|
||||
|
||||
## Scratchpad Management
|
||||
|
||||
After completing each task, append to \`{{specPath}}/.plan2code-loop/scratchpad.md\`:
|
||||
- Task completed and Phase item reference
|
||||
- Key decisions made and reasoning
|
||||
- Files changed
|
||||
- Any blockers or notes for next iteration
|
||||
|
||||
Keep entries concise. Sacrifice grammar for concision. This file helps future iterations skip exploration.
|
||||
|
||||
If key patterns or learnings were discovered, update \`./AGENTS.md\` if it exists.
|
||||
|
||||
## Previous Session Context
|
||||
{{scratchpadContent}}
|
||||
|
||||
---
|
||||
|
||||
Remember: Complete ALL tasks in the current phase. Implement each task, commit it, output TASK_COMPLETE, then continue to the next. Stop only when the phase is done.
|
||||
`;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export type LoopMode = 'task' | 'phase';
|
||||
export interface SessionConfig {
|
||||
agent: string; // "claude-code" | "copilot-cli"
|
||||
model: string; // Selected model
|
||||
@@ -8,6 +9,7 @@ export interface SessionConfig {
|
||||
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 {
|
||||
@@ -26,4 +28,5 @@ export const DEFAULT_CONFIG: Partial<SessionConfig> = {
|
||||
timeout: 30,
|
||||
verbose: false,
|
||||
currentIteration: 0,
|
||||
loopMode: 'task',
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ export {
|
||||
type SessionConfig,
|
||||
type IterationLogEntry,
|
||||
type SessionState,
|
||||
type LoopMode,
|
||||
DEFAULT_CONFIG,
|
||||
} from './config.js';
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// Completion markers for plan2code-loop
|
||||
const COMPLETION_MARKERS = ['TASK_COMPLETE', 'TASK_BLOCKED', 'LOOP_COMPLETE'] as const;
|
||||
const COMPLETION_MARKERS = ['TASK_COMPLETE', 'TASK_BLOCKED', 'LOOP_COMPLETE', 'PREREQ_COMPLETE', 'PREREQ_ASSUMED'] as const;
|
||||
|
||||
export type CompletionMarker = (typeof COMPLETION_MARKERS)[number];
|
||||
|
||||
@@ -43,6 +43,28 @@ export function checkForCompletion(output: string): CompletionCheckResult {
|
||||
};
|
||||
}
|
||||
|
||||
// Check for PREREQ_COMPLETE with prereq info
|
||||
// Format: PREREQ_COMPLETE: P1.1 - Verified Phase 1 complete
|
||||
const prereqMatch = output.match(/PREREQ_COMPLETE[:\[\s]+([^\]:\-\s]+)[\]:\-\s]+(.+?)(?:\n|$)/i);
|
||||
if (prereqMatch) {
|
||||
return {
|
||||
completed: true,
|
||||
marker: 'PREREQ_COMPLETE',
|
||||
taskId: prereqMatch[1],
|
||||
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);
|
||||
if (assumedMatch) {
|
||||
return {
|
||||
completed: true,
|
||||
marker: 'PREREQ_ASSUMED',
|
||||
taskId: assumedMatch[1],
|
||||
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);
|
||||
@@ -72,3 +94,66 @@ 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,
|
||||
* plus whether LOOP_COMPLETE or PHASE_COMPLETE was present.
|
||||
*/
|
||||
export function checkForAllCompletions(output: string): {
|
||||
tasks: CompletionCheckResult[];
|
||||
loopComplete: boolean;
|
||||
phaseComplete: boolean;
|
||||
} {
|
||||
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;
|
||||
let match: RegExpExecArray | null;
|
||||
while ((match = completeRegex.exec(output)) !== null) {
|
||||
tasks.push({
|
||||
completed: true,
|
||||
marker: 'TASK_COMPLETE',
|
||||
taskId: match[1],
|
||||
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) {
|
||||
tasks.push({
|
||||
completed: true,
|
||||
marker: 'TASK_BLOCKED',
|
||||
taskId: match[1],
|
||||
reason: match[2].trim(),
|
||||
});
|
||||
}
|
||||
// Find PREREQ_COMPLETE markers
|
||||
const prereqRegex = /PREREQ_COMPLETE[:\[\s]+([^\]:\-\s]+)[\]:\-\s]+(.+?)(?:\n|$)/gi;
|
||||
while ((match = prereqRegex.exec(output)) !== null) {
|
||||
tasks.push({
|
||||
completed: true,
|
||||
marker: 'PREREQ_COMPLETE',
|
||||
taskId: match[1],
|
||||
taskName: match[2].trim(),
|
||||
});
|
||||
}
|
||||
// Find PREREQ_ASSUMED markers
|
||||
const assumedRegex = /PREREQ_ASSUMED[:\[\s]+([^\]:\-\s]+)[\]:\-\s]+(.+?)(?:\n|$)/gi;
|
||||
while ((match = assumedRegex.exec(output)) !== null) {
|
||||
tasks.push({
|
||||
completed: true,
|
||||
marker: 'PREREQ_ASSUMED',
|
||||
taskId: match[1],
|
||||
taskName: match[2].trim(),
|
||||
});
|
||||
}
|
||||
return { tasks, loopComplete, phaseComplete };
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ export async function ensureGitRepo(cwd: string): Promise<boolean> {
|
||||
/**
|
||||
* Required entries for the .gitignore file
|
||||
*/
|
||||
const REQUIRED_GITIGNORE_ENTRIES = ['specs/', 'specs--completed/', 'nul'];
|
||||
const REQUIRED_GITIGNORE_ENTRIES = ['specs/', 'specs--completed/', 'nul', 'node_modules/'];
|
||||
|
||||
/**
|
||||
* Ensure .gitignore exists with required entries
|
||||
|
||||
@@ -13,5 +13,6 @@ export {
|
||||
createTaskCommit,
|
||||
isGitRepo,
|
||||
ensureGitRepo,
|
||||
ensureGitignore,
|
||||
type GitCommitOptions
|
||||
} from './git.js';
|
||||
|
||||
Reference in New Issue
Block a user