v1.14.0 — add Step 3b review workflow, unify file naming, resilient code references

- Add /plan2code-3b-review: 5-step post-implementation review with adaptive
  scope, 11 dimensions, reference-file architecture, and Plan/Apply/Verify fixes
- Unify workflow/skill file naming to single-dash (drop -- and --- conventions)
- Add Devin platform support and CLAUDE.md MANDATORY FIRST STEP template
- Guide agents to use semantic anchors over line numbers in workflow prompts
- Bump version to 1.14.0
This commit is contained in:
2026-05-23 06:58:20 -07:00
parent 8e0b1a2ab2
commit 5e48e98293
42 changed files with 3431 additions and 2542 deletions
+210 -210
View File
@@ -1,210 +1,210 @@
# Plan2Code Loop
An autonomous CLI tool that implements Plan2Code specs by looping through tasks automatically.
> **Note:** This is an **alternative** to `/plan2code-3--implement`, not a replacement. Use the manual Step 3 workflow when you want interactive control over each phase, or use this loop when you prefer hands-off autonomous execution.
## Installation
```bash
# From the plan2code root directory:
# Option 1: Install everything (recommended)
node install.js # Select option A
# Option 2: Install loop only
node install.js # Select option O
# Option 3: Manual build and link
cd plan2code-loop
npm install
npm run build
npm link
```
## Usage
Simply run the command - everything is interactive:
```bash
plan2code-loop
```
The CLI will:
1. Auto-detect specs in `./specs/` directory
2. Let you select a spec if multiple are found
3. Prompt to continue if an existing session is found
4. Ask for JIRA ticket ID, agent selection, loop mode, and max iterations
## How It Works
The loop uses an **LLM-driven discovery** approach:
1. **Spec Selection** - Interactive menu to select from discovered specs
2. **Task Discovery** - The AI reads `overview.md` and phase files to find unchecked tasks
3. **Implementation** - The AI implements tasks (one per iteration in task mode, or all in a phase in phase mode)
4. **Checkbox Update** - The AI marks tasks complete in the markdown file
5. **Scratchpad Update** - The AI appends notes to the per-spec scratchpad
6. **Completion Marker** - The AI outputs structured markers (e.g., `TASK_COMPLETE: 1.1 - description`)
7. **Loop** - Repeat until all tasks done or max iterations reached
### Loop Modes
The CLI asks you to choose a loop mode:
| Mode | Behavior | Git Commits | Best For |
|------|----------|-------------|----------|
| **One task per loop** (default) | Each agent invocation implements exactly one task | Node controller commits after each task | Smaller models, careful step-by-step execution |
| **One phase per loop** | Each agent invocation implements all remaining tasks in the current phase | LLM commits after each task (with JIRA ID if provided) | Smart models with larger context windows, keeping related tasks together |
### Why LLM-Driven?
The Node app does NOT parse markdown to find tasks. Instead, the AI reads the spec files directly and decides what to work on. This is:
- **More flexible** - Works with any reasonable markdown format
- **Smarter** - AI can handle edge cases and ambiguity
- **Simpler** - Node code is just orchestration, not parsing
## Completion Markers
The AI must output one of these markers at the end of each iteration:
```
TASK_COMPLETE: 1.1 - Initialize project structure
TASK_BLOCKED: 2.3 - Missing API credentials
PHASE_COMPLETE
LOOP_COMPLETE
```
| Marker | Meaning |
|--------|---------|
| `TASK_COMPLETE: X.X - desc` | Task implemented and marked complete |
| `TASK_BLOCKED: X.X - reason` | Cannot complete task (explains why) |
| `PHASE_COMPLETE` | Current phase finished (phase mode only) |
| `LOOP_COMPLETE` | All phases finished |
In **phase mode**, the AI outputs multiple `TASK_COMPLETE` markers (one per task) within a single iteration, followed by `PHASE_COMPLETE` or `LOOP_COMPLETE`.
## Session Files
Session state is stored **per-spec** inside the spec directory:
```
specs/my-feature/
├── overview.md
├── phase-1.md
├── phase-2.md
└── .plan2code-loop/ # Per-spec session state
├── config.json # Session configuration
├── scratchpad.md # LLM-managed progress notes
├── iteration.log # JSON log of each iteration
└── spec.hash # Hash for detecting spec changes
```
The scratchpad is managed by the LLM itself - after each task, the AI appends notes about what was done, decisions made, and files changed. This helps future iterations skip exploration.
## Supported Agents
| Agent | Status |
|-------|--------|
| Claude Code | Supported |
| GitHub Copilot CLI | Supported |
The loop uses your configured default model for each agent.
## Example Session
```
$ plan2code-loop
╭──────────────────────────────────────╮
│ │
│ 🔮 Plany's Loop │
│ Autonomous Implementation │
│ │
╰──────────────────────────────────────╯
Found spec: specs/todo-app
Feature: Todo App
Phases: 0/3
Tasks: 0/15
══════════════════════════════════════════════
Spec: Todo App
══════════════════════════════════════════════
Phases: 0/3
Tasks: 0/15
? JIRA Ticket ID (optional): PROJ-123
? Select AI agent: Claude Code
? Tasks per loop iteration: One task per loop (default)
? Maximum iterations: 100
Starting Plan2Code Loop
══════════════════════════════════════════════
Agent: Claude Code
Model: default
Spec: C:\projects\my-app\specs\todo-app
Loop mode: One task per loop
Max iterations: 100
Iteration 1/100
[1/100] Task 1.1: Initialize project with Vite (45s)
✓ Completed: Task 1.1: Initialize project with Vite
Iteration 2/100
[2/100] Task 1.2: Configure TypeScript (32s)
✓ Completed: Task 1.2: Configure TypeScript
...
Session Summary
══════════════════════════════════════════════
Total iterations: 12
Tasks completed: 12
Exit reason: all_complete
Session files saved to specs/todo-app/.plan2code-loop:
- config.json (session configuration)
- scratchpad.md (LLM-managed notes)
- iteration.log (history)
```
## Development
```bash
# Install dependencies
npm install
# Build
npm run build
# Link for global usage
npm link
# Unlink
npm unlink
```
## Architecture
```
src/
├── bin/plan2code-loop.ts # CLI entry point
├── index.ts # Main exports
├── controller.ts # Loop orchestration
├── cli.ts # Interactive prompts
├── agents/ # Agent implementations
│ ├── claude-code.ts
│ └── copilot-cli.ts
├── prompt/ # Prompt building
│ ├── templates.ts
│ └── builder.ts
├── state/ # Session state management
│ └── manager.ts
├── spec/ # Spec utilities (for CLI display)
│ └── utils.ts
└── utils/ # Utilities
├── completion.ts # Marker parsing
└── logger.ts
```
# Plan2Code Loop
An autonomous CLI tool that implements Plan2Code specs by looping through tasks automatically.
> **Note:** This is an **alternative** to `/plan2code-3-implement`, not a replacement. Use the manual Step 3 workflow when you want interactive control over each phase, or use this loop when you prefer hands-off autonomous execution.
## Installation
```bash
# From the plan2code root directory:
# Option 1: Install everything (recommended)
node install.js # Select option A
# Option 2: Install loop only
node install.js # Select option O
# Option 3: Manual build and link
cd plan2code-loop
npm install
npm run build
npm link
```
## Usage
Simply run the command - everything is interactive:
```bash
plan2code-loop
```
The CLI will:
1. Auto-detect specs in `./specs/` directory
2. Let you select a spec if multiple are found
3. Prompt to continue if an existing session is found
4. Ask for JIRA ticket ID, agent selection, loop mode, and max iterations
## How It Works
The loop uses an **LLM-driven discovery** approach:
1. **Spec Selection** - Interactive menu to select from discovered specs
2. **Task Discovery** - The AI reads `overview.md` and phase files to find unchecked tasks
3. **Implementation** - The AI implements tasks (one per iteration in task mode, or all in a phase in phase mode)
4. **Checkbox Update** - The AI marks tasks complete in the markdown file
5. **Scratchpad Update** - The AI appends notes to the per-spec scratchpad
6. **Completion Marker** - The AI outputs structured markers (e.g., `TASK_COMPLETE: 1.1 - description`)
7. **Loop** - Repeat until all tasks done or max iterations reached
### Loop Modes
The CLI asks you to choose a loop mode:
| Mode | Behavior | Git Commits | Best For |
|------|----------|-------------|----------|
| **One task per loop** (default) | Each agent invocation implements exactly one task | Node controller commits after each task | Smaller models, careful step-by-step execution |
| **One phase per loop** | Each agent invocation implements all remaining tasks in the current phase | LLM commits after each task (with JIRA ID if provided) | Smart models with larger context windows, keeping related tasks together |
### Why LLM-Driven?
The Node app does NOT parse markdown to find tasks. Instead, the AI reads the spec files directly and decides what to work on. This is:
- **More flexible** - Works with any reasonable markdown format
- **Smarter** - AI can handle edge cases and ambiguity
- **Simpler** - Node code is just orchestration, not parsing
## Completion Markers
The AI must output one of these markers at the end of each iteration:
```
TASK_COMPLETE: 1.1 - Initialize project structure
TASK_BLOCKED: 2.3 - Missing API credentials
PHASE_COMPLETE
LOOP_COMPLETE
```
| Marker | Meaning |
|--------|---------|
| `TASK_COMPLETE: X.X - desc` | Task implemented and marked complete |
| `TASK_BLOCKED: X.X - reason` | Cannot complete task (explains why) |
| `PHASE_COMPLETE` | Current phase finished (phase mode only) |
| `LOOP_COMPLETE` | All phases finished |
In **phase mode**, the AI outputs multiple `TASK_COMPLETE` markers (one per task) within a single iteration, followed by `PHASE_COMPLETE` or `LOOP_COMPLETE`.
## Session Files
Session state is stored **per-spec** inside the spec directory:
```
specs/my-feature/
├── overview.md
├── phase-1.md
├── phase-2.md
└── .plan2code-loop/ # Per-spec session state
├── config.json # Session configuration
├── scratchpad.md # LLM-managed progress notes
├── iteration.log # JSON log of each iteration
└── spec.hash # Hash for detecting spec changes
```
The scratchpad is managed by the LLM itself - after each task, the AI appends notes about what was done, decisions made, and files changed. This helps future iterations skip exploration.
## Supported Agents
| Agent | Status |
|-------|--------|
| Claude Code | Supported |
| GitHub Copilot CLI | Supported |
The loop uses your configured default model for each agent.
## Example Session
```
$ plan2code-loop
╭──────────────────────────────────────╮
│ │
│ 🔮 Plany's Loop │
│ Autonomous Implementation │
│ │
╰──────────────────────────────────────╯
Found spec: specs/todo-app
Feature: Todo App
Phases: 0/3
Tasks: 0/15
══════════════════════════════════════════════
Spec: Todo App
══════════════════════════════════════════════
Phases: 0/3
Tasks: 0/15
? JIRA Ticket ID (optional): PROJ-123
? Select AI agent: Claude Code
? Tasks per loop iteration: One task per loop (default)
? Maximum iterations: 100
Starting Plan2Code Loop
══════════════════════════════════════════════
Agent: Claude Code
Model: default
Spec: C:\projects\my-app\specs\todo-app
Loop mode: One task per loop
Max iterations: 100
Iteration 1/100
[1/100] Task 1.1: Initialize project with Vite (45s)
✓ Completed: Task 1.1: Initialize project with Vite
Iteration 2/100
[2/100] Task 1.2: Configure TypeScript (32s)
✓ Completed: Task 1.2: Configure TypeScript
...
Session Summary
══════════════════════════════════════════════
Total iterations: 12
Tasks completed: 12
Exit reason: all_complete
Session files saved to specs/todo-app/.plan2code-loop:
- config.json (session configuration)
- scratchpad.md (LLM-managed notes)
- iteration.log (history)
```
## Development
```bash
# Install dependencies
npm install
# Build
npm run build
# Link for global usage
npm link
# Unlink
npm unlink
```
## Architecture
```
src/
├── bin/plan2code-loop.ts # CLI entry point
├── index.ts # Main exports
├── controller.ts # Loop orchestration
├── cli.ts # Interactive prompts
├── agents/ # Agent implementations
│ ├── claude-code.ts
│ └── copilot-cli.ts
├── prompt/ # Prompt building
│ ├── templates.ts
│ └── builder.ts
├── state/ # Session state management
│ └── manager.ts
├── spec/ # Spec utilities (for CLI display)
│ └── utils.ts
└── utils/ # Utilities
├── completion.ts # Marker parsing
└── logger.ts
```
+240 -240
View File
@@ -1,240 +1,240 @@
import path from 'path';
import { confirm, input, select } from '@inquirer/prompts';
import { agentRegistry } from './agents/index.js';
import { StateManager, type SessionConfig, type LoopMode } from './state/index.js';
import { detectSpecDirectories, getSpecProgress } from './spec/utils.js';
import { logger } from './utils/index.js';
export interface SessionSetupResult {
config: SessionConfig;
isResume: boolean;
}
/**
* Detect and select a spec directory
*/
async function selectSpec(cwd: string = process.cwd()): Promise<string | null> {
// Auto-detect spec directories
const specDirs = await detectSpecDirectories(cwd);
if (specDirs.length === 0) {
logger.error('No spec directories found!');
logger.info('Expected: specs/<feature>/overview.md');
logger.info('');
logger.info('To get started:');
logger.info('');
logger.info('1. Create a spec using `plan2code-1--plan`');
logger.info(' command in our AI Agent');
logger.info('');
logger.info('2. Come back here and run `plan2code-loop`');
logger.info(' as an alternative to `plan2code-3--implement`');
logger.info('');
return null;
}
if (specDirs.length === 1) {
const spec = specDirs[0];
const progress = await getSpecProgress(spec);
logger.info(`Found spec: ${path.relative(cwd, spec)}`);
logger.dim(` Feature: ${progress.featureName}`);
logger.dim(` Phases: ${progress.totalPhases}`);
return spec;
}
// Multiple specs - let user choose
const choices = await Promise.all(
specDirs.map(async (spec) => {
const progress = await getSpecProgress(spec);
const relativePath = path.relative(cwd, spec);
return {
name: `${progress.featureName} (${progress.totalPhases} phases) - ${relativePath}`,
value: spec,
};
})
);
const selectedSpec = await select({
message: 'Select spec to implement:',
choices,
});
return selectedSpec;
}
/**
* Select AI agent
*/
async function selectAgent(): Promise<string> {
const allAgents = agentRegistry.getAll();
const agentName = await select({
message: 'Select AI agent:',
choices: allAgents.map((agent) => ({
name: agent.config.displayName,
value: agent.config.name,
})),
});
return agentName;
}
/**
* Prompt for JIRA ticket ID
*/
async function promptJiraTicketId(): Promise<string | undefined> {
const ticketId = await input({
message: 'JIRA Ticket ID (optional, for commit messages):',
});
return ticketId.trim() || undefined;
}
/**
* Select maximum iterations
*/
async function selectMaxIterations(): Promise<number> {
const choices = [15, 30, 50, 75, 100, 125, 150, 200];
const max = await select({
message: 'Maximum iterations:',
choices: choices.map((n) => ({
name: n.toString(),
value: n,
})),
default: 100,
});
return max;
}
/**
* Select loop mode: one task per loop or one phase per loop
*/
async function selectLoopMode(): Promise<LoopMode> {
const mode = await select<LoopMode>({
message: 'Tasks per loop iteration:',
choices: [
{
name: 'One task per loop (default)',
value: 'task' as LoopMode,
},
{
name: 'One phase per loop (related tasks together)',
value: 'phase' as LoopMode,
},
],
default: 'task',
});
return mode;
}
/**
* Handle existing session - returns action to take
*/
async function handleExistingSession(
stateManager: StateManager,
specPath: string
): Promise<'continue' | 'fresh' | 'new'> {
const state = await stateManager.detectSessionState(specPath);
if (state === 'new') {
return 'new';
}
if (state === 'continue') {
const config = await stateManager.readConfig();
const lastIter = config?.currentIteration ?? 0;
logger.info(`Found existing session at iteration ${lastIter}`);
const continueSession = await confirm({
message: 'Continue previous session?',
default: true,
});
return continueSession ? 'continue' : 'fresh';
}
if (state === 'changed') {
logger.warning('Spec has changed since last session.');
const startFresh = await confirm({
message: 'Start fresh? (This will clear previous progress)',
default: false,
});
return startFresh ? 'fresh' : 'continue';
}
return 'new';
}
/**
* Setup session via interactive prompts
*/
export async function setupSession(
stateManager: StateManager
): Promise<SessionSetupResult | null> {
// Show Planny welcome
logger.welcome();
// Select spec
const specPath = await selectSpec(process.cwd());
if (!specPath) {
return null;
}
// Set spec path on state manager for per-spec state directory
stateManager.setSpecPath(specPath);
// Show spec progress
const progress = await getSpecProgress(specPath);
console.log();
logger.header(`Spec: ${progress.featureName}`);
logger.info(`Phases: ${progress.totalPhases}`);
console.log();
// Check for existing session
const sessionAction = await handleExistingSession(stateManager, specPath);
if (sessionAction === 'continue') {
const existingConfig = await stateManager.readConfig();
if (existingConfig) {
const agent = await selectAgent();
existingConfig.agent = agent;
await stateManager.writeConfig(existingConfig);
logger.info('Resuming previous session...');
return { config: existingConfig, isResume: true };
}
}
if (sessionAction === 'fresh') {
await stateManager.clearState();
}
// Collect new session configuration
const jiraTicketId = await promptJiraTicketId();
const agent = await selectAgent();
const loopMode = await selectLoopMode();
const maxIterations = await selectMaxIterations();
const config: SessionConfig = {
agent,
model: 'default',
maxIterations,
specPath,
timeout: 3,
maxRetries: 5,
verbose: false,
startedAt: new Date().toISOString(),
currentIteration: 0,
jiraTicketId,
loopMode,
};
// Initialize session
await stateManager.initializeNewSession(config);
return { config, isResume: false };
}
import path from 'path';
import { confirm, input, select } from '@inquirer/prompts';
import { agentRegistry } from './agents/index.js';
import { StateManager, type SessionConfig, type LoopMode } from './state/index.js';
import { detectSpecDirectories, getSpecProgress } from './spec/utils.js';
import { logger } from './utils/index.js';
export interface SessionSetupResult {
config: SessionConfig;
isResume: boolean;
}
/**
* Detect and select a spec directory
*/
async function selectSpec(cwd: string = process.cwd()): Promise<string | null> {
// Auto-detect spec directories
const specDirs = await detectSpecDirectories(cwd);
if (specDirs.length === 0) {
logger.error('No spec directories found!');
logger.info('Expected: specs/<feature>/overview.md');
logger.info('');
logger.info('To get started:');
logger.info('');
logger.info('1. Create a spec using `plan2code-1-plan`');
logger.info(' command in our AI Agent');
logger.info('');
logger.info('2. Come back here and run `plan2code-loop`');
logger.info(' as an alternative to `plan2code-3-implement`');
logger.info('');
return null;
}
if (specDirs.length === 1) {
const spec = specDirs[0];
const progress = await getSpecProgress(spec);
logger.info(`Found spec: ${path.relative(cwd, spec)}`);
logger.dim(` Feature: ${progress.featureName}`);
logger.dim(` Phases: ${progress.totalPhases}`);
return spec;
}
// Multiple specs - let user choose
const choices = await Promise.all(
specDirs.map(async (spec) => {
const progress = await getSpecProgress(spec);
const relativePath = path.relative(cwd, spec);
return {
name: `${progress.featureName} (${progress.totalPhases} phases) - ${relativePath}`,
value: spec,
};
})
);
const selectedSpec = await select({
message: 'Select spec to implement:',
choices,
});
return selectedSpec;
}
/**
* Select AI agent
*/
async function selectAgent(): Promise<string> {
const allAgents = agentRegistry.getAll();
const agentName = await select({
message: 'Select AI agent:',
choices: allAgents.map((agent) => ({
name: agent.config.displayName,
value: agent.config.name,
})),
});
return agentName;
}
/**
* Prompt for JIRA ticket ID
*/
async function promptJiraTicketId(): Promise<string | undefined> {
const ticketId = await input({
message: 'JIRA Ticket ID (optional, for commit messages):',
});
return ticketId.trim() || undefined;
}
/**
* Select maximum iterations
*/
async function selectMaxIterations(): Promise<number> {
const choices = [15, 30, 50, 75, 100, 125, 150, 200];
const max = await select({
message: 'Maximum iterations:',
choices: choices.map((n) => ({
name: n.toString(),
value: n,
})),
default: 100,
});
return max;
}
/**
* Select loop mode: one task per loop or one phase per loop
*/
async function selectLoopMode(): Promise<LoopMode> {
const mode = await select<LoopMode>({
message: 'Tasks per loop iteration:',
choices: [
{
name: 'One task per loop (default)',
value: 'task' as LoopMode,
},
{
name: 'One phase per loop (related tasks together)',
value: 'phase' as LoopMode,
},
],
default: 'task',
});
return mode;
}
/**
* Handle existing session - returns action to take
*/
async function handleExistingSession(
stateManager: StateManager,
specPath: string
): Promise<'continue' | 'fresh' | 'new'> {
const state = await stateManager.detectSessionState(specPath);
if (state === 'new') {
return 'new';
}
if (state === 'continue') {
const config = await stateManager.readConfig();
const lastIter = config?.currentIteration ?? 0;
logger.info(`Found existing session at iteration ${lastIter}`);
const continueSession = await confirm({
message: 'Continue previous session?',
default: true,
});
return continueSession ? 'continue' : 'fresh';
}
if (state === 'changed') {
logger.warning('Spec has changed since last session.');
const startFresh = await confirm({
message: 'Start fresh? (This will clear previous progress)',
default: false,
});
return startFresh ? 'fresh' : 'continue';
}
return 'new';
}
/**
* Setup session via interactive prompts
*/
export async function setupSession(
stateManager: StateManager
): Promise<SessionSetupResult | null> {
// Show Planny welcome
logger.welcome();
// Select spec
const specPath = await selectSpec(process.cwd());
if (!specPath) {
return null;
}
// Set spec path on state manager for per-spec state directory
stateManager.setSpecPath(specPath);
// Show spec progress
const progress = await getSpecProgress(specPath);
console.log();
logger.header(`Spec: ${progress.featureName}`);
logger.info(`Phases: ${progress.totalPhases}`);
console.log();
// Check for existing session
const sessionAction = await handleExistingSession(stateManager, specPath);
if (sessionAction === 'continue') {
const existingConfig = await stateManager.readConfig();
if (existingConfig) {
const agent = await selectAgent();
existingConfig.agent = agent;
await stateManager.writeConfig(existingConfig);
logger.info('Resuming previous session...');
return { config: existingConfig, isResume: true };
}
}
if (sessionAction === 'fresh') {
await stateManager.clearState();
}
// Collect new session configuration
const jiraTicketId = await promptJiraTicketId();
const agent = await selectAgent();
const loopMode = await selectLoopMode();
const maxIterations = await selectMaxIterations();
const config: SessionConfig = {
agent,
model: 'default',
maxIterations,
specPath,
timeout: 3,
maxRetries: 5,
verbose: false,
startedAt: new Date().toISOString(),
currentIteration: 0,
jiraTicketId,
loopMode,
};
// Initialize session
await stateManager.initializeNewSession(config);
return { config, isResume: false };
}
+3 -3
View File
@@ -106,17 +106,17 @@ export class Controller {
clearInterval(elapsedInterval);
spinner.stop();
// Cancelled or interrupted — return immediately, don't retry
// Cancelled or interrupted — return immediately, don't retry
if (result.cancelled || this.interrupted) {
return result;
}
// Completed (success or error exit code) — return to caller
// Completed (success or error exit code) — return to caller
if (!result.timedOut) {
return result;
}
// Timed out — retry if attempts remain, otherwise fatal
// Timed out — retry if attempts remain, otherwise fatal
if (attempt < maxAttempts - 1) {
continue;
}
+3 -2
View File
@@ -68,7 +68,7 @@ Use only when ALL phases in overview.md are marked complete.
## Scratchpad Management
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:
- Task completed and Phase item reference
@@ -94,6 +94,7 @@ export const LOOP_PROMPT_TEMPLATE_PHASE = `# PLAN2CODE-LOOP: Autonomous Phase Im
**IMPLEMENT ALL REMAINING TASKS IN THE CURRENT PHASE.**
Find the first incomplete phase, then implement every remaining task in that phase before stopping.
Complete each task fully before moving to the next task within the phase.
## Project Information
- **Project Root:** \`{{projectRoot}}\`
- **Spec Location:** \`{{specPath}}\`
@@ -178,7 +179,7 @@ After ALL tasks in the phase are complete (or blocked), output:
## Scratchpad Management
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:
- Task completed and Phase item reference
+126 -126
View File
@@ -1,126 +1,126 @@
import chalk from 'chalk';
import ora, { type Ora } from 'ora';
// mascot - our friendly robot assistant
export const MASCOT = {
// Full mascot for headers
full: [
' ╭───╮ ',
' │ ● │ ',
' │ ◡ │ ',
' ╰───╯ ',
],
// Mini mascot for inline use
mini: '(◉‿◉)',
// Waving mascot for greetings
wave: [
' ╭───╮ ',
' │ ● │ ',
' │ ◡ │ ',
' ╰───╯ ',
],
// Celebration mascot for completion
celebrate: [
' ╭───╮ ',
' │ ★ │ ',
' │ ◡ │ ',
' ╰───╯ ',
],
};
export const logger = {
info: (message: string) => console.log(chalk.blue('i'), message),
success: (message: string) => console.log(chalk.green('+'), message),
warning: (message: string) => console.log(chalk.yellow('!'), message),
error: (message: string) => console.log(chalk.red('x'), message),
dim: (message: string) => console.log(chalk.dim(message)),
bold: (message: string) => console.log(chalk.bold(message)),
header: (message: string) => {
console.log();
console.log(chalk.cyan.bold(message));
console.log(chalk.cyan('-'.repeat(message.length)));
},
task: (phase: number, taskId: string, description: string) => {
console.log(
chalk.cyan(`[Phase ${phase}]`),
chalk.yellow(`Task ${taskId}:`),
chalk.white(description)
);
},
iteration: (num: number, max: number, status: string) => {
console.log(
chalk.cyan(`[${num}/${max}]`),
chalk.white(status)
);
},
specContent: (content: string) => {
console.log(chalk.dim('-'.repeat(50)));
console.log(chalk.white(content));
console.log(chalk.dim('-'.repeat(50)));
},
spinner: (text: string): Ora => ora({ text, color: 'cyan' }).start(),
// Display mascot with optional message
mascot: (variant: keyof typeof MASCOT = 'full', message?: string) => {
console.log();
const mascot = MASCOT[variant];
if (Array.isArray(mascot)) {
const mascotLines = [...mascot];
if (message) {
// Add message next to mascot (at the "mouth" line)
mascotLines[4] = mascotLines[4] + ' ' + chalk.cyan(message);
}
mascotLines.forEach((line) => console.log(chalk.yellow(line)));
} else {
// Mini variant
console.log(chalk.yellow(mascot), message ? chalk.cyan(message) : '');
}
console.log();
},
// Welcome banner with mascot
welcome: () => {
console.log();
console.log(chalk.cyan.bold('═'.repeat(50)));
MASCOT.wave.forEach((line, i) => {
if (i === 4) {
console.log(chalk.yellow(line) + ' ' + chalk.cyan.bold("Hi!"));
} else if (i === 5) {
console.log(chalk.yellow(line) + ' ' + chalk.dim('I\'m Your Plan2Code assistant'));
} else {
console.log(chalk.yellow(line));
}
});
console.log(chalk.cyan.bold('═'.repeat(50)));
console.log();
},
// Completion celebration with reminder
allPhasesComplete: () => {
console.log();
console.log(chalk.green.bold('═'.repeat(50)));
MASCOT.celebrate.forEach((line, i) => {
if (i === 4) {
console.log(chalk.yellow(line) + ' ' + chalk.green.bold('All phases complete!'));
} else if (i === 5) {
console.log(chalk.yellow(line) + ' ' + chalk.cyan('Great work!'));
} else {
console.log(chalk.yellow(line));
}
});
console.log(chalk.green.bold('═'.repeat(50)));
console.log();
console.log(chalk.cyan.bold('Next Step:'));
console.log(chalk.white(' Return to your AI Agent and run the'), chalk.yellow.bold('/plan2code-4--finalize'), chalk.white('step.'));
console.log(chalk.dim(' This will ensure quality, completeness, and proper documentation.'));
console.log();
},
};
export type Logger = typeof logger;
import chalk from 'chalk';
import ora, { type Ora } from 'ora';
// mascot - our friendly robot assistant
export const MASCOT = {
// Full mascot for headers
full: [
' ╭───╮ ',
' │ ● │ ',
' │ ◡ │ ',
' ╰───╯ ',
],
// Mini mascot for inline use
mini: '(◉‿◉)',
// Waving mascot for greetings
wave: [
' ╭───╮ ',
' │ ● │ ',
' │ ◡ │ ',
' ╰───╯ ',
],
// Celebration mascot for completion
celebrate: [
' ╭───╮ ',
' │ ★ │ ',
' │ ◡ │ ',
' ╰───╯ ',
],
};
export const logger = {
info: (message: string) => console.log(chalk.blue('i'), message),
success: (message: string) => console.log(chalk.green('+'), message),
warning: (message: string) => console.log(chalk.yellow('!'), message),
error: (message: string) => console.log(chalk.red('x'), message),
dim: (message: string) => console.log(chalk.dim(message)),
bold: (message: string) => console.log(chalk.bold(message)),
header: (message: string) => {
console.log();
console.log(chalk.cyan.bold(message));
console.log(chalk.cyan('-'.repeat(message.length)));
},
task: (phase: number, taskId: string, description: string) => {
console.log(
chalk.cyan(`[Phase ${phase}]`),
chalk.yellow(`Task ${taskId}:`),
chalk.white(description)
);
},
iteration: (num: number, max: number, status: string) => {
console.log(
chalk.cyan(`[${num}/${max}]`),
chalk.white(status)
);
},
specContent: (content: string) => {
console.log(chalk.dim('-'.repeat(50)));
console.log(chalk.white(content));
console.log(chalk.dim('-'.repeat(50)));
},
spinner: (text: string): Ora => ora({ text, color: 'cyan' }).start(),
// Display mascot with optional message
mascot: (variant: keyof typeof MASCOT = 'full', message?: string) => {
console.log();
const mascot = MASCOT[variant];
if (Array.isArray(mascot)) {
const mascotLines = [...mascot];
if (message) {
// Add message next to mascot (at the "mouth" line)
mascotLines[4] = mascotLines[4] + ' ' + chalk.cyan(message);
}
mascotLines.forEach((line) => console.log(chalk.yellow(line)));
} else {
// Mini variant
console.log(chalk.yellow(mascot), message ? chalk.cyan(message) : '');
}
console.log();
},
// Welcome banner with mascot
welcome: () => {
console.log();
console.log(chalk.cyan.bold('═'.repeat(50)));
MASCOT.wave.forEach((line, i) => {
if (i === 4) {
console.log(chalk.yellow(line) + ' ' + chalk.cyan.bold("Hi!"));
} else if (i === 5) {
console.log(chalk.yellow(line) + ' ' + chalk.dim('I\'m Your Plan2Code assistant'));
} else {
console.log(chalk.yellow(line));
}
});
console.log(chalk.cyan.bold('═'.repeat(50)));
console.log();
},
// Completion celebration with reminder
allPhasesComplete: () => {
console.log();
console.log(chalk.green.bold('═'.repeat(50)));
MASCOT.celebrate.forEach((line, i) => {
if (i === 4) {
console.log(chalk.yellow(line) + ' ' + chalk.green.bold('All phases complete!'));
} else if (i === 5) {
console.log(chalk.yellow(line) + ' ' + chalk.cyan('Great work!'));
} else {
console.log(chalk.yellow(line));
}
});
console.log(chalk.green.bold('═'.repeat(50)));
console.log();
console.log(chalk.cyan.bold('Next Step:'));
console.log(chalk.white(' Return to your AI Agent and run the'), chalk.yellow.bold('/plan2code-4-finalize'), chalk.white('step.'));
console.log(chalk.dim(' This will ensure quality, completeness, and proper documentation.'));
console.log();
},
};
export type Logger = typeof logger;