mirror of
https://github.com/jparkerweb/plan2code.git
synced 2026-09-17 16:22:23 -07:00
Pin LF line endings and renormalize the working tree
scripts/validate-char-count.js measures characters on disk, so a CRLF checkout added ~1 char per line and pushed the 11k-budget prompt files over the limit depending on how the repo was cloned. Pins eol=lf, marks binaries, and renormalizes the files that had CRLF. AI Assisted
This commit is contained in:
@@ -1,3 +1,29 @@
|
|||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Line endings
|
||||||
|
#
|
||||||
|
# Git stores LF, and every text file is checked out as LF on all platforms.
|
||||||
|
# This is not cosmetic: scripts/validate-char-count.js measures the characters
|
||||||
|
# actually on disk, so a CRLF checkout adds ~1 character per line. The workflow
|
||||||
|
# prompts in src/plan2code-*.md run close to their 11,000 character budget
|
||||||
|
# (several sit above 10,800), and a CRLF working tree pushes them over — turning
|
||||||
|
# `npm test` into a check that passes or fails depending on how the repo was
|
||||||
|
# cloned. Pinning eol=lf makes the count reproducible everywhere.
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
* text=auto eol=lf
|
||||||
|
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
# Binary — never line-ending-converted, never diffed as text
|
||||||
|
# ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
*.png binary
|
||||||
|
*.jpg binary
|
||||||
|
*.jpeg binary
|
||||||
|
*.gif binary
|
||||||
|
*.ico binary
|
||||||
|
*.mp4 binary
|
||||||
|
*.webm binary
|
||||||
|
*.woff binary
|
||||||
|
*.woff2 binary
|
||||||
|
|
||||||
# The encrypted sync-repo blob is base64 text but must never have its bytes
|
# The encrypted sync-repo blob is base64 text but must never have its bytes
|
||||||
# altered by line-ending normalization. Treat it as binary so autocrlf/eol
|
# altered by line-ending normalization. Treat it as binary so autocrlf/eol
|
||||||
# settings can never corrupt the ciphertext.
|
# settings can never corrupt the ciphertext.
|
||||||
|
|||||||
+16
-16
@@ -1,17 +1,17 @@
|
|||||||
specs/
|
specs/
|
||||||
specs--completed/
|
specs--completed/
|
||||||
dist/
|
dist/
|
||||||
plan2code-loop/dist
|
plan2code-loop/dist
|
||||||
plan2code-loop/node_modules
|
plan2code-loop/node_modules
|
||||||
plan2code-loop/package-lock.json
|
plan2code-loop/package-lock.json
|
||||||
plan2code-metrics/dist
|
plan2code-metrics/dist
|
||||||
plan2code-metrics/node_modules
|
plan2code-metrics/node_modules
|
||||||
plan2code-metrics/package-lock.json
|
plan2code-metrics/package-lock.json
|
||||||
.plan2code-loop
|
.plan2code-loop
|
||||||
.plan2code-metrics
|
.plan2code-metrics
|
||||||
nul
|
nul
|
||||||
.cognition/
|
.cognition/
|
||||||
handoffs/
|
handoffs/
|
||||||
node_modules/
|
node_modules/
|
||||||
package-lock.json
|
package-lock.json
|
||||||
SYNC.md
|
SYNC.md
|
||||||
@@ -1,82 +1,82 @@
|
|||||||
import type { Agent, AgentConfig, AgentExecutionOptions, AgentExecutionResult } from './types.js';
|
import type { Agent, AgentConfig, AgentExecutionOptions, AgentExecutionResult } from './types.js';
|
||||||
import { executeCommand } from '../utils/process.js';
|
import { executeCommand } from '../utils/process.js';
|
||||||
import { writeFileSync, unlinkSync } from 'fs';
|
import { writeFileSync, unlinkSync } from 'fs';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import { tmpdir } from 'os';
|
import { tmpdir } from 'os';
|
||||||
|
|
||||||
const claudeCodeConfig: AgentConfig = {
|
const claudeCodeConfig: AgentConfig = {
|
||||||
name: 'claude-code',
|
name: 'claude-code',
|
||||||
displayName: 'Claude Code',
|
displayName: 'Claude Code',
|
||||||
command: 'claude',
|
command: 'claude',
|
||||||
models: [
|
models: [
|
||||||
{ value: 'default', label: 'Default (use Claude config)' },
|
{ value: 'default', label: 'Default (use Claude config)' },
|
||||||
],
|
],
|
||||||
defaultModel: 'default',
|
defaultModel: 'default',
|
||||||
flags: {
|
flags: {
|
||||||
prompt: '--print',
|
prompt: '--print',
|
||||||
model: '--model',
|
model: '--model',
|
||||||
skipPermissions: '--dangerously-skip-permissions',
|
skipPermissions: '--dangerously-skip-permissions',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
class ClaudeCodeAgent implements Agent {
|
class ClaudeCodeAgent implements Agent {
|
||||||
readonly config = claudeCodeConfig;
|
readonly config = claudeCodeConfig;
|
||||||
|
|
||||||
async execute(options: AgentExecutionOptions): Promise<AgentExecutionResult> {
|
async execute(options: AgentExecutionOptions): Promise<AgentExecutionResult> {
|
||||||
// Write prompt to temp file - more reliable than stdin on Windows
|
// Write prompt to temp file - more reliable than stdin on Windows
|
||||||
const tempFile = join(tmpdir(), `plan2code-prompt-${Date.now()}.txt`);
|
const tempFile = join(tmpdir(), `plan2code-prompt-${Date.now()}.txt`);
|
||||||
writeFileSync(tempFile, options.prompt, 'utf-8');
|
writeFileSync(tempFile, options.prompt, 'utf-8');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Build args: flags first, then read prompt from temp file via shell
|
// Build args: flags first, then read prompt from temp file via shell
|
||||||
const args: string[] = [
|
const args: string[] = [
|
||||||
this.config.flags.prompt, // --print for non-interactive mode
|
this.config.flags.prompt, // --print for non-interactive mode
|
||||||
this.config.flags.skipPermissions,
|
this.config.flags.skipPermissions,
|
||||||
];
|
];
|
||||||
|
|
||||||
// Only add --model if not using default
|
// Only add --model if not using default
|
||||||
if (options.model && options.model !== 'default') {
|
if (options.model && options.model !== 'default') {
|
||||||
args.push(this.config.flags.model, options.model);
|
args.push(this.config.flags.model, options.model);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use stdin from the temp file
|
// Use stdin from the temp file
|
||||||
const result = await executeCommand({
|
const result = await executeCommand({
|
||||||
command: this.config.command,
|
command: this.config.command,
|
||||||
args,
|
args,
|
||||||
cwd: options.cwd,
|
cwd: options.cwd,
|
||||||
timeout: options.timeout,
|
timeout: options.timeout,
|
||||||
signal: options.signal,
|
signal: options.signal,
|
||||||
stdinFile: tempFile,
|
stdinFile: tempFile,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
stdout: result.stdout,
|
stdout: result.stdout,
|
||||||
stderr: result.stderr,
|
stderr: result.stderr,
|
||||||
exitCode: result.exitCode,
|
exitCode: result.exitCode,
|
||||||
timedOut: result.timedOut,
|
timedOut: result.timedOut,
|
||||||
cancelled: result.cancelled,
|
cancelled: result.cancelled,
|
||||||
duration: result.duration,
|
duration: result.duration,
|
||||||
};
|
};
|
||||||
} finally {
|
} finally {
|
||||||
// Clean up temp file
|
// Clean up temp file
|
||||||
try {
|
try {
|
||||||
unlinkSync(tempFile);
|
unlinkSync(tempFile);
|
||||||
} catch {
|
} catch {
|
||||||
// Ignore cleanup errors
|
// Ignore cleanup errors
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async isAvailable(): Promise<boolean> {
|
async isAvailable(): Promise<boolean> {
|
||||||
// Run claude --version to verify it's actually installed and working
|
// Run claude --version to verify it's actually installed and working
|
||||||
const result = await executeCommand({
|
const result = await executeCommand({
|
||||||
command: this.config.command,
|
command: this.config.command,
|
||||||
args: ['--version'],
|
args: ['--version'],
|
||||||
cwd: process.cwd(),
|
cwd: process.cwd(),
|
||||||
timeout: 5000,
|
timeout: 5000,
|
||||||
});
|
});
|
||||||
return result.exitCode === 0;
|
return result.exitCode === 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const claudeCodeAgent = new ClaudeCodeAgent();
|
export const claudeCodeAgent = new ClaudeCodeAgent();
|
||||||
|
|||||||
@@ -1,70 +1,70 @@
|
|||||||
import type { Agent, AgentConfig, AgentExecutionOptions, AgentExecutionResult } from './types.js';
|
import type { Agent, AgentConfig, AgentExecutionOptions, AgentExecutionResult } from './types.js';
|
||||||
import { executeCommand } from '../utils/process.js';
|
import { executeCommand } from '../utils/process.js';
|
||||||
|
|
||||||
const copilotCliConfig: AgentConfig = {
|
const copilotCliConfig: AgentConfig = {
|
||||||
name: 'copilot-cli',
|
name: 'copilot-cli',
|
||||||
displayName: 'GitHub Copilot CLI',
|
displayName: 'GitHub Copilot CLI',
|
||||||
command: 'copilot',
|
command: 'copilot',
|
||||||
models: [
|
models: [
|
||||||
{ value: 'claude-sonnet-4', label: 'Claude Sonnet 4 (Default)' },
|
{ value: 'claude-sonnet-4', label: 'Claude Sonnet 4 (Default)' },
|
||||||
{ value: 'claude-sonnet-4.5', label: 'Claude Sonnet 4.5' },
|
{ value: 'claude-sonnet-4.5', label: 'Claude Sonnet 4.5' },
|
||||||
{ value: 'claude-opus-4.5', label: 'Claude Opus 4.5' },
|
{ value: 'claude-opus-4.5', label: 'Claude Opus 4.5' },
|
||||||
{ value: 'gpt-5', label: 'GPT-5' },
|
{ value: 'gpt-5', label: 'GPT-5' },
|
||||||
{ value: 'gpt-5-mini', label: 'GPT-5 Mini' },
|
{ value: 'gpt-5-mini', label: 'GPT-5 Mini' },
|
||||||
{ value: 'gemini-3-pro-preview', label: 'Gemini 3 Pro' },
|
{ value: 'gemini-3-pro-preview', label: 'Gemini 3 Pro' },
|
||||||
],
|
],
|
||||||
defaultModel: 'claude-sonnet-4',
|
defaultModel: 'claude-sonnet-4',
|
||||||
flags: {
|
flags: {
|
||||||
prompt: '-p',
|
prompt: '-p',
|
||||||
model: '--model',
|
model: '--model',
|
||||||
skipPermissions: '--allow-all-tools',
|
skipPermissions: '--allow-all-tools',
|
||||||
silent: '-s',
|
silent: '-s',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
class CopilotCliAgent implements Agent {
|
class CopilotCliAgent implements Agent {
|
||||||
readonly config = copilotCliConfig;
|
readonly config = copilotCliConfig;
|
||||||
|
|
||||||
async execute(options: AgentExecutionOptions): Promise<AgentExecutionResult> {
|
async execute(options: AgentExecutionOptions): Promise<AgentExecutionResult> {
|
||||||
// Use stdin for prompt to handle multi-line text properly
|
// Use stdin for prompt to handle multi-line text properly
|
||||||
const args: string[] = [];
|
const args: string[] = [];
|
||||||
|
|
||||||
// Only add --model if not using default
|
// Only add --model if not using default
|
||||||
if (options.model && options.model !== 'default') {
|
if (options.model && options.model !== 'default') {
|
||||||
args.push(this.config.flags.model, options.model);
|
args.push(this.config.flags.model, options.model);
|
||||||
}
|
}
|
||||||
|
|
||||||
args.push(this.config.flags.skipPermissions, this.config.flags.silent!);
|
args.push(this.config.flags.skipPermissions, this.config.flags.silent!);
|
||||||
|
|
||||||
const result = await executeCommand({
|
const result = await executeCommand({
|
||||||
command: this.config.command,
|
command: this.config.command,
|
||||||
args,
|
args,
|
||||||
cwd: options.cwd,
|
cwd: options.cwd,
|
||||||
timeout: options.timeout,
|
timeout: options.timeout,
|
||||||
signal: options.signal,
|
signal: options.signal,
|
||||||
stdin: options.prompt,
|
stdin: options.prompt,
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
stdout: result.stdout,
|
stdout: result.stdout,
|
||||||
stderr: result.stderr,
|
stderr: result.stderr,
|
||||||
exitCode: result.exitCode,
|
exitCode: result.exitCode,
|
||||||
timedOut: result.timedOut,
|
timedOut: result.timedOut,
|
||||||
cancelled: result.cancelled,
|
cancelled: result.cancelled,
|
||||||
duration: result.duration,
|
duration: result.duration,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async isAvailable(): Promise<boolean> {
|
async isAvailable(): Promise<boolean> {
|
||||||
// Run copilot --version to verify it's installed
|
// Run copilot --version to verify it's installed
|
||||||
const result = await executeCommand({
|
const result = await executeCommand({
|
||||||
command: this.config.command,
|
command: this.config.command,
|
||||||
args: ['--version'],
|
args: ['--version'],
|
||||||
cwd: process.cwd(),
|
cwd: process.cwd(),
|
||||||
timeout: 5000,
|
timeout: 5000,
|
||||||
});
|
});
|
||||||
return result.exitCode === 0;
|
return result.exitCode === 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const copilotCliAgent = new CopilotCliAgent();
|
export const copilotCliAgent = new CopilotCliAgent();
|
||||||
|
|||||||
@@ -1,34 +1,34 @@
|
|||||||
import type { Agent } from './types.js';
|
import type { Agent } from './types.js';
|
||||||
|
|
||||||
class AgentRegistry {
|
class AgentRegistry {
|
||||||
private agents: Map<string, Agent> = new Map();
|
private agents: Map<string, Agent> = new Map();
|
||||||
|
|
||||||
register(agent: Agent): void {
|
register(agent: Agent): void {
|
||||||
this.agents.set(agent.config.name, agent);
|
this.agents.set(agent.config.name, agent);
|
||||||
}
|
}
|
||||||
|
|
||||||
get(name: string): Agent | undefined {
|
get(name: string): Agent | undefined {
|
||||||
return this.agents.get(name);
|
return this.agents.get(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
getAll(): Agent[] {
|
getAll(): Agent[] {
|
||||||
return Array.from(this.agents.values());
|
return Array.from(this.agents.values());
|
||||||
}
|
}
|
||||||
|
|
||||||
getAvailable(): Promise<Agent[]> {
|
getAvailable(): Promise<Agent[]> {
|
||||||
return Promise.all(
|
return Promise.all(
|
||||||
this.getAll().map(async (agent) => ({
|
this.getAll().map(async (agent) => ({
|
||||||
agent,
|
agent,
|
||||||
available: await agent.isAvailable(),
|
available: await agent.isAvailable(),
|
||||||
}))
|
}))
|
||||||
).then((results) =>
|
).then((results) =>
|
||||||
results.filter((r) => r.available).map((r) => r.agent)
|
results.filter((r) => r.available).map((r) => r.agent)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getNames(): string[] {
|
getNames(): string[] {
|
||||||
return Array.from(this.agents.keys());
|
return Array.from(this.agents.keys());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const agentRegistry = new AgentRegistry();
|
export const agentRegistry = new AgentRegistry();
|
||||||
|
|||||||
@@ -1,34 +1,34 @@
|
|||||||
import { run } from '../index.js';
|
import { run } from '../index.js';
|
||||||
import { logger } from '../utils/index.js';
|
import { logger } from '../utils/index.js';
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
try {
|
try {
|
||||||
const result = await run();
|
const result = await run();
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Exit codes per spec
|
// Exit codes per spec
|
||||||
switch (result.exitReason) {
|
switch (result.exitReason) {
|
||||||
case 'all_complete':
|
case 'all_complete':
|
||||||
logger.success('Loop completed successfully - all tasks done!');
|
logger.success('Loop completed successfully - all tasks done!');
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
case 'max_iterations':
|
case 'max_iterations':
|
||||||
logger.warning('Loop ended: max iterations reached');
|
logger.warning('Loop ended: max iterations reached');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
case 'interrupted':
|
case 'interrupted':
|
||||||
logger.info('Loop interrupted by user');
|
logger.info('Loop interrupted by user');
|
||||||
process.exit(2);
|
process.exit(2);
|
||||||
case 'error':
|
case 'error':
|
||||||
logger.error('Loop ended with error');
|
logger.error('Loop ended with error');
|
||||||
process.exit(3);
|
process.exit(3);
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(error instanceof Error ? error.message : String(error));
|
logger.error(error instanceof Error ? error.message : String(error));
|
||||||
process.exit(3);
|
process.exit(3);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
main();
|
main();
|
||||||
|
|||||||
+522
-522
File diff suppressed because it is too large
Load Diff
+96
-96
@@ -1,96 +1,96 @@
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { StateManager } from './state/index.js';
|
import { StateManager } from './state/index.js';
|
||||||
import { Controller, type LoopResult, type TaskCompleteInfo } from './controller.js';
|
import { Controller, type LoopResult, type TaskCompleteInfo } from './controller.js';
|
||||||
import { setupSession } from './cli.js';
|
import { setupSession } from './cli.js';
|
||||||
import { logger, createTaskCommit } from './utils/index.js';
|
import { logger, createTaskCommit } from './utils/index.js';
|
||||||
|
|
||||||
export async function run(): Promise<LoopResult | null> {
|
export async function run(): Promise<LoopResult | null> {
|
||||||
// Ensure agents are registered
|
// Ensure agents are registered
|
||||||
await import('./agents/index.js');
|
await import('./agents/index.js');
|
||||||
|
|
||||||
const stateManager = new StateManager();
|
const stateManager = new StateManager();
|
||||||
|
|
||||||
const result = await setupSession(stateManager);
|
const result = await setupSession(stateManager);
|
||||||
if (!result) {
|
if (!result) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { config, isResume } = result;
|
const { config, isResume } = result;
|
||||||
|
|
||||||
if (isResume) {
|
if (isResume) {
|
||||||
logger.info(`Resuming from iteration ${config.currentIteration}`);
|
logger.info(`Resuming from iteration ${config.currentIteration}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const controller = new Controller({
|
const controller = new Controller({
|
||||||
config,
|
config,
|
||||||
stateManager,
|
stateManager,
|
||||||
onIteration: (iter, max) => {
|
onIteration: (iter, max) => {
|
||||||
// Could add git checkpoint logic here if needed
|
// Could add git checkpoint logic here if needed
|
||||||
},
|
},
|
||||||
onTaskComplete: async (info: TaskCompleteInfo) => {
|
onTaskComplete: async (info: TaskCompleteInfo) => {
|
||||||
// Create git commit for completed task
|
// Create git commit for completed task
|
||||||
const taskName = info.taskName || info.taskId || 'Task completed';
|
const taskName = info.taskName || info.taskId || 'Task completed';
|
||||||
await createTaskCommit({
|
await createTaskCommit({
|
||||||
taskName,
|
taskName,
|
||||||
jiraTicketId: config.jiraTicketId,
|
jiraTicketId: config.jiraTicketId,
|
||||||
cwd: process.cwd(),
|
cwd: process.cwd(),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
onLoopComplete: () => {
|
onLoopComplete: () => {
|
||||||
// All tasks completed callback
|
// All tasks completed callback
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// Setup interrupt handler
|
// Setup interrupt handler
|
||||||
const handleInterrupt = () => {
|
const handleInterrupt = () => {
|
||||||
logger.warning('\nInterrupt received, saving state...');
|
logger.warning('\nInterrupt received, saving state...');
|
||||||
controller.interrupt();
|
controller.interrupt();
|
||||||
};
|
};
|
||||||
|
|
||||||
process.on('SIGINT', handleInterrupt);
|
process.on('SIGINT', handleInterrupt);
|
||||||
process.on('SIGTERM', handleInterrupt);
|
process.on('SIGTERM', handleInterrupt);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const loopResult = await controller.run();
|
const loopResult = await controller.run();
|
||||||
|
|
||||||
// Display summary
|
// Display summary
|
||||||
console.log();
|
console.log();
|
||||||
logger.header('Session Summary');
|
logger.header('Session Summary');
|
||||||
logger.info(`Total iterations: ${loopResult.iterations}`);
|
logger.info(`Total iterations: ${loopResult.iterations}`);
|
||||||
logger.info(`Tasks completed: ${loopResult.tasksCompleted}`);
|
logger.info(`Tasks completed: ${loopResult.tasksCompleted}`);
|
||||||
if (loopResult.prereqsCompleted > 0) {
|
if (loopResult.prereqsCompleted > 0) {
|
||||||
logger.info(`Prerequisites verified: ${loopResult.prereqsCompleted}`);
|
logger.info(`Prerequisites verified: ${loopResult.prereqsCompleted}`);
|
||||||
}
|
}
|
||||||
logger.info(`Exit reason: ${loopResult.exitReason}`);
|
logger.info(`Exit reason: ${loopResult.exitReason}`);
|
||||||
if (loopResult.finalMarker) {
|
if (loopResult.finalMarker) {
|
||||||
logger.info(`Completion marker: ${loopResult.finalMarker}`);
|
logger.info(`Completion marker: ${loopResult.finalMarker}`);
|
||||||
}
|
}
|
||||||
if (loopResult.error) {
|
if (loopResult.error) {
|
||||||
logger.error(`Error: ${loopResult.error.message}`);
|
logger.error(`Error: ${loopResult.error.message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show completion celebration and finalize reminder when all phases complete
|
// Show completion celebration and finalize reminder when all phases complete
|
||||||
if (loopResult.exitReason === 'all_complete') {
|
if (loopResult.exitReason === 'all_complete') {
|
||||||
logger.allPhasesComplete();
|
logger.allPhasesComplete();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show state file locations (now per-spec)
|
// Show state file locations (now per-spec)
|
||||||
console.log();
|
console.log();
|
||||||
logger.dim(`Session files saved to ${path.relative(process.cwd(), stateManager.getStateDir())}:`);
|
logger.dim(`Session files saved to ${path.relative(process.cwd(), stateManager.getStateDir())}:`);
|
||||||
logger.dim(' - config.json (session configuration)');
|
logger.dim(' - config.json (session configuration)');
|
||||||
logger.dim(' - scratchpad.md (LLM-managed notes)');
|
logger.dim(' - scratchpad.md (LLM-managed notes)');
|
||||||
logger.dim(' - iteration.log (history)');
|
logger.dim(' - iteration.log (history)');
|
||||||
|
|
||||||
return loopResult;
|
return loopResult;
|
||||||
} finally {
|
} finally {
|
||||||
process.off('SIGINT', handleInterrupt);
|
process.off('SIGINT', handleInterrupt);
|
||||||
process.off('SIGTERM', handleInterrupt);
|
process.off('SIGTERM', handleInterrupt);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Re-export types and classes
|
// Re-export types and classes
|
||||||
export { Controller, type ControllerOptions, type LoopResult, type TaskCompleteInfo } from './controller.js';
|
export { Controller, type ControllerOptions, type LoopResult, type TaskCompleteInfo } from './controller.js';
|
||||||
export { StateManager } from './state/index.js';
|
export { StateManager } from './state/index.js';
|
||||||
export { setupSession } from './cli.js';
|
export { setupSession } from './cli.js';
|
||||||
export { agentRegistry, type Agent, type AgentConfig } from './agents/index.js';
|
export { agentRegistry, type Agent, type AgentConfig } from './agents/index.js';
|
||||||
export { detectSpecDirectories, getSpecProgress } from './spec/index.js';
|
export { detectSpecDirectories, getSpecProgress } from './spec/index.js';
|
||||||
|
|||||||
@@ -1,39 +1,39 @@
|
|||||||
import type { StateManager, LoopMode } from '../state/index.js';
|
import type { StateManager, LoopMode } from '../state/index.js';
|
||||||
import { LOOP_PROMPT_TEMPLATE, LOOP_PROMPT_TEMPLATE_PHASE } from './templates.js';
|
import { LOOP_PROMPT_TEMPLATE, LOOP_PROMPT_TEMPLATE_PHASE } from './templates.js';
|
||||||
|
|
||||||
export interface PromptContext {
|
export interface PromptContext {
|
||||||
specPath: string;
|
specPath: string;
|
||||||
iteration: number;
|
iteration: number;
|
||||||
maxIterations: number;
|
maxIterations: number;
|
||||||
stateManager: StateManager;
|
stateManager: StateManager;
|
||||||
loopMode: LoopMode;
|
loopMode: LoopMode;
|
||||||
jiraTicketId?: string;
|
jiraTicketId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build the prompt for the AI agent
|
* Build the prompt for the AI agent
|
||||||
* Selects template based on loop mode (task vs phase)
|
* Selects template based on loop mode (task vs phase)
|
||||||
*/
|
*/
|
||||||
export async function buildLoopPrompt(context: PromptContext): Promise<string> {
|
export async function buildLoopPrompt(context: PromptContext): Promise<string> {
|
||||||
const { specPath, iteration, maxIterations, stateManager, loopMode, jiraTicketId } = context;
|
const { specPath, iteration, maxIterations, stateManager, loopMode, jiraTicketId } = context;
|
||||||
|
|
||||||
// Read scratchpad content for session continuity (LLM writes to this)
|
// Read scratchpad content for session continuity (LLM writes to this)
|
||||||
const scratchpadContent = await stateManager.readScratchpad();
|
const scratchpadContent = await stateManager.readScratchpad();
|
||||||
|
|
||||||
// Project root is where plan2code-loop was invoked from
|
// Project root is where plan2code-loop was invoked from
|
||||||
const projectRoot = process.cwd();
|
const projectRoot = process.cwd();
|
||||||
|
|
||||||
// Select template based on loop mode
|
// Select template based on loop mode
|
||||||
const template = loopMode === 'phase' ? LOOP_PROMPT_TEMPLATE_PHASE : LOOP_PROMPT_TEMPLATE;
|
const template = loopMode === 'phase' ? LOOP_PROMPT_TEMPLATE_PHASE : LOOP_PROMPT_TEMPLATE;
|
||||||
|
|
||||||
// Template substitution
|
// Template substitution
|
||||||
const prompt = template
|
const prompt = template
|
||||||
.replace(/{{projectRoot}}/g, projectRoot)
|
.replace(/{{projectRoot}}/g, projectRoot)
|
||||||
.replace(/{{specPath}}/g, specPath)
|
.replace(/{{specPath}}/g, specPath)
|
||||||
.replace(/{{iteration}}/g, iteration.toString())
|
.replace(/{{iteration}}/g, iteration.toString())
|
||||||
.replace(/{{maxIterations}}/g, maxIterations.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 || '');
|
.replace(/{{jiraTicketId}}/g, jiraTicketId || '');
|
||||||
|
|
||||||
return prompt;
|
return prompt;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
export {
|
export {
|
||||||
buildLoopPrompt,
|
buildLoopPrompt,
|
||||||
type PromptContext,
|
type PromptContext,
|
||||||
} from './builder.js';
|
} from './builder.js';
|
||||||
|
|
||||||
export { LOOP_PROMPT_TEMPLATE, LOOP_PROMPT_TEMPLATE_PHASE } from './templates.js';
|
export { LOOP_PROMPT_TEMPLATE, LOOP_PROMPT_TEMPLATE_PHASE } from './templates.js';
|
||||||
|
|||||||
@@ -1,200 +1,200 @@
|
|||||||
export const LOOP_PROMPT_TEMPLATE = `# PLAN2CODE-LOOP: Autonomous Task Implementation
|
export const LOOP_PROMPT_TEMPLATE = `# PLAN2CODE-LOOP: Autonomous Task Implementation
|
||||||
|
|
||||||
## CRITICAL CONSTRAINT
|
## CRITICAL CONSTRAINT
|
||||||
**IMPLEMENT EXACTLY ONE TASK PER ITERATION.**
|
**IMPLEMENT EXACTLY ONE TASK PER ITERATION.**
|
||||||
Do NOT implement multiple tasks. Do NOT complete an entire phase.
|
Do NOT implement multiple tasks. Do NOT complete an entire phase.
|
||||||
Find the FIRST unchecked task, implement ONLY that task, then STOP and report.
|
Find the FIRST unchecked task, implement ONLY that task, then STOP and report.
|
||||||
|
|
||||||
## Project Information
|
## Project Information
|
||||||
- **Project Root:** \`{{projectRoot}}\`
|
- **Project Root:** \`{{projectRoot}}\`
|
||||||
- **Spec Location:** \`{{specPath}}\`
|
- **Spec Location:** \`{{specPath}}\`
|
||||||
- Read \`AGENTS.md\` for project-specific guidance if available
|
- Read \`AGENTS.md\` for project-specific guidance if available
|
||||||
|
|
||||||
## IMPORTANT: File Locations
|
## IMPORTANT: File Locations
|
||||||
- Write ALL code files relative to the **project root** (\`{{projectRoot}}\`)
|
- Write ALL code files relative to the **project root** (\`{{projectRoot}}\`)
|
||||||
- The spec directory (\`{{specPath}}\`) is for documentation ONLY - never write code there
|
- The spec directory (\`{{specPath}}\`) is for documentation ONLY - never write code there
|
||||||
- Example: Create \`{{projectRoot}}/src/index.ts\`, NOT \`{{specPath}}/src/index.ts\`
|
- Example: Create \`{{projectRoot}}/src/index.ts\`, NOT \`{{specPath}}/src/index.ts\`
|
||||||
|
|
||||||
## Iteration
|
## Iteration
|
||||||
{{iteration}} of {{maxIterations}}
|
{{iteration}} of {{maxIterations}}
|
||||||
|
|
||||||
## Task Discovery Process
|
## Task Discovery Process
|
||||||
1. Read \`{{specPath}}/overview.md\` to see all phases
|
1. Read \`{{specPath}}/overview.md\` to see all phases
|
||||||
2. Find the FIRST phase with an unchecked checkbox (\`- [ ]\` or \`- [/]\`)
|
2. Find the FIRST phase with an unchecked checkbox (\`- [ ]\` or \`- [/]\`)
|
||||||
3. Read that phase's file (e.g., \`phase-1.md\`)
|
3. Read that phase's file (e.g., \`phase-1.md\`)
|
||||||
4. Check the \`## Prerequisites\` section FIRST
|
4. Check the \`## Prerequisites\` section FIRST
|
||||||
5. Find the FIRST unverified prerequisite (no "VERIFIED" or "ASSUMED" annotation)
|
5. Find the FIRST unverified prerequisite (no "VERIFIED" or "ASSUMED" annotation)
|
||||||
- If found, verify/complete it, then annotate "VERIFIED" or "ASSUMED: [reason]" inline
|
- If found, verify/complete it, then annotate "VERIFIED" or "ASSUMED: [reason]" inline
|
||||||
6. Only if ALL prerequisites are verified or assumed, find the FIRST unchecked task (\`- [ ]\`)
|
6. Only if ALL prerequisites are verified or assumed, find the FIRST unchecked task (\`- [ ]\`)
|
||||||
7. That is your ONE task - implement ONLY that task
|
7. That is your ONE task - implement ONLY that task
|
||||||
|
|
||||||
## Checkbox States (Task items only)
|
## Checkbox States (Task items only)
|
||||||
- \`[ ]\` = incomplete/pending (do the FIRST one you find)
|
- \`[ ]\` = incomplete/pending (do the FIRST one you find)
|
||||||
- \`[x]\` = complete (skip)
|
- \`[x]\` = complete (skip)
|
||||||
- \`[?]\` = assumed complete, couldn't verify (skip)
|
- \`[?]\` = assumed complete, couldn't verify (skip)
|
||||||
- \`[!]\` = blocked (skip)
|
- \`[!]\` = blocked (skip)
|
||||||
|
|
||||||
Prerequisites use plain bullets with inline annotations, not checkboxes.
|
Prerequisites use plain bullets with inline annotations, not checkboxes.
|
||||||
|
|
||||||
## Implementation Steps
|
## Implementation Steps
|
||||||
1. Read and understand the single task
|
1. Read and understand the single task
|
||||||
2. Implement it completely
|
2. Implement it completely
|
||||||
3. Validate it works (run tests if applicable and double-check code)
|
3. Validate it works (run tests if applicable and double-check code)
|
||||||
4. Mark ONLY that task's checkbox as \`[x]\` in the phase file
|
4. Mark ONLY that task's checkbox as \`[x]\` in the phase file
|
||||||
5. If that was the LAST task in the phase, also mark the phase \`[x]\` in overview.md
|
5. If that was the LAST task in the phase, also mark the phase \`[x]\` in overview.md
|
||||||
6. Output your completion marker and STOP
|
6. Output your completion marker and STOP
|
||||||
|
|
||||||
## Git Policy
|
## Git Policy
|
||||||
**DO NOT create git commits.** The orchestration system handles commits automatically after each task completion. Just implement the code and leave changes uncommitted.
|
**DO NOT create git commits.** The orchestration system handles commits automatically after each task completion. Just implement the code and leave changes uncommitted.
|
||||||
|
|
||||||
## Completion Markers (REQUIRED FORMAT)
|
## Completion Markers (REQUIRED FORMAT)
|
||||||
Output exactly ONE of these at the end, including the task ID and description:
|
Output exactly ONE of these at the end, including the task ID and description:
|
||||||
|
|
||||||
**PREREQ_COMPLETE: [prereq_id] - [description]**
|
**PREREQ_COMPLETE: [prereq_id] - [description]**
|
||||||
Example: \`PREREQ_COMPLETE: P1.1 - Verified Phase 1 complete\`
|
Example: \`PREREQ_COMPLETE: P1.1 - Verified Phase 1 complete\`
|
||||||
|
|
||||||
**PREREQ_ASSUMED: [prereq_id] - [description]**
|
**PREREQ_ASSUMED: [prereq_id] - [description]**
|
||||||
Example: \`PREREQ_ASSUMED: P2.1 - Design approval (cannot verify)\`
|
Example: \`PREREQ_ASSUMED: P2.1 - Design approval (cannot verify)\`
|
||||||
|
|
||||||
**TASK_COMPLETE: [task_id] - [task_description]**
|
**TASK_COMPLETE: [task_id] - [task_description]**
|
||||||
Example: \`TASK_COMPLETE: 1.1 - Initialize project structure\`
|
Example: \`TASK_COMPLETE: 1.1 - Initialize project structure\`
|
||||||
|
|
||||||
**TASK_BLOCKED: [task_id] - [reason]**
|
**TASK_BLOCKED: [task_id] - [reason]**
|
||||||
Example: \`TASK_BLOCKED: 2.3 - Missing API credentials\`
|
Example: \`TASK_BLOCKED: 2.3 - Missing API credentials\`
|
||||||
|
|
||||||
**LOOP_COMPLETE**
|
**LOOP_COMPLETE**
|
||||||
Use only when ALL phases in overview.md are marked complete.
|
Use only when ALL phases in overview.md are marked complete.
|
||||||
|
|
||||||
## Scratchpad Management
|
## Scratchpad Management
|
||||||
|
|
||||||
After completing each task, add a new entry at the **bottom** of \`{{specPath}}/.plan2code-loop/scratchpad.md\`.
|
After completing each task, add a new entry at the **bottom** of \`{{specPath}}/.plan2code-loop/scratchpad.md\`.
|
||||||
Never edit, reorganize, or insert into existing content — only append new entries to the end of the file.
|
Never edit, reorganize, or insert into existing content — only append new entries to the end of the file.
|
||||||
|
|
||||||
Each entry should include:
|
Each entry should include:
|
||||||
- Task completed and Phase item reference
|
- Task completed and Phase item reference
|
||||||
- Key decisions made and reasoning
|
- Key decisions made and reasoning
|
||||||
- Files changed
|
- Files changed
|
||||||
- Any blockers or notes for next iteration
|
- Any blockers or notes for next iteration
|
||||||
|
|
||||||
Keep entries concise. Sacrifice grammar for concision. This file helps future iterations skip exploration.
|
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.
|
If key patterns or learnings were discovered, update \`./AGENTS.md\` if it exists.
|
||||||
|
|
||||||
## Previous Session Context
|
## Previous Session Context
|
||||||
{{scratchpadContent}}
|
{{scratchpadContent}}
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Remember: ONE TASK ONLY. Find it, implement it, mark it done, output TASK_COMPLETE with the task ID and description, then stop.
|
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
|
export const LOOP_PROMPT_TEMPLATE_PHASE = `# PLAN2CODE-LOOP: Autonomous Phase Implementation
|
||||||
|
|
||||||
## CRITICAL CONSTRAINT
|
## CRITICAL CONSTRAINT
|
||||||
**IMPLEMENT ALL REMAINING TASKS IN THE CURRENT PHASE.**
|
**IMPLEMENT ALL REMAINING TASKS IN THE CURRENT PHASE.**
|
||||||
Find the first incomplete phase, then implement every remaining task in that phase before stopping.
|
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.
|
Complete each task fully before moving to the next task within the phase.
|
||||||
|
|
||||||
## Project Information
|
## Project Information
|
||||||
- **Project Root:** \`{{projectRoot}}\`
|
- **Project Root:** \`{{projectRoot}}\`
|
||||||
- **Spec Location:** \`{{specPath}}\`
|
- **Spec Location:** \`{{specPath}}\`
|
||||||
- Read \`AGENTS.md\` for project-specific guidance if available
|
- Read \`AGENTS.md\` for project-specific guidance if available
|
||||||
|
|
||||||
## IMPORTANT: File Locations
|
## IMPORTANT: File Locations
|
||||||
- Write ALL code files relative to the **project root** (\`{{projectRoot}}\`)
|
- Write ALL code files relative to the **project root** (\`{{projectRoot}}\`)
|
||||||
- The spec directory (\`{{specPath}}\`) is for documentation ONLY - never write code there
|
- The spec directory (\`{{specPath}}\`) is for documentation ONLY - never write code there
|
||||||
- Example: Create \`{{projectRoot}}/src/index.ts\`, NOT \`{{specPath}}/src/index.ts\`
|
- Example: Create \`{{projectRoot}}/src/index.ts\`, NOT \`{{specPath}}/src/index.ts\`
|
||||||
|
|
||||||
## Iteration
|
## Iteration
|
||||||
{{iteration}} of {{maxIterations}}
|
{{iteration}} of {{maxIterations}}
|
||||||
|
|
||||||
## Phase Discovery Process
|
## Phase Discovery Process
|
||||||
1. Read \`{{specPath}}/overview.md\` to see all phases
|
1. Read \`{{specPath}}/overview.md\` to see all phases
|
||||||
2. Find the FIRST phase with an unchecked checkbox (\`- [ ]\` or \`- [/]\`)
|
2. Find the FIRST phase with an unchecked checkbox (\`- [ ]\` or \`- [/]\`)
|
||||||
3. Read that phase's file (e.g., \`phase-1.md\`)
|
3. Read that phase's file (e.g., \`phase-1.md\`)
|
||||||
4. Check the \`## Prerequisites\` section FIRST
|
4. Check the \`## Prerequisites\` section FIRST
|
||||||
5. Verify ALL unverified prerequisites first, in order
|
5. Verify ALL unverified prerequisites first, in order
|
||||||
- Annotate each "VERIFIED" or "ASSUMED: [reason]" inline
|
- Annotate each "VERIFIED" or "ASSUMED: [reason]" inline
|
||||||
6. Once ALL prerequisites are verified, implement ALL unchecked tasks in order
|
6. Once ALL prerequisites are verified, implement ALL unchecked tasks in order
|
||||||
7. Continue until every task in the phase is marked \`[x]\`
|
7. Continue until every task in the phase is marked \`[x]\`
|
||||||
|
|
||||||
## Checkbox States (Task items only)
|
## Checkbox States (Task items only)
|
||||||
- \`[ ]\` = incomplete/pending
|
- \`[ ]\` = incomplete/pending
|
||||||
- \`[x]\` = complete (skip)
|
- \`[x]\` = complete (skip)
|
||||||
- \`[?]\` = assumed complete, couldn't verify (skip)
|
- \`[?]\` = assumed complete, couldn't verify (skip)
|
||||||
- \`[!]\` = blocked (skip, note in scratchpad)
|
- \`[!]\` = blocked (skip, note in scratchpad)
|
||||||
|
|
||||||
Prerequisites use plain bullets with inline annotations, not checkboxes.
|
Prerequisites use plain bullets with inline annotations, not checkboxes.
|
||||||
|
|
||||||
## Implementation Steps (repeat for EACH task in the phase)
|
## Implementation Steps (repeat for EACH task in the phase)
|
||||||
1. Read and understand the task
|
1. Read and understand the task
|
||||||
2. Implement it completely
|
2. Implement it completely
|
||||||
3. Validate it works (run tests if applicable and double-check code)
|
3. Validate it works (run tests if applicable and double-check code)
|
||||||
4. Mark that task's checkbox as \`[x]\` in the phase file
|
4. Mark that task's checkbox as \`[x]\` in the phase file
|
||||||
5. **Create a git commit** for this task (see Git Policy below)
|
5. **Create a git commit** for this task (see Git Policy below)
|
||||||
6. Output a TASK_COMPLETE marker for this task
|
6. Output a TASK_COMPLETE marker for this task
|
||||||
7. Move to the next unchecked task in the same phase
|
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
|
8. When ALL tasks in the phase are done, mark the phase \`[x]\` in overview.md
|
||||||
|
|
||||||
## Git Policy
|
## Git Policy
|
||||||
**YOU are responsible for creating git commits after each task.** The orchestration system does NOT handle commits in phase mode.
|
**YOU are responsible for creating git commits after each task.** The orchestration system does NOT handle commits in phase mode.
|
||||||
|
|
||||||
After completing each task:
|
After completing each task:
|
||||||
\`\`\`bash
|
\`\`\`bash
|
||||||
git add -A
|
git add -A
|
||||||
git commit -m "<commit message>"
|
git commit -m "<commit message>"
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
**Commit message format:**
|
**Commit message format:**
|
||||||
\`\`\`
|
\`\`\`
|
||||||
git add -A
|
git add -A
|
||||||
git commit -m "Task X.Y: description" -m "{{jiraTicketId}}" -m "AI Assisted"
|
git commit -m "Task X.Y: description" -m "{{jiraTicketId}}" -m "AI Assisted"
|
||||||
\`\`\`
|
\`\`\`
|
||||||
- With JIRA ticket: three \`-m\` flags (description, ticket ID, AI Assisted)
|
- With JIRA ticket: three \`-m\` flags (description, ticket ID, AI Assisted)
|
||||||
- Without JIRA ticket: two \`-m\` flags (description, AI Assisted)
|
- Without JIRA ticket: two \`-m\` flags (description, AI Assisted)
|
||||||
- ALWAYS include "AI Assisted" as the final \`-m\` flag
|
- ALWAYS include "AI Assisted" as the final \`-m\` flag
|
||||||
|
|
||||||
Replace X.Y with the actual task ID and description with a concise summary of what was implemented.
|
Replace X.Y with the actual task ID and description with a concise summary of what was implemented.
|
||||||
|
|
||||||
## Completion Markers (REQUIRED FORMAT)
|
## Completion Markers (REQUIRED FORMAT)
|
||||||
Output one of these **after each task** you complete:
|
Output one of these **after each task** you complete:
|
||||||
|
|
||||||
**PREREQ_COMPLETE: [prereq_id] - [description]**
|
**PREREQ_COMPLETE: [prereq_id] - [description]**
|
||||||
Example: \`PREREQ_COMPLETE: P1.1 - Verified Phase 1 complete\`
|
Example: \`PREREQ_COMPLETE: P1.1 - Verified Phase 1 complete\`
|
||||||
|
|
||||||
**PREREQ_ASSUMED: [prereq_id] - [description]**
|
**PREREQ_ASSUMED: [prereq_id] - [description]**
|
||||||
Example: \`PREREQ_ASSUMED: P2.1 - Design approval (cannot verify)\`
|
Example: \`PREREQ_ASSUMED: P2.1 - Design approval (cannot verify)\`
|
||||||
|
|
||||||
**TASK_COMPLETE: [task_id] - [task_description]**
|
**TASK_COMPLETE: [task_id] - [task_description]**
|
||||||
Example: \`TASK_COMPLETE: 1.1 - Initialize project structure\`
|
Example: \`TASK_COMPLETE: 1.1 - Initialize project structure\`
|
||||||
|
|
||||||
**TASK_BLOCKED: [task_id] - [reason]**
|
**TASK_BLOCKED: [task_id] - [reason]**
|
||||||
Example: \`TASK_BLOCKED: 2.3 - Missing API credentials\`
|
Example: \`TASK_BLOCKED: 2.3 - Missing API credentials\`
|
||||||
If a task is blocked, skip it and continue to the next task.
|
If a task is blocked, skip it and continue to the next task.
|
||||||
|
|
||||||
After ALL tasks in the phase are complete (or blocked), output:
|
After ALL tasks in the phase are complete (or blocked), output:
|
||||||
**PHASE_COMPLETE** - if only this phase is done
|
**PHASE_COMPLETE** - if only this phase is done
|
||||||
**LOOP_COMPLETE** - if ALL phases in overview.md are now marked complete
|
**LOOP_COMPLETE** - if ALL phases in overview.md are now marked complete
|
||||||
|
|
||||||
## Scratchpad Management
|
## Scratchpad Management
|
||||||
|
|
||||||
After completing each task, add a new entry at the **bottom** of \`{{specPath}}/.plan2code-loop/scratchpad.md\`.
|
After completing each task, add a new entry at the **bottom** of \`{{specPath}}/.plan2code-loop/scratchpad.md\`.
|
||||||
Never edit, reorganize, or insert into existing content — only append new entries to the end of the file.
|
Never edit, reorganize, or insert into existing content — only append new entries to the end of the file.
|
||||||
|
|
||||||
Each entry should include:
|
Each entry should include:
|
||||||
- Task completed and Phase item reference
|
- Task completed and Phase item reference
|
||||||
- Key decisions made and reasoning
|
- Key decisions made and reasoning
|
||||||
- Files changed
|
- Files changed
|
||||||
- Any blockers or notes for next iteration
|
- Any blockers or notes for next iteration
|
||||||
|
|
||||||
Keep entries concise. Sacrifice grammar for concision. This file helps future iterations skip exploration.
|
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.
|
If key patterns or learnings were discovered, update \`./AGENTS.md\` if it exists.
|
||||||
|
|
||||||
## Previous Session Context
|
## Previous Session Context
|
||||||
{{scratchpadContent}}
|
{{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.
|
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,4 +1,4 @@
|
|||||||
export {
|
export {
|
||||||
detectSpecDirectories,
|
detectSpecDirectories,
|
||||||
getSpecProgress,
|
getSpecProgress,
|
||||||
} from './utils.js';
|
} from './utils.js';
|
||||||
|
|||||||
@@ -1,70 +1,70 @@
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Auto-detect spec directories in the project
|
* Auto-detect spec directories in the project
|
||||||
* Looks for directories containing overview.md
|
* Looks for directories containing overview.md
|
||||||
*/
|
*/
|
||||||
export async function detectSpecDirectories(cwd: string = process.cwd()): Promise<string[]> {
|
export async function detectSpecDirectories(cwd: string = process.cwd()): Promise<string[]> {
|
||||||
const specsDir = path.join(cwd, 'specs');
|
const specsDir = path.join(cwd, 'specs');
|
||||||
const specsDirs: string[] = [];
|
const specsDirs: string[] = [];
|
||||||
|
|
||||||
if (await fs.pathExists(specsDir)) {
|
if (await fs.pathExists(specsDir)) {
|
||||||
// Look for overview.md files in subdirectories
|
// Look for overview.md files in subdirectories
|
||||||
const entries = await fs.readdir(specsDir, { withFileTypes: true });
|
const entries = await fs.readdir(specsDir, { withFileTypes: true });
|
||||||
|
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
if (entry.isDirectory()) {
|
if (entry.isDirectory()) {
|
||||||
const overviewPath = path.join(specsDir, entry.name, 'overview.md');
|
const overviewPath = path.join(specsDir, entry.name, 'overview.md');
|
||||||
if (await fs.pathExists(overviewPath)) {
|
if (await fs.pathExists(overviewPath)) {
|
||||||
specsDirs.push(path.join(specsDir, entry.name));
|
specsDirs.push(path.join(specsDir, entry.name));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also check if specs/ itself contains overview.md
|
// Also check if specs/ itself contains overview.md
|
||||||
const rootOverview = path.join(specsDir, 'overview.md');
|
const rootOverview = path.join(specsDir, 'overview.md');
|
||||||
if (await fs.pathExists(rootOverview)) {
|
if (await fs.pathExists(rootOverview)) {
|
||||||
specsDirs.push(specsDir);
|
specsDirs.push(specsDir);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return specsDirs;
|
return specsDirs;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Simple progress stats by counting phase-*.md files
|
* Simple progress stats by counting phase-*.md files
|
||||||
* Used for CLI display only - LLM handles actual task discovery
|
* Used for CLI display only - LLM handles actual task discovery
|
||||||
*/
|
*/
|
||||||
export async function getSpecProgress(specPath: string): Promise<{
|
export async function getSpecProgress(specPath: string): Promise<{
|
||||||
featureName: string;
|
featureName: string;
|
||||||
totalPhases: number;
|
totalPhases: number;
|
||||||
}> {
|
}> {
|
||||||
const overviewPath = path.join(specPath, 'overview.md');
|
const overviewPath = path.join(specPath, 'overview.md');
|
||||||
|
|
||||||
// Extract feature name from overview.md
|
// Extract feature name from overview.md
|
||||||
let featureName = path.basename(specPath);
|
let featureName = path.basename(specPath);
|
||||||
try {
|
try {
|
||||||
const overviewContent = await fs.readFile(overviewPath, 'utf8');
|
const overviewContent = await fs.readFile(overviewPath, 'utf8');
|
||||||
const h1Match = overviewContent.match(/^#\s+(.+)$/m);
|
const h1Match = overviewContent.match(/^#\s+(.+)$/m);
|
||||||
if (h1Match) {
|
if (h1Match) {
|
||||||
featureName = h1Match[1].trim();
|
featureName = h1Match[1].trim();
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Use directory name as fallback
|
// Use directory name as fallback
|
||||||
}
|
}
|
||||||
|
|
||||||
// Count phase-*.md files
|
// Count phase-*.md files
|
||||||
let totalPhases = 0;
|
let totalPhases = 0;
|
||||||
try {
|
try {
|
||||||
const entries = await fs.readdir(specPath);
|
const entries = await fs.readdir(specPath);
|
||||||
totalPhases = entries.filter(name => /^phase-\d+\.md$/i.test(name)).length;
|
totalPhases = entries.filter(name => /^phase-\d+\.md$/i.test(name)).length;
|
||||||
} catch {
|
} catch {
|
||||||
// Directory read failed
|
// Directory read failed
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
featureName,
|
featureName,
|
||||||
totalPhases,
|
totalPhases,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { createHash } from 'crypto';
|
import { createHash } from 'crypto';
|
||||||
|
|
||||||
export function computeHash(content: string): string {
|
export function computeHash(content: string): string {
|
||||||
return createHash('sha256').update(content).digest('hex').slice(0, 16);
|
return createHash('sha256').update(content).digest('hex').slice(0, 16);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function hashesMatch(a: string, b: string): boolean {
|
export function hashesMatch(a: string, b: string): boolean {
|
||||||
return a === b;
|
return a === b;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
export {
|
export {
|
||||||
type SessionConfig,
|
type SessionConfig,
|
||||||
type IterationLogEntry,
|
type IterationLogEntry,
|
||||||
type SessionState,
|
type SessionState,
|
||||||
type LoopMode,
|
type LoopMode,
|
||||||
DEFAULT_CONFIG,
|
DEFAULT_CONFIG,
|
||||||
} from './config.js';
|
} from './config.js';
|
||||||
|
|
||||||
export { StateManager } from './manager.js';
|
export { StateManager } from './manager.js';
|
||||||
export { computeHash, hashesMatch } from './hash.js';
|
export { computeHash, hashesMatch } from './hash.js';
|
||||||
|
|||||||
+231
-231
@@ -1,231 +1,231 @@
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import fs from 'fs-extra';
|
import fs from 'fs-extra';
|
||||||
import type { SessionConfig, IterationLogEntry, SessionState } from './config.js';
|
import type { SessionConfig, IterationLogEntry, SessionState } from './config.js';
|
||||||
import { DEFAULT_CONFIG } from './config.js';
|
import { DEFAULT_CONFIG } from './config.js';
|
||||||
import { computeHash, hashesMatch } from './hash.js';
|
import { computeHash, hashesMatch } from './hash.js';
|
||||||
|
|
||||||
const SCRATCHPAD_TEMPLATE = `# Scratchpad
|
const SCRATCHPAD_TEMPLATE = `# Scratchpad
|
||||||
|
|
||||||
Session notes appended by LLM during implementation.
|
Session notes appended by LLM during implementation.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export class StateManager {
|
export class StateManager {
|
||||||
private readonly stateDir: string;
|
private readonly stateDir: string;
|
||||||
private readonly configPath: string;
|
private readonly configPath: string;
|
||||||
private readonly scratchpadPath: string;
|
private readonly scratchpadPath: string;
|
||||||
private readonly logPath: string;
|
private readonly logPath: string;
|
||||||
private readonly hashPath: string;
|
private readonly hashPath: string;
|
||||||
private specPath: string | null = null;
|
private specPath: string | null = null;
|
||||||
|
|
||||||
constructor(cwd: string = process.cwd()) {
|
constructor(cwd: string = process.cwd()) {
|
||||||
// Default to project root - will be updated when spec is selected
|
// Default to project root - will be updated when spec is selected
|
||||||
this.stateDir = path.join(cwd, '.plan2code-loop');
|
this.stateDir = path.join(cwd, '.plan2code-loop');
|
||||||
this.configPath = path.join(this.stateDir, 'config.json');
|
this.configPath = path.join(this.stateDir, 'config.json');
|
||||||
this.scratchpadPath = path.join(this.stateDir, 'scratchpad.md');
|
this.scratchpadPath = path.join(this.stateDir, 'scratchpad.md');
|
||||||
this.logPath = path.join(this.stateDir, 'iteration.log');
|
this.logPath = path.join(this.stateDir, 'iteration.log');
|
||||||
this.hashPath = path.join(this.stateDir, 'spec.hash');
|
this.hashPath = path.join(this.stateDir, 'spec.hash');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the spec path and update all state paths to be inside the spec directory
|
* Set the spec path and update all state paths to be inside the spec directory
|
||||||
*/
|
*/
|
||||||
setSpecPath(specPath: string): void {
|
setSpecPath(specPath: string): void {
|
||||||
this.specPath = specPath;
|
this.specPath = specPath;
|
||||||
const stateDir = path.join(specPath, '.plan2code-loop');
|
const stateDir = path.join(specPath, '.plan2code-loop');
|
||||||
// Update all paths to be relative to spec directory
|
// Update all paths to be relative to spec directory
|
||||||
(this as any).stateDir = stateDir;
|
(this as any).stateDir = stateDir;
|
||||||
(this as any).configPath = path.join(stateDir, 'config.json');
|
(this as any).configPath = path.join(stateDir, 'config.json');
|
||||||
(this as any).scratchpadPath = path.join(stateDir, 'scratchpad.md');
|
(this as any).scratchpadPath = path.join(stateDir, 'scratchpad.md');
|
||||||
(this as any).logPath = path.join(stateDir, 'iteration.log');
|
(this as any).logPath = path.join(stateDir, 'iteration.log');
|
||||||
(this as any).hashPath = path.join(stateDir, 'spec.hash');
|
(this as any).hashPath = path.join(stateDir, 'spec.hash');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Directory operations
|
// Directory operations
|
||||||
|
|
||||||
async ensureStateDir(): Promise<boolean> {
|
async ensureStateDir(): Promise<boolean> {
|
||||||
const existed = await fs.pathExists(this.stateDir);
|
const existed = await fs.pathExists(this.stateDir);
|
||||||
await fs.ensureDir(this.stateDir);
|
await fs.ensureDir(this.stateDir);
|
||||||
return existed;
|
return existed;
|
||||||
}
|
}
|
||||||
|
|
||||||
getStateDir(): string {
|
getStateDir(): string {
|
||||||
return this.stateDir;
|
return this.stateDir;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Config operations
|
// Config operations
|
||||||
|
|
||||||
async readConfig(): Promise<SessionConfig | null> {
|
async readConfig(): Promise<SessionConfig | null> {
|
||||||
try {
|
try {
|
||||||
const content = await fs.readFile(this.configPath, 'utf8');
|
const content = await fs.readFile(this.configPath, 'utf8');
|
||||||
return JSON.parse(content) as SessionConfig;
|
return JSON.parse(content) as SessionConfig;
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async writeConfig(config: SessionConfig): Promise<void> {
|
async writeConfig(config: SessionConfig): Promise<void> {
|
||||||
await fs.writeFile(
|
await fs.writeFile(
|
||||||
this.configPath,
|
this.configPath,
|
||||||
JSON.stringify(config, null, 2),
|
JSON.stringify(config, null, 2),
|
||||||
'utf8'
|
'utf8'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateConfig(updates: Partial<SessionConfig>): Promise<SessionConfig> {
|
async updateConfig(updates: Partial<SessionConfig>): Promise<SessionConfig> {
|
||||||
const existing = await this.readConfig();
|
const existing = await this.readConfig();
|
||||||
const updated = { ...DEFAULT_CONFIG, ...existing, ...updates } as SessionConfig;
|
const updated = { ...DEFAULT_CONFIG, ...existing, ...updates } as SessionConfig;
|
||||||
await this.writeConfig(updated);
|
await this.writeConfig(updated);
|
||||||
return updated;
|
return updated;
|
||||||
}
|
}
|
||||||
|
|
||||||
async incrementIteration(): Promise<number> {
|
async incrementIteration(): Promise<number> {
|
||||||
const config = await this.readConfig();
|
const config = await this.readConfig();
|
||||||
if (!config) throw new Error('No session config found');
|
if (!config) throw new Error('No session config found');
|
||||||
config.currentIteration++;
|
config.currentIteration++;
|
||||||
await this.writeConfig(config);
|
await this.writeConfig(config);
|
||||||
return config.currentIteration;
|
return config.currentIteration;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Session state detection
|
// Session state detection
|
||||||
|
|
||||||
async hasExistingSession(): Promise<boolean> {
|
async hasExistingSession(): Promise<boolean> {
|
||||||
const [configExists, scratchpadExists] = await Promise.all([
|
const [configExists, scratchpadExists] = await Promise.all([
|
||||||
fs.pathExists(this.configPath),
|
fs.pathExists(this.configPath),
|
||||||
fs.pathExists(this.scratchpadPath),
|
fs.pathExists(this.scratchpadPath),
|
||||||
]);
|
]);
|
||||||
return configExists || scratchpadExists;
|
return configExists || scratchpadExists;
|
||||||
}
|
}
|
||||||
|
|
||||||
async detectSessionState(specPath: string): Promise<SessionState> {
|
async detectSessionState(specPath: string): Promise<SessionState> {
|
||||||
// Ensure we're using the correct spec path
|
// Ensure we're using the correct spec path
|
||||||
this.setSpecPath(specPath);
|
this.setSpecPath(specPath);
|
||||||
|
|
||||||
const hasSession = await this.hasExistingSession();
|
const hasSession = await this.hasExistingSession();
|
||||||
if (!hasSession) {
|
if (!hasSession) {
|
||||||
return 'new';
|
return 'new';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if the spec path has changed
|
// Check if the spec path has changed
|
||||||
const existingConfig = await this.readConfig();
|
const existingConfig = await this.readConfig();
|
||||||
if (existingConfig && existingConfig.specPath !== specPath) {
|
if (existingConfig && existingConfig.specPath !== specPath) {
|
||||||
return 'changed';
|
return 'changed';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if spec content has changed (using hash of overview.md)
|
// Check if spec content has changed (using hash of overview.md)
|
||||||
const specChanged = await this.hasSpecChanged(specPath);
|
const specChanged = await this.hasSpecChanged(specPath);
|
||||||
return specChanged ? 'changed' : 'continue';
|
return specChanged ? 'changed' : 'continue';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hash management for spec change detection
|
// Hash management for spec change detection
|
||||||
|
|
||||||
async computeSpecHash(specPath: string): Promise<string> {
|
async computeSpecHash(specPath: string): Promise<string> {
|
||||||
const overviewPath = path.join(specPath, 'overview.md');
|
const overviewPath = path.join(specPath, 'overview.md');
|
||||||
try {
|
try {
|
||||||
const content = await fs.readFile(overviewPath, 'utf8');
|
const content = await fs.readFile(overviewPath, 'utf8');
|
||||||
return computeHash(content);
|
return computeHash(content);
|
||||||
} catch {
|
} catch {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async readStoredHash(): Promise<string | null> {
|
async readStoredHash(): Promise<string | null> {
|
||||||
try {
|
try {
|
||||||
return await fs.readFile(this.hashPath, 'utf8');
|
return await fs.readFile(this.hashPath, 'utf8');
|
||||||
} catch {
|
} catch {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async storeHash(hash: string): Promise<void> {
|
async storeHash(hash: string): Promise<void> {
|
||||||
await fs.writeFile(this.hashPath, hash, 'utf8');
|
await fs.writeFile(this.hashPath, hash, 'utf8');
|
||||||
}
|
}
|
||||||
|
|
||||||
async hasSpecChanged(specPath: string): Promise<boolean> {
|
async hasSpecChanged(specPath: string): Promise<boolean> {
|
||||||
const stored = await this.readStoredHash();
|
const stored = await this.readStoredHash();
|
||||||
if (!stored) return true;
|
if (!stored) return true;
|
||||||
const current = await this.computeSpecHash(specPath);
|
const current = await this.computeSpecHash(specPath);
|
||||||
return !hashesMatch(stored, current);
|
return !hashesMatch(stored, current);
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateSpecHash(specPath: string): Promise<void> {
|
async updateSpecHash(specPath: string): Promise<void> {
|
||||||
const hash = await this.computeSpecHash(specPath);
|
const hash = await this.computeSpecHash(specPath);
|
||||||
await this.storeHash(hash);
|
await this.storeHash(hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scratchpad operations
|
// Scratchpad operations
|
||||||
|
|
||||||
async initializeScratchpad(): Promise<void> {
|
async initializeScratchpad(): Promise<void> {
|
||||||
await fs.writeFile(this.scratchpadPath, SCRATCHPAD_TEMPLATE, 'utf8');
|
await fs.writeFile(this.scratchpadPath, SCRATCHPAD_TEMPLATE, 'utf8');
|
||||||
}
|
}
|
||||||
|
|
||||||
async readScratchpad(): Promise<string> {
|
async readScratchpad(): Promise<string> {
|
||||||
try {
|
try {
|
||||||
return await fs.readFile(this.scratchpadPath, 'utf8');
|
return await fs.readFile(this.scratchpadPath, 'utf8');
|
||||||
} catch {
|
} catch {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async writeScratchpad(content: string): Promise<void> {
|
async writeScratchpad(content: string): Promise<void> {
|
||||||
await fs.writeFile(this.scratchpadPath, content, 'utf8');
|
await fs.writeFile(this.scratchpadPath, content, 'utf8');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Iteration log operations
|
// Iteration log operations
|
||||||
|
|
||||||
async appendIterationLog(entry: IterationLogEntry): Promise<void> {
|
async appendIterationLog(entry: IterationLogEntry): Promise<void> {
|
||||||
const line = JSON.stringify(entry) + '\n';
|
const line = JSON.stringify(entry) + '\n';
|
||||||
await fs.appendFile(this.logPath, line, 'utf8');
|
await fs.appendFile(this.logPath, line, 'utf8');
|
||||||
}
|
}
|
||||||
|
|
||||||
async readIterationLog(): Promise<IterationLogEntry[]> {
|
async readIterationLog(): Promise<IterationLogEntry[]> {
|
||||||
try {
|
try {
|
||||||
const content = await fs.readFile(this.logPath, 'utf8');
|
const content = await fs.readFile(this.logPath, 'utf8');
|
||||||
return content
|
return content
|
||||||
.trim()
|
.trim()
|
||||||
.split('\n')
|
.split('\n')
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.map((line) => JSON.parse(line) as IterationLogEntry);
|
.map((line) => JSON.parse(line) as IterationLogEntry);
|
||||||
} catch {
|
} catch {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getLastIteration(): Promise<IterationLogEntry | null> {
|
async getLastIteration(): Promise<IterationLogEntry | null> {
|
||||||
const log = await this.readIterationLog();
|
const log = await this.readIterationLog();
|
||||||
return log.length > 0 ? log[log.length - 1] : null;
|
return log.length > 0 ? log[log.length - 1] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// State clearing and initialization
|
// State clearing and initialization
|
||||||
|
|
||||||
async clearState(): Promise<void> {
|
async clearState(): Promise<void> {
|
||||||
const filesToDelete = [
|
const filesToDelete = [
|
||||||
this.scratchpadPath,
|
this.scratchpadPath,
|
||||||
this.configPath,
|
this.configPath,
|
||||||
this.logPath,
|
this.logPath,
|
||||||
this.hashPath,
|
this.hashPath,
|
||||||
];
|
];
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
filesToDelete.map((file) => fs.remove(file).catch(() => {}))
|
filesToDelete.map((file) => fs.remove(file).catch(() => {}))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async initializeNewSession(config: SessionConfig): Promise<void> {
|
async initializeNewSession(config: SessionConfig): Promise<void> {
|
||||||
// Ensure spec path is set before initializing
|
// Ensure spec path is set before initializing
|
||||||
this.setSpecPath(config.specPath);
|
this.setSpecPath(config.specPath);
|
||||||
|
|
||||||
await this.clearState();
|
await this.clearState();
|
||||||
await this.ensureStateDir();
|
await this.ensureStateDir();
|
||||||
await this.initializeScratchpad();
|
await this.initializeScratchpad();
|
||||||
await this.writeConfig({
|
await this.writeConfig({
|
||||||
...config,
|
...config,
|
||||||
startedAt: new Date().toISOString(),
|
startedAt: new Date().toISOString(),
|
||||||
currentIteration: 0,
|
currentIteration: 0,
|
||||||
});
|
});
|
||||||
const hash = await this.computeSpecHash(config.specPath);
|
const hash = await this.computeSpecHash(config.specPath);
|
||||||
await this.storeHash(hash);
|
await this.storeHash(hash);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,169 +1,169 @@
|
|||||||
// Completion markers for plan2code-loop
|
// Completion markers for plan2code-loop
|
||||||
const COMPLETION_MARKERS = ['TASK_COMPLETE', 'TASK_BLOCKED', 'LOOP_COMPLETE', 'PREREQ_COMPLETE', 'PREREQ_ASSUMED'] as const;
|
const COMPLETION_MARKERS = ['TASK_COMPLETE', 'TASK_BLOCKED', 'LOOP_COMPLETE', 'PREREQ_COMPLETE', 'PREREQ_ASSUMED'] as const;
|
||||||
|
|
||||||
export type CompletionMarker = (typeof COMPLETION_MARKERS)[number];
|
export type CompletionMarker = (typeof COMPLETION_MARKERS)[number];
|
||||||
|
|
||||||
export interface CompletionCheckResult {
|
export interface CompletionCheckResult {
|
||||||
completed: boolean;
|
completed: boolean;
|
||||||
marker?: CompletionMarker;
|
marker?: CompletionMarker;
|
||||||
taskId?: string; // e.g., "1.1", "2.3"
|
taskId?: string; // e.g., "1.1", "2.3"
|
||||||
taskName?: string; // e.g., "Initialize project structure"
|
taskName?: string; // e.g., "Initialize project structure"
|
||||||
reason?: string; // For TASK_BLOCKED: reason
|
reason?: string; // For TASK_BLOCKED: reason
|
||||||
}
|
}
|
||||||
|
|
||||||
export function checkForCompletion(output: string): CompletionCheckResult {
|
export function checkForCompletion(output: string): CompletionCheckResult {
|
||||||
// Check for LOOP_COMPLETE first (highest priority)
|
// Check for LOOP_COMPLETE first (highest priority)
|
||||||
if (output.includes('LOOP_COMPLETE')) {
|
if (output.includes('LOOP_COMPLETE')) {
|
||||||
return { completed: true, marker: 'LOOP_COMPLETE' };
|
return { completed: true, marker: 'LOOP_COMPLETE' };
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for TASK_COMPLETE with task info
|
// Check for TASK_COMPLETE with task info
|
||||||
// Format: TASK_COMPLETE: 1.1 - Task description
|
// Format: TASK_COMPLETE: 1.1 - Task description
|
||||||
// Or: TASK_COMPLETE: 1.1: Task description
|
// Or: TASK_COMPLETE: 1.1: Task description
|
||||||
// Or: TASK_COMPLETE[1.1]: Task description
|
// Or: TASK_COMPLETE[1.1]: Task description
|
||||||
const completeMatch = output.match(/TASK_COMPLETE[:\[\s]+(\d+\.\d+)[\]:\-\s]+(.+?)(?:\n|$)/i);
|
const completeMatch = output.match(/TASK_COMPLETE[:\[\s]+(\d+\.\d+)[\]:\-\s]+(.+?)(?:\n|$)/i);
|
||||||
if (completeMatch) {
|
if (completeMatch) {
|
||||||
return {
|
return {
|
||||||
completed: true,
|
completed: true,
|
||||||
marker: 'TASK_COMPLETE',
|
marker: 'TASK_COMPLETE',
|
||||||
taskId: completeMatch[1],
|
taskId: completeMatch[1],
|
||||||
taskName: completeMatch[2].trim(),
|
taskName: completeMatch[2].trim(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Simple TASK_COMPLETE without structured info - try to extract task from context
|
// Simple TASK_COMPLETE without structured info - try to extract task from context
|
||||||
if (output.includes('TASK_COMPLETE')) {
|
if (output.includes('TASK_COMPLETE')) {
|
||||||
// Try to find task info nearby
|
// Try to find task info nearby
|
||||||
const taskMatch = output.match(/(?:task|completed?)\s+(\d+\.\d+)[:\s]+([^\n]{5,80})/i);
|
const taskMatch = output.match(/(?:task|completed?)\s+(\d+\.\d+)[:\s]+([^\n]{5,80})/i);
|
||||||
return {
|
return {
|
||||||
completed: true,
|
completed: true,
|
||||||
marker: 'TASK_COMPLETE',
|
marker: 'TASK_COMPLETE',
|
||||||
taskId: taskMatch?.[1],
|
taskId: taskMatch?.[1],
|
||||||
taskName: taskMatch?.[2]?.trim(),
|
taskName: taskMatch?.[2]?.trim(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for PREREQ_COMPLETE with prereq info
|
// Check for PREREQ_COMPLETE with prereq info
|
||||||
// Format: PREREQ_COMPLETE: P1.1 - Verified Phase 1 complete
|
// Format: PREREQ_COMPLETE: P1.1 - Verified Phase 1 complete
|
||||||
const prereqMatch = output.match(/PREREQ_COMPLETE[:\[\s]+([^\]:\-\s]+)[\]:\-\s]+(.+?)(?:\n|$)/i);
|
const prereqMatch = output.match(/PREREQ_COMPLETE[:\[\s]+([^\]:\-\s]+)[\]:\-\s]+(.+?)(?:\n|$)/i);
|
||||||
if (prereqMatch) {
|
if (prereqMatch) {
|
||||||
return {
|
return {
|
||||||
completed: true,
|
completed: true,
|
||||||
marker: 'PREREQ_COMPLETE',
|
marker: 'PREREQ_COMPLETE',
|
||||||
taskId: prereqMatch[1],
|
taskId: prereqMatch[1],
|
||||||
taskName: prereqMatch[2].trim(),
|
taskName: prereqMatch[2].trim(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for PREREQ_ASSUMED with prereq info
|
// Check for PREREQ_ASSUMED with prereq info
|
||||||
// Format: PREREQ_ASSUMED: P2.1 - Design approval (cannot verify)
|
// Format: PREREQ_ASSUMED: P2.1 - Design approval (cannot verify)
|
||||||
const assumedMatch = output.match(/PREREQ_ASSUMED[:\[\s]+([^\]:\-\s]+)[\]:\-\s]+(.+?)(?:\n|$)/i);
|
const assumedMatch = output.match(/PREREQ_ASSUMED[:\[\s]+([^\]:\-\s]+)[\]:\-\s]+(.+?)(?:\n|$)/i);
|
||||||
if (assumedMatch) {
|
if (assumedMatch) {
|
||||||
return {
|
return {
|
||||||
completed: true,
|
completed: true,
|
||||||
marker: 'PREREQ_ASSUMED',
|
marker: 'PREREQ_ASSUMED',
|
||||||
taskId: assumedMatch[1],
|
taskId: assumedMatch[1],
|
||||||
taskName: assumedMatch[2].trim(),
|
taskName: assumedMatch[2].trim(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for TASK_BLOCKED with task info and reason
|
// Check for TASK_BLOCKED with task info and reason
|
||||||
// Format: TASK_BLOCKED: 1.1 - Reason why blocked
|
// Format: TASK_BLOCKED: 1.1 - Reason why blocked
|
||||||
const blockedWithTaskMatch = output.match(/TASK_BLOCKED[:\[\s]+(\d+\.\d+)[\]:\-\s]+(.+?)(?:\n|$)/i);
|
const blockedWithTaskMatch = output.match(/TASK_BLOCKED[:\[\s]+(\d+\.\d+)[\]:\-\s]+(.+?)(?:\n|$)/i);
|
||||||
if (blockedWithTaskMatch) {
|
if (blockedWithTaskMatch) {
|
||||||
return {
|
return {
|
||||||
completed: true,
|
completed: true,
|
||||||
marker: 'TASK_BLOCKED',
|
marker: 'TASK_BLOCKED',
|
||||||
taskId: blockedWithTaskMatch[1],
|
taskId: blockedWithTaskMatch[1],
|
||||||
reason: blockedWithTaskMatch[2].trim(),
|
reason: blockedWithTaskMatch[2].trim(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// TASK_BLOCKED with just reason (no task ID)
|
// TASK_BLOCKED with just reason (no task ID)
|
||||||
const blockedMatch = output.match(/TASK_BLOCKED:\s*(.+?)(?:\n|$)/);
|
const blockedMatch = output.match(/TASK_BLOCKED:\s*(.+?)(?:\n|$)/);
|
||||||
if (blockedMatch) {
|
if (blockedMatch) {
|
||||||
return {
|
return {
|
||||||
completed: true,
|
completed: true,
|
||||||
marker: 'TASK_BLOCKED',
|
marker: 'TASK_BLOCKED',
|
||||||
reason: blockedMatch[1].trim()
|
reason: blockedMatch[1].trim()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Simple TASK_BLOCKED without any info
|
// Simple TASK_BLOCKED without any info
|
||||||
if (output.includes('TASK_BLOCKED')) {
|
if (output.includes('TASK_BLOCKED')) {
|
||||||
return { completed: true, marker: 'TASK_BLOCKED', reason: 'No reason provided' };
|
return { completed: true, marker: 'TASK_BLOCKED', reason: 'No reason provided' };
|
||||||
}
|
}
|
||||||
|
|
||||||
return { completed: false };
|
return { completed: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extract ALL completion markers from output (for phase mode).
|
* Extract ALL completion markers from output (for phase mode).
|
||||||
* Returns an array of all TASK_COMPLETE/TASK_BLOCKED markers found,
|
* Returns an array of all TASK_COMPLETE/TASK_BLOCKED markers found,
|
||||||
* plus whether LOOP_COMPLETE or PHASE_COMPLETE was present.
|
* plus whether LOOP_COMPLETE or PHASE_COMPLETE was present.
|
||||||
*/
|
*/
|
||||||
export function checkForAllCompletions(output: string): {
|
export function checkForAllCompletions(output: string): {
|
||||||
tasks: CompletionCheckResult[];
|
tasks: CompletionCheckResult[];
|
||||||
loopComplete: boolean;
|
loopComplete: boolean;
|
||||||
phaseComplete: boolean;
|
phaseComplete: boolean;
|
||||||
} {
|
} {
|
||||||
const tasks: CompletionCheckResult[] = [];
|
const tasks: CompletionCheckResult[] = [];
|
||||||
let loopComplete = false;
|
let loopComplete = false;
|
||||||
let phaseComplete = false;
|
let phaseComplete = false;
|
||||||
|
|
||||||
if (output.includes('LOOP_COMPLETE')) {
|
if (output.includes('LOOP_COMPLETE')) {
|
||||||
loopComplete = true;
|
loopComplete = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (output.includes('PHASE_COMPLETE')) {
|
if (output.includes('PHASE_COMPLETE')) {
|
||||||
phaseComplete = true;
|
phaseComplete = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find all TASK_COMPLETE markers with task info
|
// Find all TASK_COMPLETE markers with task info
|
||||||
// Format: TASK_COMPLETE: 1.1 - Task description
|
// Format: TASK_COMPLETE: 1.1 - Task description
|
||||||
const completeRegex = /TASK_COMPLETE[:\[\s]+(\d+\.\d+)[\]:\-\s]+(.+?)(?:\n|$)/gi;
|
const completeRegex = /TASK_COMPLETE[:\[\s]+(\d+\.\d+)[\]:\-\s]+(.+?)(?:\n|$)/gi;
|
||||||
let match: RegExpExecArray | null;
|
let match: RegExpExecArray | null;
|
||||||
while ((match = completeRegex.exec(output)) !== null) {
|
while ((match = completeRegex.exec(output)) !== null) {
|
||||||
tasks.push({
|
tasks.push({
|
||||||
completed: true,
|
completed: true,
|
||||||
marker: 'TASK_COMPLETE',
|
marker: 'TASK_COMPLETE',
|
||||||
taskId: match[1],
|
taskId: match[1],
|
||||||
taskName: match[2].trim(),
|
taskName: match[2].trim(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find all TASK_BLOCKED markers with task info
|
// Find all TASK_BLOCKED markers with task info
|
||||||
const blockedRegex = /TASK_BLOCKED[:\[\s]+(\d+\.\d+)[\]:\-\s]+(.+?)(?:\n|$)/gi;
|
const blockedRegex = /TASK_BLOCKED[:\[\s]+(\d+\.\d+)[\]:\-\s]+(.+?)(?:\n|$)/gi;
|
||||||
while ((match = blockedRegex.exec(output)) !== null) {
|
while ((match = blockedRegex.exec(output)) !== null) {
|
||||||
tasks.push({
|
tasks.push({
|
||||||
completed: true,
|
completed: true,
|
||||||
marker: 'TASK_BLOCKED',
|
marker: 'TASK_BLOCKED',
|
||||||
taskId: match[1],
|
taskId: match[1],
|
||||||
reason: match[2].trim(),
|
reason: match[2].trim(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find PREREQ_COMPLETE markers
|
// Find PREREQ_COMPLETE markers
|
||||||
const prereqRegex = /PREREQ_COMPLETE[:\[\s]+([^\]:\-\s]+)[\]:\-\s]+(.+?)(?:\n|$)/gi;
|
const prereqRegex = /PREREQ_COMPLETE[:\[\s]+([^\]:\-\s]+)[\]:\-\s]+(.+?)(?:\n|$)/gi;
|
||||||
while ((match = prereqRegex.exec(output)) !== null) {
|
while ((match = prereqRegex.exec(output)) !== null) {
|
||||||
tasks.push({
|
tasks.push({
|
||||||
completed: true,
|
completed: true,
|
||||||
marker: 'PREREQ_COMPLETE',
|
marker: 'PREREQ_COMPLETE',
|
||||||
taskId: match[1],
|
taskId: match[1],
|
||||||
taskName: match[2].trim(),
|
taskName: match[2].trim(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find PREREQ_ASSUMED markers
|
// Find PREREQ_ASSUMED markers
|
||||||
const assumedRegex = /PREREQ_ASSUMED[:\[\s]+([^\]:\-\s]+)[\]:\-\s]+(.+?)(?:\n|$)/gi;
|
const assumedRegex = /PREREQ_ASSUMED[:\[\s]+([^\]:\-\s]+)[\]:\-\s]+(.+?)(?:\n|$)/gi;
|
||||||
while ((match = assumedRegex.exec(output)) !== null) {
|
while ((match = assumedRegex.exec(output)) !== null) {
|
||||||
tasks.push({
|
tasks.push({
|
||||||
completed: true,
|
completed: true,
|
||||||
marker: 'PREREQ_ASSUMED',
|
marker: 'PREREQ_ASSUMED',
|
||||||
taskId: match[1],
|
taskId: match[1],
|
||||||
taskName: match[2].trim(),
|
taskName: match[2].trim(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return { tasks, loopComplete, phaseComplete };
|
return { tasks, loopComplete, phaseComplete };
|
||||||
}
|
}
|
||||||
|
|||||||
+136
-136
@@ -1,136 +1,136 @@
|
|||||||
import { execa } from 'execa';
|
import { execa } from 'execa';
|
||||||
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import { logger } from './logger.js';
|
import { logger } from './logger.js';
|
||||||
|
|
||||||
export interface GitCommitOptions {
|
export interface GitCommitOptions {
|
||||||
taskName: string;
|
taskName: string;
|
||||||
jiraTicketId?: string;
|
jiraTicketId?: string;
|
||||||
cwd?: string;
|
cwd?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if directory is a git repository
|
* Check if directory is a git repository
|
||||||
*/
|
*/
|
||||||
export async function isGitRepo(cwd: string): Promise<boolean> {
|
export async function isGitRepo(cwd: string): Promise<boolean> {
|
||||||
const result = await execa('git', ['rev-parse', '--git-dir'], { cwd, reject: false });
|
const result = await execa('git', ['rev-parse', '--git-dir'], { cwd, reject: false });
|
||||||
return result.exitCode === 0;
|
return result.exitCode === 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize a git repository if one doesn't exist
|
* Initialize a git repository if one doesn't exist
|
||||||
*/
|
*/
|
||||||
export async function ensureGitRepo(cwd: string): Promise<boolean> {
|
export async function ensureGitRepo(cwd: string): Promise<boolean> {
|
||||||
if (await isGitRepo(cwd)) {
|
if (await isGitRepo(cwd)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info('Initializing git repository...');
|
logger.info('Initializing git repository...');
|
||||||
const result = await execa('git', ['init'], { cwd, reject: false });
|
const result = await execa('git', ['init'], { cwd, reject: false });
|
||||||
|
|
||||||
if (result.exitCode !== 0) {
|
if (result.exitCode !== 0) {
|
||||||
logger.error(`Failed to initialize git repo: ${result.stderr}`);
|
logger.error(`Failed to initialize git repo: ${result.stderr}`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.success('Git repository initialized');
|
logger.success('Git repository initialized');
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Required entries for the .gitignore file
|
* Required entries for the .gitignore file
|
||||||
*/
|
*/
|
||||||
const REQUIRED_GITIGNORE_ENTRIES = ['specs/', 'specs--completed/', '.plan2code-loop', '.plan2code-metrics', 'nul', 'node_modules/'];
|
const REQUIRED_GITIGNORE_ENTRIES = ['specs/', 'specs--completed/', '.plan2code-loop', '.plan2code-metrics', 'nul', 'node_modules/'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensure .gitignore exists with required entries
|
* Ensure .gitignore exists with required entries
|
||||||
*/
|
*/
|
||||||
export function ensureGitignore(cwd: string): void {
|
export function ensureGitignore(cwd: string): void {
|
||||||
const gitignorePath = join(cwd, '.gitignore');
|
const gitignorePath = join(cwd, '.gitignore');
|
||||||
let content = '';
|
let content = '';
|
||||||
|
|
||||||
if (existsSync(gitignorePath)) {
|
if (existsSync(gitignorePath)) {
|
||||||
content = readFileSync(gitignorePath, 'utf-8');
|
content = readFileSync(gitignorePath, 'utf-8');
|
||||||
}
|
}
|
||||||
|
|
||||||
const lines = content.split('\n').map(line => line.trim());
|
const lines = content.split('\n').map(line => line.trim());
|
||||||
const missingEntries = REQUIRED_GITIGNORE_ENTRIES.filter(entry => !lines.includes(entry));
|
const missingEntries = REQUIRED_GITIGNORE_ENTRIES.filter(entry => !lines.includes(entry));
|
||||||
|
|
||||||
if (missingEntries.length > 0) {
|
if (missingEntries.length > 0) {
|
||||||
const needsNewline = content.length > 0 && !content.endsWith('\n');
|
const needsNewline = content.length > 0 && !content.endsWith('\n');
|
||||||
const newContent = content + (needsNewline ? '\n' : '') + missingEntries.join('\n') + '\n';
|
const newContent = content + (needsNewline ? '\n' : '') + missingEntries.join('\n') + '\n';
|
||||||
writeFileSync(gitignorePath, newContent);
|
writeFileSync(gitignorePath, newContent);
|
||||||
logger.dim(`Added to .gitignore: ${missingEntries.join(', ')}`);
|
logger.dim(`Added to .gitignore: ${missingEntries.join(', ')}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a local git commit for a completed task
|
* Create a local git commit for a completed task
|
||||||
*/
|
*/
|
||||||
export async function createTaskCommit(options: GitCommitOptions): Promise<boolean> {
|
export async function createTaskCommit(options: GitCommitOptions): Promise<boolean> {
|
||||||
const { taskName, jiraTicketId, cwd = process.cwd() } = options;
|
const { taskName, jiraTicketId, cwd = process.cwd() } = options;
|
||||||
|
|
||||||
logger.dim(`Git commit check in: ${cwd}`);
|
logger.dim(`Git commit check in: ${cwd}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Ensure we have a git repo
|
// Ensure we have a git repo
|
||||||
if (!await ensureGitRepo(cwd)) {
|
if (!await ensureGitRepo(cwd)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure .gitignore exists with required entries
|
// Ensure .gitignore exists with required entries
|
||||||
ensureGitignore(cwd);
|
ensureGitignore(cwd);
|
||||||
|
|
||||||
// Check if there are any changes to commit
|
// Check if there are any changes to commit
|
||||||
const statusResult = await execa('git', ['status', '--porcelain'], { cwd, reject: false });
|
const statusResult = await execa('git', ['status', '--porcelain'], { cwd, reject: false });
|
||||||
|
|
||||||
// Debug: show what git status returned
|
// Debug: show what git status returned
|
||||||
if (statusResult.stdout?.trim()) {
|
if (statusResult.stdout?.trim()) {
|
||||||
logger.dim(`Git status found changes:\n${statusResult.stdout.slice(0, 500)}`);
|
logger.dim(`Git status found changes:\n${statusResult.stdout.slice(0, 500)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (statusResult.exitCode !== 0) {
|
if (statusResult.exitCode !== 0) {
|
||||||
logger.error(`Git status failed: ${statusResult.stderr}`);
|
logger.error(`Git status failed: ${statusResult.stderr}`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!statusResult.stdout?.trim()) {
|
if (!statusResult.stdout?.trim()) {
|
||||||
logger.dim('No changes to commit');
|
logger.dim('No changes to commit');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stage all changes
|
// Stage all changes
|
||||||
const addResult = await execa('git', ['add', '-A'], { cwd, reject: false });
|
const addResult = await execa('git', ['add', '-A'], { cwd, reject: false });
|
||||||
if (addResult.exitCode !== 0) {
|
if (addResult.exitCode !== 0) {
|
||||||
logger.error(`Git add failed: ${addResult.stderr}`);
|
logger.error(`Git add failed: ${addResult.stderr}`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build commit message
|
// Build commit message
|
||||||
let commitMessage = taskName;
|
let commitMessage = taskName;
|
||||||
if (jiraTicketId) {
|
if (jiraTicketId) {
|
||||||
commitMessage = `${taskName}\n\n${jiraTicketId}\nAI Assisted`;
|
commitMessage = `${taskName}\n\n${jiraTicketId}\nAI Assisted`;
|
||||||
} else {
|
} else {
|
||||||
commitMessage = `${taskName}\n\nAI Assisted`;
|
commitMessage = `${taskName}\n\nAI Assisted`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the commit
|
// Create the commit
|
||||||
const commitResult = await execa('git', ['commit', '-m', commitMessage], { cwd, reject: false });
|
const commitResult = await execa('git', ['commit', '-m', commitMessage], { cwd, reject: false });
|
||||||
|
|
||||||
if (commitResult.exitCode !== 0) {
|
if (commitResult.exitCode !== 0) {
|
||||||
// Check if it's just "nothing to commit" vs actual error
|
// Check if it's just "nothing to commit" vs actual error
|
||||||
if (commitResult.stdout?.includes('nothing to commit') || commitResult.stderr?.includes('nothing to commit')) {
|
if (commitResult.stdout?.includes('nothing to commit') || commitResult.stderr?.includes('nothing to commit')) {
|
||||||
logger.dim('No changes to commit');
|
logger.dim('No changes to commit');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
logger.error(`Git commit failed: ${commitResult.stderr || commitResult.stdout}`);
|
logger.error(`Git commit failed: ${commitResult.stderr || commitResult.stdout}`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.success(`Created commit: ${taskName}${jiraTicketId ? ` (${jiraTicketId})` : ''}`);
|
logger.success(`Created commit: ${taskName}${jiraTicketId ? ` (${jiraTicketId})` : ''}`);
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(`Failed to create commit: ${error instanceof Error ? error.message : String(error)}`);
|
logger.error(`Failed to create commit: ${error instanceof Error ? error.message : String(error)}`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
export { logger, MASCOT, type Logger } from './logger.js';
|
export { logger, MASCOT, type Logger } from './logger.js';
|
||||||
export {
|
export {
|
||||||
checkForCompletion,
|
checkForCompletion,
|
||||||
checkForAllCompletions,
|
checkForAllCompletions,
|
||||||
type CompletionMarker,
|
type CompletionMarker,
|
||||||
type CompletionCheckResult
|
type CompletionCheckResult
|
||||||
} from './completion.js';
|
} from './completion.js';
|
||||||
export {
|
export {
|
||||||
executeCommand,
|
executeCommand,
|
||||||
type ExecuteOptions,
|
type ExecuteOptions,
|
||||||
type ExecuteResult
|
type ExecuteResult
|
||||||
} from './process.js';
|
} from './process.js';
|
||||||
export {
|
export {
|
||||||
createTaskCommit,
|
createTaskCommit,
|
||||||
isGitRepo,
|
isGitRepo,
|
||||||
ensureGitRepo,
|
ensureGitRepo,
|
||||||
ensureGitignore,
|
ensureGitignore,
|
||||||
type GitCommitOptions
|
type GitCommitOptions
|
||||||
} from './git.js';
|
} from './git.js';
|
||||||
|
|||||||
@@ -1,81 +1,81 @@
|
|||||||
import { execa, type Options as ExecaOptions } from 'execa';
|
import { execa, type Options as ExecaOptions } from 'execa';
|
||||||
|
|
||||||
const MAX_OUTPUT_SIZE = 10 * 1024 * 1024; // 10MB
|
const MAX_OUTPUT_SIZE = 10 * 1024 * 1024; // 10MB
|
||||||
|
|
||||||
function truncateOutput(output: string, maxSize: number): string {
|
function truncateOutput(output: string, maxSize: number): string {
|
||||||
if (output.length > maxSize) {
|
if (output.length > maxSize) {
|
||||||
return output.slice(0, maxSize) + '\n...[truncated]';
|
return output.slice(0, maxSize) + '\n...[truncated]';
|
||||||
}
|
}
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExecuteOptions {
|
export interface ExecuteOptions {
|
||||||
command: string;
|
command: string;
|
||||||
args: string[];
|
args: string[];
|
||||||
cwd: string;
|
cwd: string;
|
||||||
timeout: number; // milliseconds
|
timeout: number; // milliseconds
|
||||||
env?: Record<string, string>;
|
env?: Record<string, string>;
|
||||||
signal?: AbortSignal; // For cancellation
|
signal?: AbortSignal; // For cancellation
|
||||||
stdin?: string; // Input to pass via stdin as string
|
stdin?: string; // Input to pass via stdin as string
|
||||||
stdinFile?: string; // Path to file to pipe as stdin
|
stdinFile?: string; // Path to file to pipe as stdin
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExecuteResult {
|
export interface ExecuteResult {
|
||||||
stdout: string;
|
stdout: string;
|
||||||
stderr: string;
|
stderr: string;
|
||||||
exitCode: number;
|
exitCode: number;
|
||||||
timedOut: boolean;
|
timedOut: boolean;
|
||||||
cancelled: boolean;
|
cancelled: boolean;
|
||||||
duration: number; // milliseconds
|
duration: number; // milliseconds
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function executeCommand(
|
export async function executeCommand(
|
||||||
options: ExecuteOptions
|
options: ExecuteOptions
|
||||||
): Promise<ExecuteResult> {
|
): Promise<ExecuteResult> {
|
||||||
const startTime = Date.now();
|
const startTime = Date.now();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const execaOptions: ExecaOptions = {
|
const execaOptions: ExecaOptions = {
|
||||||
cwd: options.cwd,
|
cwd: options.cwd,
|
||||||
timeout: options.timeout,
|
timeout: options.timeout,
|
||||||
env: { ...process.env, ...options.env },
|
env: { ...process.env, ...options.env },
|
||||||
reject: false,
|
reject: false,
|
||||||
all: true,
|
all: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add cancellation signal if provided
|
// Add cancellation signal if provided
|
||||||
if (options.signal) {
|
if (options.signal) {
|
||||||
(execaOptions as any).cancelSignal = options.signal;
|
(execaOptions as any).cancelSignal = options.signal;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add stdin input if provided (string or file)
|
// Add stdin input if provided (string or file)
|
||||||
if (options.stdin) {
|
if (options.stdin) {
|
||||||
(execaOptions as any).input = options.stdin;
|
(execaOptions as any).input = options.stdin;
|
||||||
} else if (options.stdinFile) {
|
} else if (options.stdinFile) {
|
||||||
(execaOptions as any).inputFile = options.stdinFile;
|
(execaOptions as any).inputFile = options.stdinFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await execa(options.command, options.args, execaOptions);
|
const result = await execa(options.command, options.args, execaOptions);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
stdout: truncateOutput(result.stdout || '', MAX_OUTPUT_SIZE),
|
stdout: truncateOutput(result.stdout || '', MAX_OUTPUT_SIZE),
|
||||||
stderr: truncateOutput(result.stderr || '', MAX_OUTPUT_SIZE),
|
stderr: truncateOutput(result.stderr || '', MAX_OUTPUT_SIZE),
|
||||||
exitCode: result.exitCode ?? 1,
|
exitCode: result.exitCode ?? 1,
|
||||||
timedOut: result.timedOut ?? false,
|
timedOut: result.timedOut ?? false,
|
||||||
cancelled: result.isCanceled ?? false,
|
cancelled: result.isCanceled ?? false,
|
||||||
duration: Date.now() - startTime,
|
duration: Date.now() - startTime,
|
||||||
};
|
};
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
// Check if this was a cancellation
|
// Check if this was a cancellation
|
||||||
const isCancelled = error?.isCanceled || options.signal?.aborted;
|
const isCancelled = error?.isCanceled || options.signal?.aborted;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
stdout: error?.stdout || '',
|
stdout: error?.stdout || '',
|
||||||
stderr: error?.stderr || (error instanceof Error ? error.message : String(error)),
|
stderr: error?.stderr || (error instanceof Error ? error.message : String(error)),
|
||||||
exitCode: isCancelled ? -1 : 1,
|
exitCode: isCancelled ? -1 : 1,
|
||||||
timedOut: false,
|
timedOut: false,
|
||||||
cancelled: isCancelled,
|
cancelled: isCancelled,
|
||||||
duration: Date.now() - startTime,
|
duration: Date.now() - startTime,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2022",
|
"target": "ES2022",
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"lib": ["ES2022"],
|
"lib": ["ES2022"],
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
"rootDir": ".",
|
"rootDir": ".",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
"declarationMap": true,
|
"declarationMap": true,
|
||||||
"sourceMap": true
|
"sourceMap": true
|
||||||
},
|
},
|
||||||
"include": ["src/**/*"],
|
"include": ["src/**/*"],
|
||||||
"exclude": ["node_modules", "dist"]
|
"exclude": ["node_modules", "dist"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import { defineConfig } from 'tsup';
|
import { defineConfig } from 'tsup';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
entry: {
|
entry: {
|
||||||
'bin/plan2code-loop': 'src/bin/plan2code-loop.ts',
|
'bin/plan2code-loop': 'src/bin/plan2code-loop.ts',
|
||||||
index: 'src/index.ts',
|
index: 'src/index.ts',
|
||||||
},
|
},
|
||||||
format: ['esm'],
|
format: ['esm'],
|
||||||
dts: false,
|
dts: false,
|
||||||
clean: true,
|
clean: true,
|
||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
banner: {
|
banner: {
|
||||||
js: '#!/usr/bin/env node',
|
js: '#!/usr/bin/env node',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user