mirror of
https://github.com/jparkerweb/plan2code.git
synced 2026-07-21 18:33:22 -07:00
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:
+210
-210
@@ -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
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user