From 34704eaf84ba4bd99f7ea7a428679453aa1ec832 Mon Sep 17 00:00:00 2001 From: Justin Parker Date: Tue, 27 Jan 2026 12:11:00 -0800 Subject: [PATCH] v1.5.2 --- .gitignore | 6 +- AGENTS.md | 179 +++++++++ CHANGELOG.md | 86 +++++ QUICK-REFERENCE.md | 28 +- README.md | 438 ++++++++++++++++++----- docs/install-script.jpg | Bin 46766 -> 150405 bytes install.js | 406 +++++++++++++++++++-- plan2code-loop/README.md | 195 ++++++++++ plan2code-loop/package.json | 45 +++ plan2code-loop/src/agents/claude-code.ts | 82 +++++ plan2code-loop/src/agents/copilot-cli.ts | 70 ++++ plan2code-loop/src/agents/index.ts | 19 + plan2code-loop/src/agents/registry.ts | 34 ++ plan2code-loop/src/agents/types.ts | 42 +++ plan2code-loop/src/bin/plan2code-loop.ts | 34 ++ plan2code-loop/src/cli.ts | 228 ++++++++++++ plan2code-loop/src/controller.ts | 314 ++++++++++++++++ plan2code-loop/src/index.ts | 93 +++++ plan2code-loop/src/prompt/builder.ts | 33 ++ plan2code-loop/src/prompt/index.ts | 6 + plan2code-loop/src/prompt/templates.ts | 85 +++++ plan2code-loop/src/spec/index.ts | 4 + plan2code-loop/src/spec/utils.ts | 70 ++++ plan2code-loop/src/state/config.ts | 29 ++ plan2code-loop/src/state/hash.ts | 9 + plan2code-loop/src/state/index.ts | 9 + plan2code-loop/src/state/manager.ts | 231 ++++++++++++ plan2code-loop/src/utils/completion.ts | 74 ++++ plan2code-loop/src/utils/git.ts | 134 +++++++ plan2code-loop/src/utils/index.ts | 17 + plan2code-loop/src/utils/logger.ts | 126 +++++++ plan2code-loop/src/utils/process.ts | 81 +++++ plan2code-loop/tsconfig.json | 20 ++ plan2code-loop/tsup.config.ts | 15 + src/plan2code---init-update.md | 183 ++++++---- src/plan2code---init.md | 68 +++- src/plan2code---quick-task.md | 22 ++ src/plan2code-1--plan.md | 29 +- src/plan2code-1b--revise-plan.md | 28 ++ src/plan2code-2--document.md | 51 ++- src/plan2code-3--implement.md | 178 ++++++++- src/plan2code-4--finalize.md | 17 +- version.json | 2 +- 43 files changed, 3603 insertions(+), 217 deletions(-) create mode 100644 AGENTS.md create mode 100644 plan2code-loop/README.md create mode 100644 plan2code-loop/package.json create mode 100644 plan2code-loop/src/agents/claude-code.ts create mode 100644 plan2code-loop/src/agents/copilot-cli.ts create mode 100644 plan2code-loop/src/agents/index.ts create mode 100644 plan2code-loop/src/agents/registry.ts create mode 100644 plan2code-loop/src/agents/types.ts create mode 100644 plan2code-loop/src/bin/plan2code-loop.ts create mode 100644 plan2code-loop/src/cli.ts create mode 100644 plan2code-loop/src/controller.ts create mode 100644 plan2code-loop/src/index.ts create mode 100644 plan2code-loop/src/prompt/builder.ts create mode 100644 plan2code-loop/src/prompt/index.ts create mode 100644 plan2code-loop/src/prompt/templates.ts create mode 100644 plan2code-loop/src/spec/index.ts create mode 100644 plan2code-loop/src/spec/utils.ts create mode 100644 plan2code-loop/src/state/config.ts create mode 100644 plan2code-loop/src/state/hash.ts create mode 100644 plan2code-loop/src/state/index.ts create mode 100644 plan2code-loop/src/state/manager.ts create mode 100644 plan2code-loop/src/utils/completion.ts create mode 100644 plan2code-loop/src/utils/git.ts create mode 100644 plan2code-loop/src/utils/index.ts create mode 100644 plan2code-loop/src/utils/logger.ts create mode 100644 plan2code-loop/src/utils/process.ts create mode 100644 plan2code-loop/tsconfig.json create mode 100644 plan2code-loop/tsup.config.ts diff --git a/.gitignore b/.gitignore index 54ff8fd..98ce459 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,7 @@ specs/ +specs--completed/ +CLAUDE.md dist/ -CLAUDE.md \ No newline at end of file +plan2code-loop/dist +plan2code-loop/node_modules +plan2code-loop/package-lock.json diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..f1d8ce1 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,179 @@ +# AGENTS.md + +This file provides guidance to AI coding agents like Claude Code (claude.ai/code), Cursor AI, Codex, Gemini CLI, GitHub Copilot, and other AI coding assistants when working with code in this repository. + +## Project Overview + +Plan2Code is a structured 4-step workflow methodology for AI-assisted software development. It provides prompt templates that can be installed globally or per-project for various AI coding tools (Claude Code, Cursor, Copilot, Continue, Windsurf, Codeium). + +**Version:** Check `version.json` for current version +**Author:** Justin Parker +**License:** MIT + +## Architecture + +``` +plan2code/ +├── src/ # Source workflow prompts (8 markdown files) +├── plan2code-loop/ # Autonomous loop CLI tool (Node.js/TypeScript) +│ ├── src/ # TypeScript source +│ └── dist/ # Built output (tsup) +├── dist/ # Generated distribution files (auto-generated) +│ ├── global-commands/ # For global installation (~/.claude/, etc.) +│ └── local-commands/ # For per-project installation (.claude/, etc.) +├── docs/ # Documentation and assets +├── specs/ # Feature specs (if any in-progress) +├── install.js # Interactive installer (Node.js) +├── version.json # Version metadata +└── README.md # User documentation +``` + +## Key Files + +| File | Purpose | +|------|---------| +| `install.js` | Main installer - generates and installs workflow files to AI tool directories | +| `src/plan2code-*.md` | Source workflow prompts (the "source of truth") | +| `version.json` | Version metadata (name, version, description) | +| `QUICK-REFERENCE.md` | User quick-reference card | + +## Workflow Prompts (in `src/`) + +| File | Step | Purpose | +|------|------|---------| +| `plan2code---init.md` | Init | Generate AGENTS.md for projects | +| `plan2code---init-update.md` | Update | Update AGENTS.md with learnings | +| `plan2code---quick-task.md` | 0 | Lightweight planning for small tasks | +| `plan2code-1--plan.md` | 1 | Requirements analysis & architecture | +| `plan2code-1b--revise-plan.md` | 1b | Mid-implementation revisions | +| `plan2code-2--document.md` | 2 | Create implementation specs | +| `plan2code-3--implement.md` | 3 | Execute implementation (phase by phase) | +| `plan2code-4--finalize.md` | 4 | Validate, summarize, archive | + +## Plan2Code Loop (`plan2code-loop/`) + +A separate Node.js CLI tool that autonomously implements specs by looping through tasks. + +### Loop Architecture + +The loop uses an **LLM-driven discovery** approach: +- Node app just orchestrates iterations and parses completion markers +- The LLM reads spec files (`overview.md`, `phase-X.md`) to discover tasks +- The LLM finds unchecked checkboxes, implements ONE task per iteration, marks it complete +- No regex parsing of markdown in Node - the AI handles all task discovery + +### Loop Commands + +```bash +# Build the loop CLI +cd plan2code-loop && npm run build + +# Run the loop (after linking) - fully interactive +plan2code-loop +``` + +The CLI auto-detects specs in `./specs/`, prompts for selection if multiple found, and handles session continuation interactively. Session state is stored per-spec in `specs//.plan2code-loop/`. + +### Completion Markers + +The LLM must output one of these formats: +- `TASK_COMPLETE: 1.1 - Task description` - Task done successfully +- `TASK_BLOCKED: 1.1 - Reason` - Cannot complete task +- `LOOP_COMPLETE` - All phases finished + +## Development Commands + +```bash +# Run the interactive installer +node install.js + +# Install to specific platform only +node install.js --platform claude +node install.js --platform cursor +node install.js --platform copilot +node install.js --platform continue +node install.js --platform windsurf +node install.js --platform codeium +node install.js --platform vscode-copilot + +# Preview changes without installing +node install.js --dry-run + +# Show local (per-project) installation instructions +node install.js --local + +# Uninstall all Plan2Code files +node install.js --uninstall + +# Plan2Code Loop +cd plan2code-loop && npm install # First time setup +cd plan2code-loop && npm run build # Build the CLI +``` + +### Installer Menu Options + +| Option | Action | +|--------|--------| +| `A` | Install to ALL platforms + build & link loop CLI | +| `O` | Build & link plan2code-loop CLI only | +| `U` | Uninstall prompts from all platforms + unlink loop CLI | +| `L` | Show local (per-project) install instructions | +| `1-7` | Install to specific platform | + +## How the Installer Works + +1. **Reads source prompts** from `src/plan2code-*.md` +2. **Generates platform-specific files** with appropriate headers (YAML frontmatter for some platforms) +3. **Writes to `dist/`** subdirectories organized by destination type +4. **Copies to target directories** (global: `~/.claude/commands/`, etc.) + +### Platform-Specific File Formats + +| Platform | Extension | Header | +|----------|-----------|--------| +| Claude Code | `.md` | None | +| Cursor | `.md` | None | +| Copilot CLI | `.md` | YAML frontmatter | +| Continue | `.prompt.md` | YAML frontmatter | +| Windsurf | `.md` | YAML frontmatter | +| VS Code Copilot | `.prompt.md` | YAML frontmatter | +| Codeium | `.md` | YAML frontmatter | + +## Naming Convention + +Workflow files follow a strict naming pattern: +- **Utilities:** `plan2code---.md` (triple dash) +- **Numbered steps:** `plan2code---.md` (single dash, number, double dash) + +Examples: +- `plan2code---init.md` (utility) +- `plan2code-1--plan.md` (step 1) +- `plan2code-1b--revise-plan.md` (step 1b) + +## Editing Workflow Prompts + +When modifying workflow prompts in `src/`: + +1. Edit the source file in `src/` +2. Run `node install.js` to regenerate distribution files +3. Test the workflow in your AI tool of choice +4. The `dist/` folder is regenerated automatically - don't edit files there directly + +## Code Style + +- **JavaScript:** CommonJS modules (Node.js built-ins only - no external dependencies) +- **Console output:** Uses ANSI color codes via the `COLORS` constant +- **User interaction:** Readline-based interactive prompts +- **File operations:** Synchronous fs operations for simplicity + +## Gotchas/Pitfalls + +- **Version sync:** When adding a new version to `CHANGELOG.md`, also update `version.json` to match. The installer displays the version from `version.json` in its header. +## Mascot +The project has a mascot - an ASCII art robot that appears in installer output and workflow prompts. Mascot variants are defined in `MASCOT` constant in `install.js` and appear in workflow markdown files. +``` + ╭───╮ + │ ● │ + │ ◡ │ + ╰───╯ +``` diff --git a/CHANGELOG.md b/CHANGELOG.md index 169e115..fea161d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,92 @@ All notable changes to Plan2Code will be documented in this file. +## v1.5.2 + +### ✨ Added + +- **AI Agent File Sync** - Init and Init-Update workflows now detect and sync other AI agent config files + - Detects 6 file types: CLAUDE.md, GEMINI.md, .cursorrules, .github/copilot-instructions.md, .cursor/rules/, .windsurf/rules/ + - Offers to replace with references to AGENTS.md as single source of truth + - User confirmation required before any modifications + - Correct relative paths for each file location (./AGENTS.md, ../AGENTS.md, ../../AGENTS.md) +- **Knowledge Transfer** - Init mode now uses existing CLAUDE.md content as context when creating new AGENTS.md + - Preserves project knowledge during migration to AGENTS.md +## v1.5.1 +### ✨ Added +- **Prerequisite verification workflow** - Agents now verify/complete prerequisites before starting phase tasks + - Implementation mode processes prerequisites in order: verify, complete, or mark assumed + - Loop prompt treats prerequisites as "Task 0.X" - one per iteration before tasks + - New completion markers: `PREREQ_COMPLETE` and `PREREQ_ASSUMED` +- **New checkbox state `[?]`** - "Assumed complete, couldn't verify" for prerequisites that can't be validated + - Use when prerequisite cannot be programmatically verified (e.g., "Design approved by stakeholder") + - Agents skip `[?]` items like `[x]` items +- **Re-opened phase handling in Revision mode** - Properly handle adding tasks to completed phases + - New "Re-opening" impact type (Medium-High risk) in impact assessment + - New tasks in completed phases get `🆕 ADDED` flag + - Phase checkbox changes from `[x]` to `[ ]` in overview.md when new tasks added + - "Phases Re-opened" section in revision summary + - Consistency check now verifies phase completion status matches task completion + +### 🔧 Changed +- **Installer UI refresh** - Cleaner, narrower layout for better terminal compatibility + - Narrower menu boxes (65 characters instead of 76) + - Smaller mascot display at end of installation + - Added first-time user documentation link after successful install + - Updated menu descriptions to show "+ loop CLI" for relevant options +- **Planning workflow guardrails** - Prevent users from skipping the documentation step + - Added critical reminder after Phase 7 to direct to `/plan2code-2--document` + - Added workflow order reminder in Session End section: Plan → Document → Implement → Finalize + - Updated example closing message to emphasize documentation as next step + - Added workflow order to Important Reminders section +- **AGENTS.md pre-flight message** - Improved guidance for new projects + - Message now explains that new projects can continue without AGENTS.md + - Suggests creating basic AGENTS.md first with rules can still be valuable + +## v1.5.0 - 2026-01-22 + +### ✨ Added +- **Plan2Code Loop** - New autonomous CLI tool for hands-off spec implementation + - Separate Node.js/TypeScript tool in `plan2code-loop/` directory + - LLM-driven task discovery - AI reads spec files and finds unchecked tasks + - Iterates through tasks one at a time, marking checkboxes as complete + - Structured completion markers: `TASK_COMPLETE: 1.1 - description` + - Session persistence with scratchpad and iteration logging + - Supports Claude Code and GitHub Copilot CLI agents +- **Installer integration** for loop CLI + - Option `A` now installs prompts to all platforms AND builds/links the loop CLI + - Option `O` builds and links plan2code-loop CLI only + - Option `U` uninstalls prompts AND unlinks the loop CLI + +### 📝 Documentation +- Updated README.md with "Autonomous Loop" section explaining when to use loop vs manual Step 3 +- Updated QUICK-REFERENCE.md with loop commands and decision tree +- Updated AGENTS.md with loop architecture, commands, and completion markers + +## v1.4.0 - 2026-01-09 + +### ✨ Added +- **Parallel Phase Execution** - Run multiple implementation phases simultaneously in separate agent instances + - Documentation Mode auto-detects parallel-eligible phases based on file conflicts and dependencies + - Implementation Mode presents phase selection UI when parallel options are available + - New "Parallel Execution Groups" section in `overview.md` tracks which phases can run together + - Conflict detection criteria: file overlap, prerequisite dependencies, data/output dependencies, shared state + - Users can start multiple `/plan2code-3--implement` sessions to work on different parallel phases +- **In-Progress Phase Tracking** - Track which phases are actively being worked on + - New `[/]` checkbox status indicates a phase is in-progress (between `[ ]` pending and `[x]` complete) + - Phases marked `[/]` when an agent starts working, `[x]` when user approves completion + - Aborted phases stay `[/]` to enable resume - never reset back to `[ ]` + - Parallel selection UI shows `[IN PROGRESS]` vs `[AVAILABLE]` status for each phase + - Single in-progress phase prompts user to confirm resume (prevents accidental overlap) + - Supports multiple agent sessions on parallel phases with clear visibility of what's active + +### 🔧 Changed +- **Documentation Mode process** - Added step 6 "Analyze phases for parallel execution eligibility" +- **Implementation Mode detection** - Now checks for parallel siblings before starting phase +- **Implementation Mode phase selection** - 4-case decision logic for parallel, resume, auto-start scenarios +- **Session end summaries** - Documentation Mode now reports parallel execution groups +- **Abort handling** - Phases remain `[/]` on abort with clear resume instructions + ## v1.3.3 - 2025-12-30 ### ✨ Added diff --git a/QUICK-REFERENCE.md b/QUICK-REFERENCE.md index 3b8cf62..49fb5bf 100644 --- a/QUICK-REFERENCE.md +++ b/QUICK-REFERENCE.md @@ -29,21 +29,34 @@ specs--completed/ # After Step 4 ## Key Rules - Start NEW conversation for each step (and each implementation phase) -- ONE phase per conversation +- ONE phase per conversation (but parallel phases can run in separate instances) - Reply "approved" to complete phases - 90% confidence required before planning completes - Never look in `specs--completed/` (it's archived specs) +## Phase Status +| Checkbox | Status | Meaning | +|----------|--------|---------| +| `[ ]` | Pending | Not started | +| `[/]` | In Progress | Agent working (or paused) | +| `[x]` | Complete | Approved | +## Parallel Execution +When phases have no file conflicts or dependencies, they can run simultaneously: +1. Documentation Mode auto-detects parallel-eligible phases +2. Implementation Mode shows selection UI with status for each phase +3. Run multiple `/plan2code-3--implement` instances on different phases +4. `[/]` status shows which phases are actively being worked on ## Quick Troubleshooting | Issue | Solution | -| ---------------------- | ----------------------------------------------- | +| ---------------------- | ------------------------------------------------- | | Lost context mid-phase | Attach spec files, say "resume from Task X.Y" | | Wrong phase started | Say "abort", start correct phase | | Need to change plan | Use `/plan2code-1b--revise-plan` | | Multiple spec folders | Specify which: "Continue with specs/user-auth/" | | Need AGENTS.md file | Use `/plan2code---init` to generate one | | Update AGENTS.md | Use `/plan2code---init-update` after sessions | +| Run phases in parallel | Check Parallel Execution Groups in overview.md | ## Workflow Decision @@ -59,8 +72,19 @@ Is it a quick, small task? └── No → /plan2code-1--plan (full workflow) ├── /plan2code-2--document ├── /plan2code-3--implement (repeat per phase) + │ └── OR: plan2code-loop (autonomous alternative) └── /plan2code-4--finalize Need to revise mid-implementation? └── /plan2code-1b--revise-plan ``` +## Autonomous Loop (Alternative) +The `plan2code-loop` CLI is an **alternative** to Step 3, not a replacement. +| Approach | Use When | +|----------|----------| +| `/plan2code-3--implement` | You want interactive control per phase | +| `plan2code-loop` | You want hands-off autonomous execution | +```bash +plan2code-loop # Fully interactive - auto-detects specs, prompts for options +``` +Session state stored per-spec in `specs//.plan2code-loop/` diff --git a/README.md b/README.md index 5010586..afb1e6e 100644 --- a/README.md +++ b/README.md @@ -38,118 +38,357 @@ See [QUICK-REFERENCE.md](QUICK-REFERENCE.md) for full reference card. ## Installation +Plan2Code includes an interactive installer that generates and installs workflow files for all major AI coding assistants. + ### Prerequisites -**Node.js** (v14 or later) is required. If you don't have it: -- Download from [nodejs.org](https://nodejs.org/) -- Or: `brew install node` (macOS) | `winget install OpenJS.NodeJS` (Windows) | `sudo apt install nodejs` (Linux) +The install script requires **Node.js** (v14 or later). If you don't have Node.js installed: +1. Download from [nodejs.org](https://nodejs.org/) +2. Or use a package manager: + - **macOS:** `brew install node` + - **Windows:** `winget install OpenJS.NodeJS` or `choco install nodejs` + - **Linux:** `sudo apt install nodejs` (Debian/Ubuntu) or `sudo dnf install nodejs` (Fedora) +### Supported Platforms +| Platform | Global Directory | Invocation | +| ---------------- | -------------------------------------- | ---------------------------- | +| Claude Code | `~/.claude/commands/` | `/plan2code-1--plan`, etc. | +| Copilot CLI | `~/.copilot/agents/` | `--agent=plan2code-1--plan` | +| Cursor | `~/.cursor/commands/` | `/plan2code-1--plan`, etc. | +| Continue | `~/.continue/prompts/` | `/plan2code-1--plan`, etc. | +| Windsurf | `~/.codeium/windsurf/global_workflows/`| `/plan2code-1--plan`, etc. | +| Codeium (IJ) | `~/.codeium/global_workflows/` | `/plan2code-1--plan`, etc. | +| VS Code Copilot | Platform-specific (see below) | Slash commands in chat | +**VS Code Copilot paths:** +- Windows: `%APPDATA%\Code\User\prompts\` +- macOS: `~/Library/Application Support/Code/User/prompts/` +- Linux: `~/.config/Code/User/prompts/` ### Quick Start ```bash -git clone https://github.com/jparkerweb/plan2code.git +# Clone the repository +git clone https://github.com/plan/plan2code.git cd plan2code +# Run the interactive installer node install.js ``` -The interactive installer will guide you through: -- **Global installation** (recommended) - commands available in all projects -- **Local/project installation** - commands for a specific project only -- **Uninstall** - remove previously installed files +The installer will display an interactive menu: - +``` +Available platforms: -### Supported Platforms + 1. Claude Code (~/.claude/commands/) + 2. Copilot CLI (~/.copilot/agents/) + 3. Cursor (~/.cursor/commands/) + 4. Continue (~/.continue/prompts/) + 5. Windsurf (~/.codeium/windsurf/global_workflows/) + 6. Codeium (IJ) (~/.codeium/global_workflows/) + 7. VS Code Copilot (%APPDATA%\Code\User\prompts\) + A. Install ALL platforms + loop CLI + O. Build/link plan2code-loop CLI only + L. Show local (project) install instructions + U. Uninstall Plan2Code files + unlink loop CLI + Q. Quit -| Platform | Global Location | Invocation | -|----------|-----------------|------------| -| Claude Code | `~/.claude/commands/` | `/plan2code-1--plan` | -| Copilot CLI | `~/.copilot/agents/` | `--agent=plan2code-1--plan` | -| VS Code Copilot | `~/Library/Application Support/Code/User/prompts/` | Slash commands | -| Windsurf | `~/.codeium/windsurf/global_workflows/` | `/plan2code-1--plan` | -| Cursor | `~/.cursor/commands/` | `/plan2code-1--plan` | -| Continue | `~/.continue/prompts/` | `/plan2code-1--plan` | -| Antigravity | `.agent/workflows/` (project only) | `/plan2code-1--plan` | +Enter choice (1-7, A, O, L, U, Q, or comma-separated like 1,3,5): +``` -### CLI Options +### Installer Options + +| Command | Description | +|---------|-------------| +| `node install.js` | Interactive mode - select platforms from menu | +| `node install.js --platform ` | Install to specific platform only | +| `node install.js --dry-run` | Preview what would be installed without making changes | +| `node install.js --local` | Show instructions for project-level installation | +| `node install.js --uninstall` | Remove installed files and unlink loop CLI | +| `node install.js --help` | Display help information | + +**Valid platform IDs:** `claude`, `copilot`, `cursor`, `continue`, `windsurf`, `codeium`, `vscode-copilot` + +**Examples:** ```bash -node install.js # Interactive menu -node install.js --platform claude # Install specific platform -node install.js --local # Install to current project instead of global -node install.js --dry-run # Preview what would be installed -node install.js --uninstall # Remove installed files -node install.js --help # Show all options +# Install to Claude Code only +node install.js --platform claude +# Install to multiple specific platforms +node install.js # Then enter "1,3,5" for Claude, Cursor, Windsurf + +# Preview installation without making changes +node install.js --dry-run +# Remove all installed files +node install.js --uninstall ``` +### Per-Project Installation +If you prefer project-specific configuration instead of global installation: +```bash +# Run the installer with --local flag +node install.js --local +``` +This will generate the distribution files and display instructions for copying them to your project. The generated files will be in `dist/local-commands/` organized by platform: +``` +dist/local-commands/ +├── .claude/commands/ # Claude Code +├── .cursor/commands/ # Cursor +├── .github/prompts/ # VS Code GitHub Copilot +├── .github/agents/ # GitHub Copilot CLI (agents) +├── .continue/prompts/ # Continue +├── .windsurf/workflows/ # Windsurf +└── .agent/workflows/ # Google Antigravity +``` +> **Note:** Some AI tools (Windsurf, Cursor, Continue) may not recognize workflows in gitignored directories. If your project's `.gitignore` includes patterns like `.windsurf/`, consider using global installation instead. +--- + +## Platform-Specific Notes +
+Claude Code CLI + +**Installation:** +```bash +node install.js --platform claude +# Or use interactive mode and select option 1 +``` + +**Global location:** `~/.claude/commands/` + +**Usage:** +```bash +/plan2code-1--plan # Start planning a new feature +/plan2code-2--document # Create implementation docs from plan +/plan2code-3--implement # Begin/continue implementation +/plan2code-4--finalize # Wrap up after all phases complete +``` + +Restart Claude Code or start a new session after installation. + +**Documentation:** [Claude Code Slash Commands](https://docs.anthropic.com/en/docs/claude-code/slash-commands) + +
+ ---
-Platform Details - -### Claude Code CLI - -Type `/plan2code-1--plan` in chat. Restart Claude Code after installation. - -**Docs:** [Claude Code Slash Commands](https://code.claude.com/docs/en/slash-commands) - -### GitHub Copilot CLI +GitHub Copilot CLI +**Installation:** ```bash +node install.js --platform copilot +# Or use interactive mode and select option 2 +``` + +**Global location:** `~/.copilot/agents/` + +Ensure GitHub Copilot CLI is installed: `npm install -g @github/copilot@latest` + +**Usage:** +```bash +# Using --agent flag copilot --agent=plan2code-1--plan --prompt "I want to build a REST API" + +# Using slash commands in interactive mode +copilot +> /agent plan2code-1--plan ``` -Requires: `npm install -g @github/copilot@latest` - -**Docs:** [GitHub Copilot CLI Custom Agents](https://docs.github.com/en/copilot/concepts/agents/about-copilot-cli) - -### VS Code GitHub Copilot - -Open Copilot Chat (`Ctrl+Shift+I`), type `/` to see prompts. Requires per-project install via `node install.js --local`. - -**Docs:** [VS Code Copilot Prompt Files](https://code.visualstudio.com/docs/copilot/customization/prompt-files) - -### Windsurf IDE - -Type `/plan2code-1--plan` in Cascade. Note: 12,000 character limit per workflow. - -**Docs:** [Windsurf Workflows](https://docs.windsurf.com/windsurf/cascade/workflows) - -### Cursor AI - -Type `/` in chat, select command from dropdown. - -**Docs:** [Cursor Commands](https://docs.cursor.com/agent/chat/commands) - -### Google Antigravity - -Type `/plan2code-1--plan` in chat. Requires per-project install via `node install.js --local`. - -**Docs:** [Customize Antigravity](https://atamel.dev/posts/2025/11-25_customize_antigravity_rules_workflows/) - -### Continue (VS Code/JetBrains) - -Type `/plan2code-1--plan` in chat. Install the Continue extension first. - -**Docs:** [Continue Prompts](https://docs.continue.dev/customize/deep-dives/prompts) +**Documentation:** [GitHub Copilot CLI Custom Agents](https://docs.github.com/en/copilot/concepts/agents/about-copilot-cli)
---
-Manual Installation +VS Code GitHub Copilot -If your AI tool isn't listed or you prefer manual setup: +**Installation:** +```bash +node install.js --platform vscode-copilot +# Or use interactive mode and select option 7 +``` -1. **Copy/Paste:** Copy contents from `src/plan2code-*.md` files into your conversation -2. **File Reference:** Tell the AI: `Please follow the instructions in src/plan2code-1--plan.md` -3. **Custom Integration:** Adapt files from `dist/` directories to your tool's format +**Global location:** Platform-specific (Windows: `%APPDATA%\Code\User\prompts\`, macOS: `~/Library/Application Support/Code/User/prompts/`, Linux: `~/.config/Code/User/prompts/`) + +**Usage:** +- Open Copilot Chat (Ctrl+Shift+I or Cmd+Shift+I) +- Type `/` to see available prompts +- Select the desired workflow step + +**Documentation:** [VS Code Copilot Prompt Files](https://code.visualstudio.com/docs/copilot/customization/prompt-files)
--- +
+Windsurf IDE + +**Installation:** +```bash +node install.js --platform windsurf +# Or use interactive mode and select option 5 +``` + +**Global location:** `~/.codeium/windsurf/global_workflows/` + +> **Note:** Windsurf has a 12,000 character limit per workflow file. + +**Usage:** +- In Cascade, type `/plan2code-1--plan` to invoke the planning workflow + +**Documentation:** [Windsurf Workflows](https://docs.windsurf.com/windsurf/cascade/workflows) + +
+ +--- + +
+Cursor AI + +**Installation:** +```bash +node install.js --platform cursor +# Or use interactive mode and select option 3 +``` + +**Global location:** `~/.cursor/commands/` + +**Usage:** +- Type `/` in Cursor chat to see available commands +- Select `plan2code-1--plan` from the dropdown +- Commands from both project and global directories appear automatically + +**Documentation:** [Cursor Commands](https://docs.cursor.com/agent/chat/commands) + +
+ +--- + +
+Continue (VS Code/JetBrains) + +**Installation:** +```bash +node install.js --platform continue +# Or use interactive mode and select option 4 +``` + +**Global location:** `~/.continue/prompts/` + +Install the Continue extension for VS Code or JetBrains. Prompts are automatically recognized. + +**Usage:** +- In Continue chat, type `/plan2code-1--plan` to invoke the planning workflow + +**Documentation:** [Continue Prompts](https://docs.continue.dev/customize/deep-dives/prompts) + +
+ +--- + +
+Codeium (IntelliJ) + +**Installation:** +```bash +node install.js --platform codeium +# Or use interactive mode and select option 6 +``` + +**Global location:** `~/.codeium/global_workflows/` + +**Usage:** +- Type `/plan2code-1--plan` in the Codeium chat to invoke workflows + +
+ +--- + +
+Google Antigravity + +**Installation:** Use the `--local` option and copy files to your project: +```bash +node install.js --local +# Then copy dist/local-commands/.agent to your project +``` + +**Project location:** `.agent/workflows/` (per-project only) + +> **Note:** Antigravity requires per-project installation. + +**Usage:** +- Type `/plan2code-1--plan` in the agent chat to invoke the planning workflow + +**Documentation:** [Customize Antigravity](https://atamel.dev/posts/2025/11-25_customize_antigravity_rules_workflows/) + +
+ +--- + +## Autonomous Loop (Alternative to Step 3) + +For hands-off implementation, Plan2Code includes an optional autonomous loop CLI that iterates through your spec tasks automatically. + +> **Note:** The loop is an **alternative** to `/plan2code-3--implement`, not a replacement. Use the manual Step 3 workflow when you want direct control over each phase, or use the loop when you prefer autonomous execution. + +### When to Use Each + +| Approach | Best For | +|----------|----------| +| `/plan2code-3--implement` | Interactive control, reviewing each phase, complex logic requiring human judgment | +| `plan2code-loop` | Straightforward implementations, batch processing, overnight runs | + +### Installing the Loop + +```bash +# Option 1: Install everything (prompts + loop) +node install.js # Select option A + +# Option 2: Install loop only +node install.js # Select option O +``` + +### Using the Loop + +```bash +# Run the loop - fully interactive +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, and max iterations + +Session state is stored per-spec in `specs//.plan2code-loop/`, keeping each feature's progress isolated. + +The loop will: +1. Read your `overview.md` and phase files +2. Find the first unchecked task +3. Implement it and mark the checkbox complete +4. Repeat until all tasks are done or max iterations reached + +See [plan2code-loop/](plan2code-loop/) for full documentation. + +--- + +### Manual Usage (No Installation) + +If you prefer not to install, you can use Plan2Code prompts directly: + +1. **Copy/Paste Method:** Copy the contents of the appropriate `src/plan2code-*.md` file and paste it at the start of your conversation. + +2. **File Reference Method:** Reference the file directly in your prompt: + ``` + Please follow the instructions in src/plan2code-1--plan.md + + I want to build a user authentication system. + ``` + +--- + ## Important: Start Fresh Conversations **Start a new conversation/chat session before each step.** This includes: @@ -202,12 +441,14 @@ Fresh conversations prevent context pollution and ensure the AI focuses on the c ``` specs/ └── / - ├── overview.md # High-level overview with phase checkboxes + ├── overview.md # High-level overview with phase checkboxes and parallel groups ├── Phase 1.md # Detailed tasks for Phase 1 ├── Phase 2.md # Detailed tasks for Phase 2 └── Phase N.md # ...additional phases ``` +The `overview.md` includes a "Parallel Execution Groups" section that identifies which phases can be run simultaneously in separate agent instances. + **Document Format:** - Each phase file contains detailed one-story-point tasks @@ -228,11 +469,14 @@ specs/ **Workflow:** 1. Identify the next uncompleted phase (unchecked in `overview.md`) -2. Implement ALL tasks in that phase exactly as specified -3. Update `Phase X.md` checkboxes as tasks complete `[x]` -4. Update `overview.md` phase checkbox when phase completes -5. Perform code review to ensure nothing was missed -6. Add completion summary to the phase document +2. Check for parallel execution options (if phases can run simultaneously) +3. Implement ALL tasks in that phase exactly as specified +4. Update `Phase X.md` checkboxes as tasks complete `[x]` +5. Update `overview.md` phase checkbox when phase completes +6. Perform code review to ensure nothing was missed +7. Add completion summary to the phase document + +**Parallel Execution:** If the next phase is part of a parallel-eligible group, you'll be prompted to choose which phase to implement. This allows running multiple agent instances simultaneously on different phases that don't conflict with each other. **Key Rules:** @@ -264,15 +508,13 @@ specs/ ### Option 1: Platform-Specific Slash Commands (Recommended) -This repository includes pre-configured workflow files for all major AI coding assistants. See the [Installation](#installation) section above for platform-specific setup instructions. - -**Quick commands after setup:** +After running `node install.js`, use the slash commands directly in your AI tool: ``` -/plan # Start planning a new feature -/document # Create implementation docs from plan -/implement # Begin/continue implementation -/finalize # Wrap up after all phases complete +/plan2code-1--plan # Start planning a new feature +/plan2code-2--document # Create implementation docs from plan +/plan2code-3--implement # Begin/continue implementation +/plan2code-4--finalize # Wrap up after all phases complete ``` ### Option 2: Direct File Reference @@ -372,18 +614,24 @@ AI: 🧹 [SPEC CLEANUP] The checkbox system enables seamless progress tracking across multiple sessions: -**overview.md:** +**Phase Status (overview.md):** + +| Checkbox | Status | Meaning | +|----------|--------|---------| +| `[ ]` | Pending | Not yet started | +| `[/]` | In Progress | Agent actively working (or paused/aborted) | +| `[x]` | Complete | Finished and approved | ```markdown ## Phases - [x] Phase 1: Project Setup - [x] Phase 2: Database Models -- [ ] Phase 3: API Endpoints <- Next phase to implement -- [ ] Phase 4: Authentication +- [/] Phase 3: API Endpoints <- In progress (agent working) +- [ ] Phase 4: Authentication <- Next available ``` -**Phase 3.md:** +**Task Status (phase-X.md):** ```markdown ## Tasks @@ -395,6 +643,8 @@ The checkbox system enables seamless progress tracking across multiple sessions: - [ ] Implement DELETE /tasks/:id ``` +The `[/]` status enables parallel execution - multiple agents can work on different phases simultaneously, and you can see which phases are actively being worked on. + --- ## Best Practices @@ -456,9 +706,9 @@ Feel free to modify these prompts to fit your workflow: **Slash commands/workflows not recognized:** -- Check if your project's `.gitignore` includes patterns like `.windsurf/`, `.cursor/`, `.continue/`, or `.agent/` -- Many AI tools don't recognize workflows in gitignored directories -- Solution: Use global installation by copying the directories to your home directory (e.g., `cp -r .windsurf ~/`) +- Ensure you ran `node install.js` and selected the appropriate platform +- Restart your AI tool after installation +- For per-project installation, ensure the directory isn't in `.gitignore` **AI jumps ahead to implementation during planning:** @@ -481,5 +731,3 @@ Feel free to modify these prompts to fit your workflow: **Too many/few phases:** - Adjust during Step 2 (Documentation) - phases should represent logical groupings of work - - diff --git a/docs/install-script.jpg b/docs/install-script.jpg index a3c66f1187f3039554f6b76e3d09089be779cc9f..17f53ce1ea7afeb3a6044d73c2a01a152f0a61d7 100644 GIT binary patch literal 150405 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%0DmLVZV|H_X#yu)fxip{tk)Ah{aGD2QTVhSpVJ}HZ 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

dmeMiu>g-?7G1;1hZ$01Y44 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 W|4FkCiXr}+4C{YfGQ9Y6;=cf`1`GoL literal 46766 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 Z(R5tP^Ygsd){?HY0G+nr*>C 0; // DISPLAY FUNCTIONS // ============================================================================ +/** + * Display mascot with optional message + */ +function displayMascot(variant = 'full', message = '') { + const mascotLines = MASCOT[variant] || MASCOT.full; + console.log(''); + mascotLines.forEach(line => { + console.log(`${COLORS.MAGENTA}${line}${COLORS.RESET}`); + }); + if (message) { + console.log(`${COLORS.CYAN}${message}${COLORS.RESET}`); + } + console.log(''); +} + /** * Display header */ @@ -441,6 +482,10 @@ function displayHeader() { console.log(''); console.log(`${COLORS.GREEN}${COLORS.BRIGHT}`); console.log('╔═════════════════════════════════════════════════════════════════════════════════╗'); + console.log('║ ╭───╮ ║'); + console.log('║ │ ● │ Hi! ║'); + console.log('║ │ ◡ │ Nice to meet you ║'); + console.log('║ ╰───╯ ║'); console.log('║ ║'); console.log('║ ██████╗ ██╗ █████╗ ███╗ ██╗ ██████╗ ██████╗ ██████╗ ██████╗ ███████╗ ║'); console.log('║ ██╔══██╗██║ ██╔══██╗████╗ ██║ ╚════██╗ ██╔════╝██╔═══██╗██╔══██╗██╔════╝ ║'); @@ -529,6 +574,12 @@ function displayHelp() { `${COLORS.CYAN}◆${COLORS.RESET} ${COLORS.BRIGHT}node install.js --uninstall${COLORS.RESET}`, ` ${COLORS.DIM}Remove all installed Plan2Code files${COLORS.RESET}`, '', + `${COLORS.CYAN}◆${COLORS.RESET} ${COLORS.BRIGHT}node install.js --loop${COLORS.RESET}`, + ` ${COLORS.DIM}Build and install plan2code-loop CLI tool${COLORS.RESET}`, + '', + `${COLORS.CYAN}◆${COLORS.RESET} ${COLORS.BRIGHT}node install.js --uninstall-loop${COLORS.RESET}`, + ` ${COLORS.DIM}Remove plan2code-loop global symlink${COLORS.RESET}`, + '', `${COLORS.CYAN}◆${COLORS.RESET} ${COLORS.BRIGHT}node install.js --help${COLORS.RESET}`, ` ${COLORS.DIM}Display this help information${COLORS.RESET}`, ]); @@ -919,7 +970,18 @@ function install(targets = null) { console.log(`${COLORS.CYAN}║${COLORS.RESET}${' '.repeat(75)}${COLORS.CYAN}║${COLORS.RESET}`); console.log(`${COLORS.CYAN}╚═══════════════════════════════════════════════════════════════════════════╝${COLORS.RESET}`); - if (dryRun) { + // Show celebratory or sad mascot based on result + console.log(''); + if (totalErrors === 0 && !dryRun) { + console.log(`${COLORS.GREEN} ╭───╮${COLORS.RESET}`); + console.log(`${COLORS.GREEN} │ ${COLORS.CYAN}★${COLORS.GREEN} │${COLORS.RESET} ${COLORS.BRIGHT}All done! Happy coding!${COLORS.RESET}`); + console.log(`${COLORS.GREEN} │ ${COLORS.BRIGHT}◡${COLORS.GREEN} │${COLORS.RESET} ${COLORS.BRIGHT}If this is your first time using Plan2Code, read the docs here:${COLORS.RESET}`); + console.log(`${COLORS.GREEN} ╰───╯${COLORS.RESET} ${COLORS.BRIGHT}https://github.com/jparkerweb/plan2code${COLORS.RESET}`); + } else if (dryRun) { + console.log(`${COLORS.YELLOW} ╭───╮${COLORS.RESET}`); + console.log(`${COLORS.YELLOW} │ ${COLORS.CYAN}●${COLORS.YELLOW} │${COLORS.RESET}`); + console.log(`${COLORS.YELLOW} │ ○ │${COLORS.RESET} ${COLORS.DIM}That was just a preview!${COLORS.RESET}`); + console.log(`${COLORS.YELLOW} ╰───╯${COLORS.RESET}`); console.log(''); console.log(`${COLORS.YELLOW}${SYMBOLS.INFO} Run without --dry-run to install files${COLORS.RESET}`); } @@ -1085,12 +1147,9 @@ function displayLocalInstructions() { { name: 'Claude Code', dir: '.claude/commands/', desc: 'Slash commands for Claude Code' }, { name: 'Cursor', dir: '.cursor/commands/', desc: 'Slash commands for Cursor IDE' }, { name: 'GitHub Copilot', dir: '.github/prompts/', desc: 'Prompts for GitHub Copilot' }, - { name: 'GitHub Agents', dir: '.github/agents/', desc: 'Agent definitions for Copilot' }, { name: 'Continue', dir: '.continue/prompts/', desc: 'Prompts for Continue extension' }, { name: 'Windsurf', dir: '.windsurf/workflows/', desc: 'Workflows for Windsurf' }, - { name: 'Windsurf Global', dir: '.codeium/windsurf/global_workflows/', desc: 'Global workflows' }, - { name: 'Agent', dir: '.agent/workflows/', desc: 'Agent workflows' }, - { name: 'Codeium', dir: '.codeium/global_workflows/', desc: 'Codeium global workflows' }, + { name: 'Codeium (IntelliJ)', dir: '.codeium/global_workflows/', desc: 'Codeium global workflows' }, ]; localDirs.forEach((item, i) => { @@ -1146,9 +1205,9 @@ function runInteractive() { console.log(`${COLORS.BLUE}${COLORS.BRIGHT}Version Info:${COLORS.RESET} ${projectVersion.name} ${projectVersion.version}`); console.log(`${COLORS.GREEN}${SYMBOLS.ACTIVE} SELECT PLATFORMS${COLORS.RESET}\n`); - console.log(`${COLORS.CYAN}╔═══════════════════════════════════════════════════════════════════════════╗${COLORS.RESET}`); - console.log(padEndVisible(`${COLORS.CYAN}║${COLORS.RESET} ${COLORS.BRIGHT}AVAILABLE PLATFORMS${COLORS.RESET}`, 76) + `${COLORS.CYAN}║${COLORS.RESET}`); - console.log(`${COLORS.CYAN}╠═══════════════════════════════════════════════════════════════════════════╣${COLORS.RESET}`); + console.log(`${COLORS.CYAN}╔════════════════════════════════════════════════════════════════╗${COLORS.RESET}`); + console.log(padEndVisible(`${COLORS.CYAN}║${COLORS.RESET} ${COLORS.BRIGHT}AVAILABLE PLATFORMS${COLORS.RESET}`, 65) + `${COLORS.CYAN}║${COLORS.RESET}`); + console.log(`${COLORS.CYAN}╠════════════════════════════════════════════════════════════════╣${COLORS.RESET}`); INSTALL_TARGETS.forEach((target, i) => { const num = `${COLORS.BRIGHT}${i + 1}.${COLORS.RESET}`; @@ -1156,18 +1215,25 @@ function runInteractive() { const name = `${COLORS.BRIGHT}${target.name.padEnd(14)}${COLORS.RESET}`; const targetPath = `${COLORS.DIM}${getDisplayPath(target)}${COLORS.RESET}`; - console.log(padEndVisible(`${COLORS.CYAN}║${COLORS.RESET} ${num} ${icon} ${name} ${targetPath}`, 76) + `${COLORS.CYAN}║${COLORS.RESET}`); + console.log(padEndVisible(`${COLORS.CYAN}║${COLORS.RESET} ${num} ${icon} ${name} ${targetPath}`, 65) + `${COLORS.CYAN}║${COLORS.RESET}`); }); - console.log(`${COLORS.CYAN}╠═══════════════════════════════════════════════════════════════════════════╣${COLORS.RESET}`); - console.log(padEndVisible(`${COLORS.CYAN}║${COLORS.RESET} ${COLORS.BRIGHT}A.${COLORS.RESET} ${COLORS.MAGENTA}◉ ALL${COLORS.RESET} ${COLORS.BRIGHT}Install to ALL platforms${COLORS.RESET}`, 76) + `${COLORS.CYAN}║${COLORS.RESET}`); - console.log(padEndVisible(`${COLORS.CYAN}║${COLORS.RESET} ${COLORS.BRIGHT}L.${COLORS.RESET} ${COLORS.BLUE}◉ LOCAL${COLORS.RESET} ${COLORS.BRIGHT}Show local (project) install instructions${COLORS.RESET}`, 76) + `${COLORS.CYAN}║${COLORS.RESET}`); - console.log(padEndVisible(`${COLORS.CYAN}║${COLORS.RESET} ${COLORS.BRIGHT}U.${COLORS.RESET} ${COLORS.RED}◉ UNINSTALL${COLORS.RESET} ${COLORS.BRIGHT}Remove Plan2Code files${COLORS.RESET}`, 76) + `${COLORS.CYAN}║${COLORS.RESET}`); - console.log(padEndVisible(`${COLORS.CYAN}║${COLORS.RESET} ${COLORS.BRIGHT}Q.${COLORS.RESET} ${COLORS.DIM}◉ QUIT${COLORS.RESET} ${COLORS.BRIGHT}Exit${COLORS.RESET}`, 76) + `${COLORS.CYAN}║${COLORS.RESET}`); - console.log(`${COLORS.CYAN}╚═══════════════════════════════════════════════════════════════════════════╝${COLORS.RESET}`); + console.log(`${COLORS.CYAN}╠════════════════════════════════════════════════════════════════╣${COLORS.RESET}`); + console.log(padEndVisible(`${COLORS.CYAN}║${COLORS.RESET} ${COLORS.BRIGHT}A.${COLORS.RESET} ${COLORS.MAGENTA}◉ ALL${COLORS.RESET} ${COLORS.BRIGHT}Install to ALL platforms + loop CLI${COLORS.RESET}`, 65) + `${COLORS.CYAN}║${COLORS.RESET}`); + console.log(padEndVisible(`${COLORS.CYAN}║${COLORS.RESET} ${COLORS.BRIGHT}L.${COLORS.RESET} ${COLORS.BLUE}◉ LOCAL${COLORS.RESET} ${COLORS.BRIGHT}Show local (project) install instructions${COLORS.RESET}`, 65) + `${COLORS.CYAN}║${COLORS.RESET}`); + console.log(padEndVisible(`${COLORS.CYAN}║${COLORS.RESET} ${COLORS.BRIGHT}O.${COLORS.RESET} ${COLORS.GREEN}◉ LOOP${COLORS.RESET} ${COLORS.BRIGHT}Install plan2code-loop CLI only${COLORS.RESET}`, 65) + `${COLORS.CYAN}║${COLORS.RESET}`); + console.log(padEndVisible(`${COLORS.CYAN}║${COLORS.RESET} ${COLORS.BRIGHT}U.${COLORS.RESET} ${COLORS.RED}◉ UNINSTALL${COLORS.RESET} ${COLORS.BRIGHT}Remove Plan2Code files + loop CLI${COLORS.RESET}`, 65) + `${COLORS.CYAN}║${COLORS.RESET}`); + console.log(padEndVisible(`${COLORS.CYAN}║${COLORS.RESET} ${COLORS.BRIGHT}Q.${COLORS.RESET} ${COLORS.DIM}◉ QUIT${COLORS.RESET} ${COLORS.BRIGHT}Exit${COLORS.RESET}`, 65) + `${COLORS.CYAN}║${COLORS.RESET}`); + console.log(`${COLORS.CYAN}╚════════════════════════════════════════════════════════════════╝${COLORS.RESET}`); console.log(''); - const answer = await question(`${COLORS.CYAN}${SYMBOLS.SELECT} SELECT OPTION${COLORS.RESET} (1-7, A, L, U, Q, or 1,3,5): `); + // Show thinking mascot for input prompt + console.log(`${COLORS.MAGENTA} ╭───╮${COLORS.RESET}`); + console.log(`${COLORS.MAGENTA} │ ${COLORS.CYAN}●${COLORS.MAGENTA} │${COLORS.RESET} ${COLORS.DIM}What would you like to do?${COLORS.RESET}`); + console.log(`${COLORS.MAGENTA} │ ${COLORS.YELLOW}~${COLORS.MAGENTA} │${COLORS.RESET} ${COLORS.GREEN}I suggest A: 'Install to ALL platforms + loop CLI'${COLORS.RESET}`); + console.log(`${COLORS.MAGENTA} ╰───╯${COLORS.RESET}`); + console.log(''); + const answer = await question(`${COLORS.CYAN}${SYMBOLS.SELECT} SELECT OPTION${COLORS.RESET} (1-7, A, L, O, U, Q, or 1,3,5): `); const input = answer.trim().toUpperCase(); if (input === 'Q' || input === '') { @@ -1181,6 +1247,11 @@ function runInteractive() { rl.close(); process.exit(displayLocalInstructions()); } + if (input === 'O') { + // Install plan2code-loop + rl.close(); + process.exit(installPlan2CodeLoop()); + } if (input === 'U') { // Uninstall flow @@ -1199,11 +1270,17 @@ function runInteractive() { }); console.log(`${COLORS.CYAN}╠═══════════════════════════════════════════════════════════════════════════╣${COLORS.RESET}`); - console.log(padEndVisible(`${COLORS.CYAN}║${COLORS.RESET} ${COLORS.BRIGHT}A.${COLORS.RESET} ${COLORS.RED}◉ ALL${COLORS.RESET} ${COLORS.BRIGHT}Uninstall from ALL platforms${COLORS.RESET}`, 76) + `${COLORS.CYAN}║${COLORS.RESET}`); + console.log(padEndVisible(`${COLORS.CYAN}║${COLORS.RESET} ${COLORS.BRIGHT}A.${COLORS.RESET} ${COLORS.RED}◉ ALL${COLORS.RESET} ${COLORS.BRIGHT}Uninstall from ALL platforms + loop CLI${COLORS.RESET}`, 76) + `${COLORS.CYAN}║${COLORS.RESET}`); console.log(padEndVisible(`${COLORS.CYAN}║${COLORS.RESET} ${COLORS.BRIGHT}Q.${COLORS.RESET} ${COLORS.DIM}◉ CANCEL${COLORS.RESET} ${COLORS.BRIGHT}Cancel${COLORS.RESET}`, 76) + `${COLORS.CYAN}║${COLORS.RESET}`); console.log(`${COLORS.CYAN}╚═══════════════════════════════════════════════════════════════════════════╝${COLORS.RESET}`); console.log(''); + // Show worried mascot for uninstall prompt + console.log(`${COLORS.RED} ╭───╮${COLORS.RESET}`); + console.log(`${COLORS.RED} │ ${COLORS.YELLOW}○${COLORS.RED} │${COLORS.RESET}`); + console.log(`${COLORS.RED} │ ${COLORS.YELLOW}~${COLORS.RED} │${COLORS.RESET} ${COLORS.DIM}Are you sure about this?${COLORS.RESET}`); + console.log(`${COLORS.RED} ╰───╯${COLORS.RESET}`); + console.log(''); const uninstallAnswer = await question(`${COLORS.RED}${SYMBOLS.SELECT} SELECT PLATFORMS TO UNINSTALL${COLORS.RESET} (1-7, A, Q, or 1,3,5): `); const uninstallInput = uninstallAnswer.trim().toUpperCase(); @@ -1239,7 +1316,13 @@ function runInteractive() { } rl.close(); - process.exit(uninstallFiles(targets)); + const uninstallResult = uninstallFiles(targets); + // If uninstalling from ALL platforms, also unlink plan2code-loop + if (uninstallInput === 'A') { + console.log(''); + uninstallPlan2CodeLoop(); + } + process.exit(uninstallResult); } // Install flow @@ -1260,7 +1343,13 @@ function runInteractive() { } const platformNames = targets.map(t => `${COLORS.YELLOW}${t.name}${COLORS.RESET}`).join(', '); - const confirm = await question(`\n${COLORS.GREEN}${SYMBOLS.SELECT} CONFIRM INSTALL${COLORS.RESET} to ${targets.length} platform(s): ${platformNames}? (Y/n): `); + // Show happy mascot for install confirmation + console.log(''); + console.log(`${COLORS.GREEN} ╭───╮${COLORS.RESET}`); + console.log(`${COLORS.GREEN} │ ${COLORS.CYAN}●${COLORS.GREEN} │${COLORS.RESET}`); + console.log(`${COLORS.GREEN} │ ${COLORS.BRIGHT}◡${COLORS.GREEN} │${COLORS.RESET} ${COLORS.DIM}Ready to install!${COLORS.RESET}`); + console.log(`${COLORS.GREEN} ╰───╯${COLORS.RESET}`); + const confirm = await question(`\n${COLORS.GREEN}${SYMBOLS.SELECT} CONFIRM INSTALL${COLORS.RESET}? (Y/n): `); if (confirm.trim().toLowerCase() === 'n') { console.log(`\n${COLORS.YELLOW}${SYMBOLS.WARNING} CANCELLED${COLORS.RESET}\n`); @@ -1269,7 +1358,17 @@ function runInteractive() { } rl.close(); - process.exit(install(targets)); + // If installing ALL platforms, install plan2code-loop first + let loopResult = 0; + if (input === 'A') { + loopResult = installPlan2CodeLoop(); + console.log(''); + } + const installResult = install(targets); + if (input === 'A') { + process.exit(installResult === 0 && loopResult === 0 ? 0 : 1); + } + process.exit(installResult); } main().catch(err => { @@ -1279,12 +1378,277 @@ function runInteractive() { }); } +// ============================================================================ +// PLAN2CODE-LOOP INSTALLATION +// ============================================================================ +const { execSync, spawn } = require('child_process'); +/** + * Build plan2code-loop package + */ +function buildPlan2CodeLoop() { + const loopDir = path.join(__dirname, 'plan2code-loop'); + // Check if directory exists + if (!fs.existsSync(loopDir)) { + console.log(`${COLORS.YELLOW}${SYMBOLS.WARNING}${COLORS.RESET} plan2code-loop directory not found`); + return false; + } + console.log(`${COLORS.CYAN}${SYMBOLS.ACTIVE} Building plan2code-loop...${COLORS.RESET}`); + try { + // Install dependencies + console.log(` ${COLORS.DIM}Installing dependencies...${COLORS.RESET}`); + execSync('npm install', { cwd: loopDir, stdio: 'pipe' }); + // Build + console.log(` ${COLORS.DIM}Running build...${COLORS.RESET}`); + execSync('npm run build', { cwd: loopDir, stdio: 'pipe' }); + console.log(` ${COLORS.GREEN}${SYMBOLS.SUCCESS}${COLORS.RESET} Build complete`); + return true; + } catch (error) { + console.log(` ${COLORS.RED}${SYMBOLS.ERROR}${COLORS.RESET} Build failed: ${error.message}`); + return false; + } +} +/** + * Clean up existing plan2code-loop symlinks/files before creating new ones + * This prevents the Windows lstat error with stale symlinks + */ +function cleanupExistingSymlinks() { + console.log(` ${COLORS.DIM}Cleaning up existing symlinks...${COLORS.RESET}`); + + try { + // Get npm global prefix - more reliable than npm bin -g on Windows + const npmPrefix = execSync('npm config get prefix', { encoding: 'utf8' }).trim(); + + // On Windows, bin files are directly in prefix; on Unix they're in prefix/bin + const npmBinGlobal = process.platform === 'win32' ? npmPrefix : path.join(npmPrefix, 'bin'); + const npmRootGlobal = path.join(npmPrefix, 'node_modules'); + + // Files/symlinks to clean up in npm bin directory + const binFiles = [ + path.join(npmBinGlobal, 'plan2code-loop'), + path.join(npmBinGlobal, 'plan2code-loop.cmd'), + path.join(npmBinGlobal, 'plan2code-loop.ps1') + ]; + + // Symlink in node_modules - this is the main culprit for the lstat error + const nodeModulesLink = path.join(npmRootGlobal, 'plan2code-loop'); + + // Remove bin files first + for (const file of binFiles) { + try { + const stats = fs.lstatSync(file); + if (stats) { + fs.unlinkSync(file); + console.log(` ${COLORS.DIM}Removed: ${path.basename(file)}${COLORS.RESET}`); + } + } catch (err) { + if (err.code !== 'ENOENT') { + // File exists but can't be removed normally, try rmSync + try { + fs.rmSync(file, { force: true, recursive: true }); + console.log(` ${COLORS.DIM}Removed: ${path.basename(file)}${COLORS.RESET}`); + } catch (rmErr) { + console.log(` ${COLORS.DIM}Could not remove: ${path.basename(file)}${COLORS.RESET}`); + } + } + } + } + + // Remove node_modules symlink - this is critical for fixing the lstat error + try { + const stats = fs.lstatSync(nodeModulesLink); + if (stats) { + // On Windows, junction points need special handling + if (process.platform === 'win32') { + // Try using rmdir for junction points + try { + fs.rmdirSync(nodeModulesLink); + console.log(` ${COLORS.DIM}Removed: node_modules/plan2code-loop (junction)${COLORS.RESET}`); + } catch (rmdirErr) { + // Fall back to rmSync + fs.rmSync(nodeModulesLink, { force: true, recursive: true }); + console.log(` ${COLORS.DIM}Removed: node_modules/plan2code-loop${COLORS.RESET}`); + } + } else { + fs.rmSync(nodeModulesLink, { force: true, recursive: true }); + console.log(` ${COLORS.DIM}Removed: node_modules/plan2code-loop${COLORS.RESET}`); + } + } + } catch (err) { + if (err.code !== 'ENOENT') { + console.log(` ${COLORS.DIM}Could not remove node_modules symlink: ${err.message}${COLORS.RESET}`); + } + } + + return true; + } catch (error) { + // Non-fatal - npm link might still work + console.log(` ${COLORS.DIM}Cleanup skipped: ${error.message}${COLORS.RESET}`); + return true; + } +} + +/** + * Create global symlink for plan2code-loop + */ +function createGlobalSymlink() { + const loopDir = path.join(__dirname, 'plan2code-loop'); + const distBin = path.join(loopDir, 'dist', 'bin', 'plan2code-loop.js'); + // Check if built binary exists + if (!fs.existsSync(distBin)) { + console.log(`${COLORS.YELLOW}${SYMBOLS.WARNING}${COLORS.RESET} plan2code-loop binary not found. Run build first.`); + return false; + } + console.log(`${COLORS.CYAN}${SYMBOLS.ACTIVE} Creating global symlink...${COLORS.RESET}`); + + // Clean up existing symlinks first to prevent Windows lstat errors + cleanupExistingSymlinks(); + + // On Windows, skip npm link entirely and create batch file directly + // npm link has persistent issues with junctions and lstat errors on Windows + if (process.platform === 'win32') { + try { + const npmPrefix = execSync('npm config get prefix', { encoding: 'utf8' }).trim(); + const symlinkCmd = path.join(npmPrefix, 'plan2code-loop.cmd'); + const symlinkPs1 = path.join(npmPrefix, 'plan2code-loop.ps1'); + + // Create batch file wrapper for cmd.exe + const batchContent = `@echo off\r\nnode "${distBin}" %*\r\n`; + fs.writeFileSync(symlinkCmd, batchContent); + console.log(` ${COLORS.GREEN}${SYMBOLS.SUCCESS}${COLORS.RESET} Created: plan2code-loop.cmd`); + + // Create PowerShell wrapper for better shell support + const ps1Content = `#!/usr/bin/env pwsh\r\nnode "${distBin}" $args\r\n`; + fs.writeFileSync(symlinkPs1, ps1Content); + console.log(` ${COLORS.GREEN}${SYMBOLS.SUCCESS}${COLORS.RESET} Created: plan2code-loop.ps1`); + + console.log(` ${COLORS.DIM}Run 'plan2code-loop --help' from any directory${COLORS.RESET}`); + return true; + } catch (error) { + console.log(` ${COLORS.RED}${SYMBOLS.ERROR}${COLORS.RESET} Failed to create wrappers: ${error.message}`); + return false; + } + } + + // On Unix systems, use npm link as it works reliably + try { + execSync('npm link', { cwd: loopDir, stdio: 'pipe' }); + console.log(` ${COLORS.GREEN}${SYMBOLS.SUCCESS}${COLORS.RESET} Global symlink created`); + console.log(` ${COLORS.DIM}Run 'plan2code-loop --help' from any directory${COLORS.RESET}`); + return true; + } catch (error) { + console.log(` ${COLORS.RED}${SYMBOLS.ERROR}${COLORS.RESET} Symlink failed: ${error.message}`); + return false; + } +} +/** + * Remove global symlink for plan2code-loop + */ +function removeGlobalSymlink() { + console.log(`${COLORS.CYAN}${SYMBOLS.ACTIVE} Removing global symlink...${COLORS.RESET}`); + + let removedCount = 0; + + try { + // Get npm global prefix + const npmPrefix = execSync('npm config get prefix', { encoding: 'utf8' }).trim(); + + // On Windows, bin files are directly in prefix; on Unix they're in prefix/bin + const npmBinGlobal = process.platform === 'win32' ? npmPrefix : path.join(npmPrefix, 'bin'); + const npmRootGlobal = path.join(npmPrefix, 'node_modules'); + + // Files to remove + const filesToRemove = [ + { path: path.join(npmBinGlobal, 'plan2code-loop'), name: 'plan2code-loop' }, + { path: path.join(npmBinGlobal, 'plan2code-loop.cmd'), name: 'plan2code-loop.cmd' }, + { path: path.join(npmBinGlobal, 'plan2code-loop.ps1'), name: 'plan2code-loop.ps1' }, + { path: path.join(npmRootGlobal, 'plan2code-loop'), name: 'node_modules/plan2code-loop' } + ]; + + for (const file of filesToRemove) { + try { + fs.lstatSync(file.path); + // File exists, try to remove it + try { + // Try rmdirSync first for junctions/symlinks + fs.rmdirSync(file.path); + console.log(` ${COLORS.GREEN}${SYMBOLS.SUCCESS}${COLORS.RESET} Removed: ${file.name}`); + removedCount++; + } catch (rmdirErr) { + // Fall back to unlinkSync for regular files + try { + fs.unlinkSync(file.path); + console.log(` ${COLORS.GREEN}${SYMBOLS.SUCCESS}${COLORS.RESET} Removed: ${file.name}`); + removedCount++; + } catch (unlinkErr) { + // Last resort: rmSync + fs.rmSync(file.path, { force: true, recursive: true }); + console.log(` ${COLORS.GREEN}${SYMBOLS.SUCCESS}${COLORS.RESET} Removed: ${file.name}`); + removedCount++; + } + } + } catch (err) { + // File doesn't exist, skip it + } + } + + if (removedCount === 0) { + console.log(` ${COLORS.DIM}${SYMBOLS.INFO} No files found to remove${COLORS.RESET}`); + } + + return true; + } catch (error) { + console.log(` ${COLORS.RED}${SYMBOLS.ERROR}${COLORS.RESET} Uninstall failed: ${error.message}`); + return false; + } +} +/** + * Install plan2code-loop (build + symlink) + */ +function installPlan2CodeLoop() { + displaySectionHeader('PLAN2CODE-LOOP', '[ BUILD & INSTALL ]'); + const buildSuccess = buildPlan2CodeLoop(); + if (!buildSuccess) { + return 1; + } + const symlinkSuccess = createGlobalSymlink(); + if (!symlinkSuccess) { + return 1; + } + console.log(''); + console.log(`${COLORS.GREEN}${SYMBOLS.SUCCESS} plan2code-loop installed successfully!${COLORS.RESET}`); + console.log(''); + console.log(`${COLORS.CYAN}Usage:${COLORS.RESET}`); + console.log(` ${COLORS.BRIGHT}plan2code-loop${COLORS.RESET} Start the autonomous loop`); + console.log(` ${COLORS.BRIGHT}plan2code-loop -c${COLORS.RESET} Resume previous session`); + console.log(` ${COLORS.BRIGHT}plan2code-loop -v${COLORS.RESET} Verbose mode`); + console.log(` ${COLORS.BRIGHT}plan2code-loop --help${COLORS.RESET} Show all options`); + console.log(''); + return 0; +} +/** + * Uninstall plan2code-loop + */ +function uninstallPlan2CodeLoop() { + displaySectionHeader('PLAN2CODE-LOOP', '[ UNINSTALL ]'); + removeGlobalSymlink(); + console.log(''); + console.log(`${COLORS.GREEN}${SYMBOLS.SUCCESS} plan2code-loop uninstalled${COLORS.RESET}`); + console.log(''); + return 0; +} // ============================================================================ // MAIN // ============================================================================ +// Additional CLI arguments for plan2code-loop +const installLoop = args.includes('--loop'); +const uninstallLoop = args.includes('--uninstall-loop'); // Run the appropriate function -if (!hasArgs) { +if (installLoop) { + process.exit(installPlan2CodeLoop()); +} else if (uninstallLoop) { + process.exit(uninstallPlan2CodeLoop()); +} else if (!hasArgs) { // No arguments - run interactive menu runInteractive(); } else if (localInstall) { diff --git a/plan2code-loop/README.md b/plan2code-loop/README.md new file mode 100644 index 0000000..00bb107 --- /dev/null +++ b/plan2code-loop/README.md @@ -0,0 +1,195 @@ +# 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, 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 ONE task per iteration +4. **Checkbox Update** - The AI marks the task complete in the markdown file +5. **Scratchpad Update** - The AI appends notes to the per-spec scratchpad +6. **Completion Marker** - The AI outputs a structured marker (e.g., `TASK_COMPLETE: 1.1 - description`) +7. **Loop** - Repeat until all tasks done or max iterations reached + +### 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 +LOOP_COMPLETE +``` + +| Marker | Meaning | +|--------|---------| +| `TASK_COMPLETE: X.X - desc` | Task implemented and marked complete | +| `TASK_BLOCKED: X.X - reason` | Cannot complete task (explains why) | +| `LOOP_COMPLETE` | All phases finished | + +## 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 +? Maximum iterations: 100 + +Starting Plan2Code Loop +══════════════════════════════════════════════ +Agent: Claude Code +Model: default +Spec: C:\projects\my-app\specs\todo-app +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 +``` diff --git a/plan2code-loop/package.json b/plan2code-loop/package.json new file mode 100644 index 0000000..05b88a8 --- /dev/null +++ b/plan2code-loop/package.json @@ -0,0 +1,45 @@ +{ + "name": "plan2code-loop", + "version": "1.0.0", + "description": "Plan2Code Loop - Autonomous spec-driven implementation CLI", + "type": "module", + "main": "dist/index.js", + "bin": { + "plan2code-loop": "./dist/bin/plan2code-loop.js" + }, + "files": [ + "dist" + ], + "engines": { + "node": ">=18.0.0" + }, + "keywords": [ + "cli", + "ai", + "agent", + "automation", + "claude", + "copilot", + "spec-driven" + ], + "license": "MIT", + "scripts": { + "build": "tsup", + "dev": "tsup --watch", + "start": "node dist/bin/plan2code-loop.js", + "prepublishOnly": "npm run build" + }, + "dependencies": { + "@inquirer/prompts": "^8.1.0", + "chalk": "^5.6.2", + "execa": "^9.6.1", + "fs-extra": "^11.3.3", + "ora": "^9.0.0" + }, + "devDependencies": { + "@types/fs-extra": "^11.0.4", + "@types/node": "^25.0.3", + "tsup": "^8.5.1", + "typescript": "^5.9.3" + } +} diff --git a/plan2code-loop/src/agents/claude-code.ts b/plan2code-loop/src/agents/claude-code.ts new file mode 100644 index 0000000..9f77cb0 --- /dev/null +++ b/plan2code-loop/src/agents/claude-code.ts @@ -0,0 +1,82 @@ +import type { Agent, AgentConfig, AgentExecutionOptions, AgentExecutionResult } from './types.js'; +import { executeCommand } from '../utils/process.js'; +import { writeFileSync, unlinkSync } from 'fs'; +import { join } from 'path'; +import { tmpdir } from 'os'; + +const claudeCodeConfig: AgentConfig = { + name: 'claude-code', + displayName: 'Claude Code', + command: 'claude', + models: [ + { value: 'default', label: 'Default (use Claude config)' }, + ], + defaultModel: 'default', + flags: { + prompt: '--print', + model: '--model', + skipPermissions: '--dangerously-skip-permissions', + }, +}; + +class ClaudeCodeAgent implements Agent { + readonly config = claudeCodeConfig; + + async execute(options: AgentExecutionOptions): Promise { + // Write prompt to temp file - more reliable than stdin on Windows + const tempFile = join(tmpdir(), `plan2code-prompt-${Date.now()}.txt`); + writeFileSync(tempFile, options.prompt, 'utf-8'); + + try { + // Build args: flags first, then read prompt from temp file via shell + const args: string[] = [ + this.config.flags.prompt, // --print for non-interactive mode + this.config.flags.skipPermissions, + ]; + + // Only add --model if not using default + if (options.model && options.model !== 'default') { + args.push(this.config.flags.model, options.model); + } + + // Use stdin from the temp file + const result = await executeCommand({ + command: this.config.command, + args, + cwd: options.cwd, + timeout: options.timeout, + signal: options.signal, + stdinFile: tempFile, + }); + + return { + stdout: result.stdout, + stderr: result.stderr, + exitCode: result.exitCode, + timedOut: result.timedOut, + cancelled: result.cancelled, + duration: result.duration, + }; + } finally { + // Clean up temp file + try { + unlinkSync(tempFile); + } catch { + // Ignore cleanup errors + } + } + } + + async isAvailable(): Promise { + // Run claude --version to verify it's actually installed and working + const result = await executeCommand({ + command: this.config.command, + args: ['--version'], + cwd: process.cwd(), + timeout: 5000, + }); + return result.exitCode === 0; + } +} + +export const claudeCodeAgent = new ClaudeCodeAgent(); diff --git a/plan2code-loop/src/agents/copilot-cli.ts b/plan2code-loop/src/agents/copilot-cli.ts new file mode 100644 index 0000000..ffbb361 --- /dev/null +++ b/plan2code-loop/src/agents/copilot-cli.ts @@ -0,0 +1,70 @@ +import type { Agent, AgentConfig, AgentExecutionOptions, AgentExecutionResult } from './types.js'; +import { executeCommand } from '../utils/process.js'; + +const copilotCliConfig: AgentConfig = { + name: 'copilot-cli', + displayName: 'GitHub Copilot CLI', + command: 'copilot', + models: [ + { value: 'claude-sonnet-4', label: 'Claude Sonnet 4 (Default)' }, + { value: 'claude-sonnet-4.5', label: 'Claude Sonnet 4.5' }, + { value: 'claude-opus-4.5', label: 'Claude Opus 4.5' }, + { value: 'gpt-5', label: 'GPT-5' }, + { value: 'gpt-5-mini', label: 'GPT-5 Mini' }, + { value: 'gemini-3-pro-preview', label: 'Gemini 3 Pro' }, + ], + defaultModel: 'claude-sonnet-4', + flags: { + prompt: '-p', + model: '--model', + skipPermissions: '--allow-all-tools', + silent: '-s', + }, +}; + +class CopilotCliAgent implements Agent { + readonly config = copilotCliConfig; + + async execute(options: AgentExecutionOptions): Promise { + // Use stdin for prompt to handle multi-line text properly + const args: string[] = []; + + // Only add --model if not using default + if (options.model && options.model !== 'default') { + args.push(this.config.flags.model, options.model); + } + + args.push(this.config.flags.skipPermissions, this.config.flags.silent!); + + const result = await executeCommand({ + command: this.config.command, + args, + cwd: options.cwd, + timeout: options.timeout, + signal: options.signal, + stdin: options.prompt, + }); + + return { + stdout: result.stdout, + stderr: result.stderr, + exitCode: result.exitCode, + timedOut: result.timedOut, + cancelled: result.cancelled, + duration: result.duration, + }; + } + + async isAvailable(): Promise { + // Run copilot --version to verify it's installed + const result = await executeCommand({ + command: this.config.command, + args: ['--version'], + cwd: process.cwd(), + timeout: 5000, + }); + return result.exitCode === 0; + } +} + +export const copilotCliAgent = new CopilotCliAgent(); diff --git a/plan2code-loop/src/agents/index.ts b/plan2code-loop/src/agents/index.ts new file mode 100644 index 0000000..0cdea32 --- /dev/null +++ b/plan2code-loop/src/agents/index.ts @@ -0,0 +1,19 @@ +export type { + Agent, + AgentConfig, + AgentExecutionOptions, + AgentExecutionResult, + ModelOption, +} from './types.js'; + +export { agentRegistry } from './registry.js'; +export { claudeCodeAgent } from './claude-code.js'; +export { copilotCliAgent } from './copilot-cli.js'; + +// Register all agents +import { agentRegistry } from './registry.js'; +import { claudeCodeAgent } from './claude-code.js'; +import { copilotCliAgent } from './copilot-cli.js'; + +agentRegistry.register(claudeCodeAgent); +agentRegistry.register(copilotCliAgent); diff --git a/plan2code-loop/src/agents/registry.ts b/plan2code-loop/src/agents/registry.ts new file mode 100644 index 0000000..c90a3d6 --- /dev/null +++ b/plan2code-loop/src/agents/registry.ts @@ -0,0 +1,34 @@ +import type { Agent } from './types.js'; + +class AgentRegistry { + private agents: Map = new Map(); + + register(agent: Agent): void { + this.agents.set(agent.config.name, agent); + } + + get(name: string): Agent | undefined { + return this.agents.get(name); + } + + getAll(): Agent[] { + return Array.from(this.agents.values()); + } + + getAvailable(): Promise { + return Promise.all( + this.getAll().map(async (agent) => ({ + agent, + available: await agent.isAvailable(), + })) + ).then((results) => + results.filter((r) => r.available).map((r) => r.agent) + ); + } + + getNames(): string[] { + return Array.from(this.agents.keys()); + } +} + +export const agentRegistry = new AgentRegistry(); diff --git a/plan2code-loop/src/agents/types.ts b/plan2code-loop/src/agents/types.ts new file mode 100644 index 0000000..94937b2 --- /dev/null +++ b/plan2code-loop/src/agents/types.ts @@ -0,0 +1,42 @@ +export interface ModelOption { + value: string; + label: string; +} + +export interface AgentConfig { + name: string; + displayName: string; + command: string; + models: ModelOption[]; + defaultModel: string; + flags: { + prompt: string; + model: string; + skipPermissions: string; + silent?: string; + }; +} + +export interface AgentExecutionOptions { + prompt: string; + model: string; + timeout: number; // milliseconds + verbose: boolean; + cwd: string; + signal?: AbortSignal; // For cancellation +} + +export interface AgentExecutionResult { + stdout: string; + stderr: string; + exitCode: number; + timedOut: boolean; + cancelled: boolean; + duration: number; // milliseconds +} + +export interface Agent { + config: AgentConfig; + execute(options: AgentExecutionOptions): Promise; + isAvailable(): Promise; +} diff --git a/plan2code-loop/src/bin/plan2code-loop.ts b/plan2code-loop/src/bin/plan2code-loop.ts new file mode 100644 index 0000000..fe1d5d3 --- /dev/null +++ b/plan2code-loop/src/bin/plan2code-loop.ts @@ -0,0 +1,34 @@ +import { run } from '../index.js'; +import { logger } from '../utils/index.js'; + +async function main() { + try { + const result = await run(); + + if (!result) { + process.exit(0); + } + + // Exit codes per spec + switch (result.exitReason) { + case 'all_complete': + logger.success('Loop completed successfully - all tasks done!'); + process.exit(0); + case 'max_iterations': + logger.warning('Loop ended: max iterations reached'); + process.exit(1); + case 'interrupted': + logger.info('Loop interrupted by user'); + process.exit(2); + case 'error': + logger.error('Loop ended with error'); + process.exit(3); + } + + } catch (error) { + logger.error(error instanceof Error ? error.message : String(error)); + process.exit(3); + } +} + +main(); diff --git a/plan2code-loop/src/cli.ts b/plan2code-loop/src/cli.ts new file mode 100644 index 0000000..36c1cec --- /dev/null +++ b/plan2code-loop/src/cli.ts @@ -0,0 +1,228 @@ +import path from 'path'; +import { confirm, input, select } from '@inquirer/prompts'; +import { agentRegistry } from './agents/index.js'; +import { StateManager, type SessionConfig } from './state/index.js'; +import { 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 { + // Auto-detect spec directories + const specDirs = await detectSpecDirectories(cwd); + + if (specDirs.length === 0) { + logger.error('No spec directories found!'); + logger.info('Expected: specs//overview.md'); + logger.info(''); + logger.info('To get started:'); + logger.info('1. Create a spec directory: mkdir -p specs/my-feature'); + logger.info('2. Create overview.md with phases listed as checkboxes'); + logger.info('3. Create phase-1.md, phase-2.md, etc. with task checkboxes'); + 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 { + const availableAgents = await agentRegistry.getAvailable(); + + if (availableAgents.length === 0) { + logger.error('No AI agents detected on your system!'); + console.log(); + logger.info('Please install at least one of the following:'); + console.log(); + logger.bold('Claude Code:'); + logger.dim(' npm install -g @anthropic-ai/claude-code'); + console.log(); + logger.bold('GitHub Copilot CLI:'); + logger.dim(' gh extension install github/gh-copilot'); + console.log(); + throw new Error('No agents available. Please install an AI agent and try again.'); + } + + if (availableAgents.length === 1) { + const agent = availableAgents[0]; + logger.info(`Using ${agent.config.displayName} (only available agent)`); + return agent.config.name; + } + + const agentName = await select({ + message: 'Select AI agent:', + choices: availableAgents.map((agent) => ({ + name: agent.config.displayName, + value: agent.config.name, + })), + }); + + return agentName; +} + +/** + * Prompt for JIRA ticket ID + */ +async function promptJiraTicketId(): Promise { + const ticketId = await input({ + message: 'JIRA Ticket ID (optional, for commit messages):', + }); + + return ticketId.trim() || undefined; +} + +/** + * Select maximum iterations + */ +async function selectMaxIterations(): Promise { + 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; +} + +/** + * 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 { + // Show 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) { + 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 maxIterations = await selectMaxIterations(); + + const config: SessionConfig = { + agent, + model: 'default', + maxIterations, + specPath, + timeout: 30, + verbose: false, + startedAt: new Date().toISOString(), + currentIteration: 0, + jiraTicketId, + }; + + // Initialize session + await stateManager.initializeNewSession(config); + + return { config, isResume: false }; +} diff --git a/plan2code-loop/src/controller.ts b/plan2code-loop/src/controller.ts new file mode 100644 index 0000000..619d916 --- /dev/null +++ b/plan2code-loop/src/controller.ts @@ -0,0 +1,314 @@ +import { agentRegistry, type Agent, type AgentExecutionResult } from './agents/index.js'; +import { StateManager, type SessionConfig, type IterationLogEntry } from './state/index.js'; +import { buildLoopPrompt } from './prompt/index.js'; +import { checkForCompletion, logger, type CompletionCheckResult } from './utils/index.js'; + +export interface TaskCompleteInfo { + marker: string; + taskId?: string; + taskName?: string; +} + +export interface ControllerOptions { + config: SessionConfig; + stateManager: StateManager; + onIteration?: (iteration: number, max: number) => void; + onTaskComplete?: (info: TaskCompleteInfo) => void | Promise; + onLoopComplete?: () => void; +} + +export interface LoopResult { + completed: boolean; + iterations: number; + finalMarker?: string; + exitReason: 'all_complete' | 'max_iterations' | 'interrupted' | 'error'; + tasksCompleted: number; + error?: Error; +} + +export class Controller { + private readonly config: SessionConfig; + private readonly stateManager: StateManager; + private readonly agent: Agent; + private readonly onIteration?: (iteration: number, max: number) => void; + private readonly onTaskComplete?: (info: TaskCompleteInfo) => void | Promise; + private readonly onLoopComplete?: () => void; + private interrupted = false; + private abortController: AbortController | null = null; + private tasksCompleted = 0; + + constructor(options: ControllerOptions) { + this.config = options.config; + this.stateManager = options.stateManager; + this.onIteration = options.onIteration; + this.onTaskComplete = options.onTaskComplete; + this.onLoopComplete = options.onLoopComplete; + + const agent = agentRegistry.get(this.config.agent); + if (!agent) { + throw new Error(`Agent not found: ${this.config.agent}`); + } + this.agent = agent; + } + + private async buildPrompt(): Promise { + return buildLoopPrompt({ + specPath: this.config.specPath, + iteration: this.config.currentIteration + 1, + maxIterations: this.config.maxIterations, + stateManager: this.stateManager, + }); + } + + private async executeIteration(prompt: string): Promise { + const timeoutMs = this.config.timeout * 60 * 1000; + + // Create new AbortController for this iteration + this.abortController = new AbortController(); + + const result = await this.agent.execute({ + prompt, + model: this.config.model, + timeout: timeoutMs, + verbose: this.config.verbose, + cwd: process.cwd(), + signal: this.abortController.signal, + }); + + this.abortController = null; + return result; + } + + private createLogEntry( + result: AgentExecutionResult, + status: IterationLogEntry['status'], + marker?: string + ): IterationLogEntry { + return { + iteration: this.config.currentIteration + 1, + timestamp: new Date().toISOString(), + duration: result.duration, + exitCode: result.exitCode, + status, + completionMarker: marker, + }; + } + + private formatTaskDisplay(completion: CompletionCheckResult): string { + if (completion.taskId && completion.taskName) { + return `Task ${completion.taskId}: ${completion.taskName}`; + } else if (completion.taskId) { + return `Task ${completion.taskId}`; + } + return ''; + } + + private displayIterationResult(result: AgentExecutionResult, iterNum: number, completion: CompletionCheckResult): void { + const duration = Math.round(result.duration / 1000); + const taskDisplay = this.formatTaskDisplay(completion); + + // Show iteration completion with task info if available + if (taskDisplay) { + logger.iteration(iterNum, this.config.maxIterations, `${taskDisplay} (${duration}s)`); + } else { + logger.iteration(iterNum, this.config.maxIterations, `completed in ${duration}s`); + } + + // Verbose mode: show full output + if (this.config.verbose) { + console.log(); + logger.dim('--- Agent Output ---'); + console.log(result.stdout); + if (result.stderr) { + logger.dim('--- Agent Stderr ---'); + console.log(result.stderr); + } + logger.dim('--- End Output ---'); + console.log(); + } else if (result.exitCode !== 0 && result.stderr.trim()) { + logger.error(` ${result.stderr.trim().split('\n')[0]}`); + } + } + + async run(): Promise { + logger.header('Starting Plan2Code Loop'); + logger.info(`Agent: ${this.agent.config.displayName}`); + logger.info(`Model: ${this.config.model}`); + logger.info(`Spec: ${this.config.specPath}`); + logger.info(`Max iterations: ${this.config.maxIterations}`); + console.log(); + + while (this.config.currentIteration < this.config.maxIterations) { + if (this.interrupted) { + return { + completed: false, + iterations: this.config.currentIteration, + exitReason: 'interrupted', + tasksCompleted: this.tasksCompleted, + }; + } + + const iterNum = this.config.currentIteration + 1; + this.onIteration?.(iterNum, this.config.maxIterations); + + console.log(); + logger.info(`Iteration ${iterNum}/${this.config.maxIterations}`); + + // Build prompt - simple, just spec path and iteration info + const prompt = await this.buildPrompt(); + + const spinner = logger.spinner('Waiting for agent response'); + const startTime = Date.now(); + + // Update spinner with elapsed time every second + const elapsedInterval = setInterval(() => { + const elapsed = Math.round((Date.now() - startTime) / 1000); + spinner.text = `Waiting for agent response (${elapsed}s)`; + }, 1000); + + try { + const result = await this.executeIteration(prompt); + clearInterval(elapsedInterval); + spinner.stop(); + + // Check if cancelled + if (result.cancelled || this.interrupted) { + logger.info('Agent process cancelled'); + + const entry: IterationLogEntry = { + iteration: iterNum, + timestamp: new Date().toISOString(), + duration: result.duration, + exitCode: -1, + status: 'interrupted', + }; + await this.stateManager.appendIterationLog(entry); + + return { + completed: false, + iterations: this.config.currentIteration, + exitReason: 'interrupted', + tasksCompleted: this.tasksCompleted, + }; + } + + // Check for completion markers in output (now includes task info) + const completion = checkForCompletion(result.stdout + result.stderr); + + // Determine status + let status: IterationLogEntry['status'] = 'running'; + if (completion.completed) { + if (completion.marker === 'TASK_BLOCKED') { + status = 'blocked'; + } else { + status = 'completed'; + } + } else if (result.timedOut) { + status = 'timeout'; + } else if (result.exitCode !== 0) { + status = 'error'; + } + + // Log iteration + const logEntry = this.createLogEntry(result, status, completion.marker); + await this.stateManager.appendIterationLog(logEntry); + + // Display iteration result with task info from completion marker + this.displayIterationResult(result, iterNum, completion); + + // Note: Scratchpad updates are now handled by the LLM directly + + // Handle completion markers + if (completion.completed) { + const taskDisplay = this.formatTaskDisplay(completion); + + if (completion.marker === 'TASK_COMPLETE') { + this.tasksCompleted++; + await this.onTaskComplete?.({ + marker: completion.marker, + taskId: completion.taskId, + taskName: completion.taskName, + }); + logger.success(taskDisplay ? `Completed: ${taskDisplay}` : 'Task completed!'); + } else if (completion.marker === 'TASK_BLOCKED') { + const blockInfo = completion.taskId + ? `Task ${completion.taskId} blocked: ${completion.reason || 'Unknown reason'}` + : `Task blocked: ${completion.reason || 'Unknown reason'}`; + logger.warning(blockInfo); + } else if (completion.marker === 'LOOP_COMPLETE') { + // Commit any final changes before completing + this.tasksCompleted++; + await this.onTaskComplete?.({ + marker: completion.marker, + taskId: completion.taskId, + taskName: completion.taskName || 'Final implementation complete', + }); + this.onLoopComplete?.(); + logger.success('All tasks complete!'); + return { + completed: true, + iterations: iterNum, + finalMarker: 'LOOP_COMPLETE', + exitReason: 'all_complete', + tasksCompleted: this.tasksCompleted, + }; + } + } + + // Increment iteration and update spec hash (so resume doesn't see false changes) + await this.stateManager.incrementIteration(); + await this.stateManager.updateSpecHash(this.config.specPath); + this.config.currentIteration++; + + // Handle timeout + if (result.timedOut) { + logger.warning(`Iteration ${iterNum} timed out, continuing...`); + } + + // Handle error (but continue - LLM might recover) + if (result.exitCode !== 0 && !result.timedOut && !completion.completed) { + logger.warning(`Iteration ${iterNum} exited with code ${result.exitCode}, continuing...`); + } + + } catch (error) { + clearInterval(elapsedInterval); + spinner.stop(); + logger.error(`Iteration ${iterNum} failed: ${error}`); + + // Log the error + const entry: IterationLogEntry = { + iteration: iterNum, + timestamp: new Date().toISOString(), + duration: 0, + exitCode: -1, + status: 'error', + }; + await this.stateManager.appendIterationLog(entry); + + return { + completed: false, + iterations: this.config.currentIteration, + exitReason: 'error', + tasksCompleted: this.tasksCompleted, + error: error instanceof Error ? error : new Error(String(error)), + }; + } + } + + // Max iterations reached + logger.warning('Max iterations reached'); + return { + completed: false, + iterations: this.config.currentIteration, + exitReason: 'max_iterations', + tasksCompleted: this.tasksCompleted, + }; + } + + interrupt(): void { + this.interrupted = true; + if (this.abortController) { + this.abortController.abort(); + } + } +} diff --git a/plan2code-loop/src/index.ts b/plan2code-loop/src/index.ts new file mode 100644 index 0000000..7f38515 --- /dev/null +++ b/plan2code-loop/src/index.ts @@ -0,0 +1,93 @@ +import path from 'path'; +import { StateManager } from './state/index.js'; +import { Controller, type LoopResult, type TaskCompleteInfo } from './controller.js'; +import { setupSession } from './cli.js'; +import { logger, createTaskCommit } from './utils/index.js'; + +export async function run(): Promise { + // Ensure agents are registered + await import('./agents/index.js'); + + const stateManager = new StateManager(); + + const result = await setupSession(stateManager); + if (!result) { + return null; + } + + const { config, isResume } = result; + + if (isResume) { + logger.info(`Resuming from iteration ${config.currentIteration}`); + } + + const controller = new Controller({ + config, + stateManager, + onIteration: (iter, max) => { + // Could add git checkpoint logic here if needed + }, + onTaskComplete: async (info: TaskCompleteInfo) => { + // Create git commit for completed task + const taskName = info.taskName || info.taskId || 'Task completed'; + await createTaskCommit({ + taskName, + jiraTicketId: config.jiraTicketId, + cwd: process.cwd(), + }); + }, + onLoopComplete: () => { + // All tasks completed callback + }, + }); + + // Setup interrupt handler + const handleInterrupt = () => { + logger.warning('\nInterrupt received, saving state...'); + controller.interrupt(); + }; + + process.on('SIGINT', handleInterrupt); + process.on('SIGTERM', handleInterrupt); + + try { + const loopResult = await controller.run(); + + // Display summary + console.log(); + logger.header('Session Summary'); + logger.info(`Total iterations: ${loopResult.iterations}`); + logger.info(`Tasks completed: ${loopResult.tasksCompleted}`); + logger.info(`Exit reason: ${loopResult.exitReason}`); + if (loopResult.finalMarker) { + logger.info(`Completion marker: ${loopResult.finalMarker}`); + } + if (loopResult.error) { + logger.error(`Error: ${loopResult.error.message}`); + } + + // Show completion celebration and finalize reminder when all phases complete + if (loopResult.exitReason === 'all_complete') { + logger.allPhasesComplete(); + } + + // Show state file locations (now per-spec) + console.log(); + logger.dim(`Session files saved to ${path.relative(process.cwd(), stateManager.getStateDir())}:`); + logger.dim(' - config.json (session configuration)'); + logger.dim(' - scratchpad.md (LLM-managed notes)'); + logger.dim(' - iteration.log (history)'); + + return loopResult; + } finally { + process.off('SIGINT', handleInterrupt); + process.off('SIGTERM', handleInterrupt); + } +} + +// Re-export types and classes +export { Controller, type ControllerOptions, type LoopResult, type TaskCompleteInfo } from './controller.js'; +export { StateManager } from './state/index.js'; +export { setupSession } from './cli.js'; +export { agentRegistry, type Agent, type AgentConfig } from './agents/index.js'; +export { detectSpecDirectories, getSpecProgress } from './spec/index.js'; diff --git a/plan2code-loop/src/prompt/builder.ts b/plan2code-loop/src/prompt/builder.ts new file mode 100644 index 0000000..bb80846 --- /dev/null +++ b/plan2code-loop/src/prompt/builder.ts @@ -0,0 +1,33 @@ +import type { StateManager } from '../state/index.js'; +import { LOOP_PROMPT_TEMPLATE } from './templates.js'; + +export interface PromptContext { + specPath: string; + iteration: number; + maxIterations: number; + stateManager: StateManager; +} + +/** + * Build the prompt for the AI agent + * Simple: just pass spec path and let the LLM discover the next task + */ +export async function buildLoopPrompt(context: PromptContext): Promise { + const { specPath, iteration, maxIterations, stateManager } = context; + + // Read scratchpad content for session continuity (LLM writes to this) + const scratchpadContent = await stateManager.readScratchpad(); + + // Project root is where plan2code-loop was invoked from + const projectRoot = process.cwd(); + + // Simple template substitution + const prompt = LOOP_PROMPT_TEMPLATE + .replace(/{{projectRoot}}/g, projectRoot) + .replace(/{{specPath}}/g, specPath) + .replace(/{{iteration}}/g, iteration.toString()) + .replace(/{{maxIterations}}/g, maxIterations.toString()) + .replace(/{{scratchpadContent}}/g, scratchpadContent || '(First iteration - no previous progress)'); + + return prompt; +} diff --git a/plan2code-loop/src/prompt/index.ts b/plan2code-loop/src/prompt/index.ts new file mode 100644 index 0000000..fd08d3c --- /dev/null +++ b/plan2code-loop/src/prompt/index.ts @@ -0,0 +1,6 @@ +export { + buildLoopPrompt, + type PromptContext, +} from './builder.js'; + +export { LOOP_PROMPT_TEMPLATE } from './templates.js'; diff --git a/plan2code-loop/src/prompt/templates.ts b/plan2code-loop/src/prompt/templates.ts new file mode 100644 index 0000000..7f33fa4 --- /dev/null +++ b/plan2code-loop/src/prompt/templates.ts @@ -0,0 +1,85 @@ +export const LOOP_PROMPT_TEMPLATE = `# PLAN-LOOP: Autonomous Task Implementation + +## CRITICAL CONSTRAINT +**IMPLEMENT EXACTLY ONE TASK PER ITERATION.** +Do NOT implement multiple tasks. Do NOT complete an entire phase. +Find the FIRST unchecked task, implement ONLY that task, then STOP and report. + +## Project Information +- **Project Root:** \`{{projectRoot}}\` +- **Spec Location:** \`{{specPath}}\` +- Read \`AGENTS.md\` for project-specific guidance if available + +## IMPORTANT: File Locations +- Write ALL code files relative to the **project root** (\`{{projectRoot}}\`) +- The spec directory (\`{{specPath}}\`) is for documentation ONLY - never write code there +- Example: Create \`{{projectRoot}}/src/index.ts\`, NOT \`{{specPath}}/src/index.ts\` + +## Iteration +{{iteration}} of {{maxIterations}} + +## Task Discovery Process +1. Read \`{{specPath}}/overview.md\` to see all phases +2. Find the FIRST phase with an unchecked checkbox (\`- [ ]\` or \`- [/]\`) +3. Read that phase's file (e.g., \`phase-1.md\`) +4. Check the \`## Prerequisites\` section FIRST +5. Find the FIRST unchecked prerequisite (\`- [ ]\`) + - If found, that is your task for this iteration + - Verify/complete it, then mark \`[x]\` or \`[?]\` +6. Only if ALL prerequisites are complete (\`[x]\` or \`[?]\`), find the FIRST unchecked task +7. That is your ONE task - implement ONLY that task + +## Checkbox States +- \`[ ]\` = incomplete/pending (do the FIRST one you find) +- \`[x]\` = complete (skip) +- \`[?]\` = assumed complete, couldn't verify (skip) +- \`[!]\` = blocked (skip) + +## Implementation Steps +1. Read and understand the single task +2. Implement it completely +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 +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 + +## 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. + +## Completion Markers (REQUIRED FORMAT) +Output exactly ONE of these at the end, including the task ID and description: + +**PREREQ_COMPLETE: [prereq_id] - [description]** +Example: \`PREREQ_COMPLETE: P1.1 - Verified Phase 1 complete\` + +**PREREQ_ASSUMED: [prereq_id] - [description]** +Example: \`PREREQ_ASSUMED: P2.1 - Design approval (cannot verify)\` + +**TASK_COMPLETE: [task_id] - [task_description]** +Example: \`TASK_COMPLETE: 1.1 - Initialize project structure\` + +**TASK_BLOCKED: [task_id] - [reason]** +Example: \`TASK_BLOCKED: 2.3 - Missing API credentials\` + +**LOOP_COMPLETE** +Use only when ALL phases in overview.md are marked complete. + +## Scratchpad Management + +After completing each task, append to \`{{specPath}}/.plan2code-loop/scratchpad.md\`: +- Task completed and Phase item reference +- Key decisions made and reasoning +- Files changed +- Any blockers or notes for next iteration + +Keep entries concise. Sacrifice grammar for concision. This file helps future iterations skip exploration. + +If key patterns or learnings were discovered, update \`./AGENTS.md\` if it exists. + +## Previous Session Context +{{scratchpadContent}} + +--- + +Remember: ONE TASK ONLY. Find it, implement it, mark it done, output TASK_COMPLETE with the task ID and description, then stop. +`; diff --git a/plan2code-loop/src/spec/index.ts b/plan2code-loop/src/spec/index.ts new file mode 100644 index 0000000..ba42c81 --- /dev/null +++ b/plan2code-loop/src/spec/index.ts @@ -0,0 +1,4 @@ +export { + detectSpecDirectories, + getSpecProgress, +} from './utils.js'; diff --git a/plan2code-loop/src/spec/utils.ts b/plan2code-loop/src/spec/utils.ts new file mode 100644 index 0000000..e9ea1ae --- /dev/null +++ b/plan2code-loop/src/spec/utils.ts @@ -0,0 +1,70 @@ +import path from 'path'; +import fs from 'fs-extra'; + +/** + * Auto-detect spec directories in the project + * Looks for directories containing overview.md + */ +export async function detectSpecDirectories(cwd: string = process.cwd()): Promise { + const specsDir = path.join(cwd, 'specs'); + const specsDirs: string[] = []; + + if (await fs.pathExists(specsDir)) { + // Look for overview.md files in subdirectories + const entries = await fs.readdir(specsDir, { withFileTypes: true }); + + for (const entry of entries) { + if (entry.isDirectory()) { + const overviewPath = path.join(specsDir, entry.name, 'overview.md'); + if (await fs.pathExists(overviewPath)) { + specsDirs.push(path.join(specsDir, entry.name)); + } + } + } + + // Also check if specs/ itself contains overview.md + const rootOverview = path.join(specsDir, 'overview.md'); + if (await fs.pathExists(rootOverview)) { + specsDirs.push(specsDir); + } + } + + return specsDirs; +} + +/** + * Simple progress stats by counting phase-*.md files + * Used for CLI display only - LLM handles actual task discovery + */ +export async function getSpecProgress(specPath: string): Promise<{ + featureName: string; + totalPhases: number; +}> { + const overviewPath = path.join(specPath, 'overview.md'); + + // Extract feature name from overview.md + let featureName = path.basename(specPath); + try { + const overviewContent = await fs.readFile(overviewPath, 'utf8'); + const h1Match = overviewContent.match(/^#\s+(.+)$/m); + if (h1Match) { + featureName = h1Match[1].trim(); + } + } catch { + // Use directory name as fallback + } + + // Count phase-*.md files + let totalPhases = 0; + try { + const entries = await fs.readdir(specPath); + totalPhases = entries.filter(name => /^phase-\d+\.md$/i.test(name)).length; + } catch { + // Directory read failed + } + + return { + featureName, + totalPhases, + }; +} diff --git a/plan2code-loop/src/state/config.ts b/plan2code-loop/src/state/config.ts new file mode 100644 index 0000000..5e89e0b --- /dev/null +++ b/plan2code-loop/src/state/config.ts @@ -0,0 +1,29 @@ +export interface SessionConfig { + agent: string; // "claude-code" | "copilot-cli" + model: string; // Selected model + maxIterations: number; // 5-50 + timeout: number; // Minutes per iteration + verbose: boolean; + specPath: string; // Path to the spec directory + startedAt: string; // ISO timestamp + currentIteration: number; + jiraTicketId?: string; // JIRA ticket ID for commit messages +} + +export interface IterationLogEntry { + iteration: number; + timestamp: string; // ISO timestamp + duration: number; // milliseconds + exitCode: number; + status: 'running' | 'completed' | 'error' | 'timeout' | 'interrupted' | 'blocked'; + completionMarker?: string; +} + +export type SessionState = 'new' | 'continue' | 'changed'; + +export const DEFAULT_CONFIG: Partial = { + maxIterations: 100, + timeout: 30, + verbose: false, + currentIteration: 0, +}; diff --git a/plan2code-loop/src/state/hash.ts b/plan2code-loop/src/state/hash.ts new file mode 100644 index 0000000..cb1ea0f --- /dev/null +++ b/plan2code-loop/src/state/hash.ts @@ -0,0 +1,9 @@ +import { createHash } from 'crypto'; + +export function computeHash(content: string): string { + return createHash('sha256').update(content).digest('hex').slice(0, 16); +} + +export function hashesMatch(a: string, b: string): boolean { + return a === b; +} diff --git a/plan2code-loop/src/state/index.ts b/plan2code-loop/src/state/index.ts new file mode 100644 index 0000000..b82eb7e --- /dev/null +++ b/plan2code-loop/src/state/index.ts @@ -0,0 +1,9 @@ +export { + type SessionConfig, + type IterationLogEntry, + type SessionState, + DEFAULT_CONFIG, +} from './config.js'; + +export { StateManager } from './manager.js'; +export { computeHash, hashesMatch } from './hash.js'; diff --git a/plan2code-loop/src/state/manager.ts b/plan2code-loop/src/state/manager.ts new file mode 100644 index 0000000..c00b6f0 --- /dev/null +++ b/plan2code-loop/src/state/manager.ts @@ -0,0 +1,231 @@ +import path from 'path'; +import fs from 'fs-extra'; +import type { SessionConfig, IterationLogEntry, SessionState } from './config.js'; +import { DEFAULT_CONFIG } from './config.js'; +import { computeHash, hashesMatch } from './hash.js'; + +const SCRATCHPAD_TEMPLATE = `# Scratchpad + +Session notes appended by LLM during implementation. + +--- + +`; + +export class StateManager { + private readonly stateDir: string; + private readonly configPath: string; + private readonly scratchpadPath: string; + private readonly logPath: string; + private readonly hashPath: string; + private specPath: string | null = null; + + constructor(cwd: string = process.cwd()) { + // Default to project root - will be updated when spec is selected + this.stateDir = path.join(cwd, '.plan2code-loop'); + this.configPath = path.join(this.stateDir, 'config.json'); + this.scratchpadPath = path.join(this.stateDir, 'scratchpad.md'); + this.logPath = path.join(this.stateDir, 'iteration.log'); + this.hashPath = path.join(this.stateDir, 'spec.hash'); + } + + /** + * Set the spec path and update all state paths to be inside the spec directory + */ + setSpecPath(specPath: string): void { + this.specPath = specPath; + const stateDir = path.join(specPath, '.plan2code-loop'); + // Update all paths to be relative to spec directory + (this as any).stateDir = stateDir; + (this as any).configPath = path.join(stateDir, 'config.json'); + (this as any).scratchpadPath = path.join(stateDir, 'scratchpad.md'); + (this as any).logPath = path.join(stateDir, 'iteration.log'); + (this as any).hashPath = path.join(stateDir, 'spec.hash'); + } + + // Directory operations + + async ensureStateDir(): Promise { + const existed = await fs.pathExists(this.stateDir); + await fs.ensureDir(this.stateDir); + return existed; + } + + getStateDir(): string { + return this.stateDir; + } + + // Config operations + + async readConfig(): Promise { + try { + const content = await fs.readFile(this.configPath, 'utf8'); + return JSON.parse(content) as SessionConfig; + } catch { + return null; + } + } + + async writeConfig(config: SessionConfig): Promise { + await fs.writeFile( + this.configPath, + JSON.stringify(config, null, 2), + 'utf8' + ); + } + + async updateConfig(updates: Partial): Promise { + const existing = await this.readConfig(); + const updated = { ...DEFAULT_CONFIG, ...existing, ...updates } as SessionConfig; + await this.writeConfig(updated); + return updated; + } + + async incrementIteration(): Promise { + const config = await this.readConfig(); + if (!config) throw new Error('No session config found'); + config.currentIteration++; + await this.writeConfig(config); + return config.currentIteration; + } + + // Session state detection + + async hasExistingSession(): Promise { + const [configExists, scratchpadExists] = await Promise.all([ + fs.pathExists(this.configPath), + fs.pathExists(this.scratchpadPath), + ]); + return configExists || scratchpadExists; + } + + async detectSessionState(specPath: string): Promise { + // Ensure we're using the correct spec path + this.setSpecPath(specPath); + + const hasSession = await this.hasExistingSession(); + if (!hasSession) { + return 'new'; + } + + // Check if the spec path has changed + const existingConfig = await this.readConfig(); + if (existingConfig && existingConfig.specPath !== specPath) { + return 'changed'; + } + + // Check if spec content has changed (using hash of overview.md) + const specChanged = await this.hasSpecChanged(specPath); + return specChanged ? 'changed' : 'continue'; + } + + // Hash management for spec change detection + + async computeSpecHash(specPath: string): Promise { + const overviewPath = path.join(specPath, 'overview.md'); + try { + const content = await fs.readFile(overviewPath, 'utf8'); + return computeHash(content); + } catch { + return ''; + } + } + + async readStoredHash(): Promise { + try { + return await fs.readFile(this.hashPath, 'utf8'); + } catch { + return null; + } + } + + async storeHash(hash: string): Promise { + await fs.writeFile(this.hashPath, hash, 'utf8'); + } + + async hasSpecChanged(specPath: string): Promise { + const stored = await this.readStoredHash(); + if (!stored) return true; + const current = await this.computeSpecHash(specPath); + return !hashesMatch(stored, current); + } + + async updateSpecHash(specPath: string): Promise { + const hash = await this.computeSpecHash(specPath); + await this.storeHash(hash); + } + + // Scratchpad operations + + async initializeScratchpad(): Promise { + await fs.writeFile(this.scratchpadPath, SCRATCHPAD_TEMPLATE, 'utf8'); + } + + async readScratchpad(): Promise { + try { + return await fs.readFile(this.scratchpadPath, 'utf8'); + } catch { + return ''; + } + } + + async writeScratchpad(content: string): Promise { + await fs.writeFile(this.scratchpadPath, content, 'utf8'); + } + + // Iteration log operations + + async appendIterationLog(entry: IterationLogEntry): Promise { + const line = JSON.stringify(entry) + '\n'; + await fs.appendFile(this.logPath, line, 'utf8'); + } + + async readIterationLog(): Promise { + try { + const content = await fs.readFile(this.logPath, 'utf8'); + return content + .trim() + .split('\n') + .filter(Boolean) + .map((line) => JSON.parse(line) as IterationLogEntry); + } catch { + return []; + } + } + + async getLastIteration(): Promise { + const log = await this.readIterationLog(); + return log.length > 0 ? log[log.length - 1] : null; + } + + // State clearing and initialization + + async clearState(): Promise { + const filesToDelete = [ + this.scratchpadPath, + this.configPath, + this.logPath, + this.hashPath, + ]; + + await Promise.all( + filesToDelete.map((file) => fs.remove(file).catch(() => {})) + ); + } + + async initializeNewSession(config: SessionConfig): Promise { + // Ensure spec path is set before initializing + this.setSpecPath(config.specPath); + + await this.clearState(); + await this.ensureStateDir(); + await this.initializeScratchpad(); + await this.writeConfig({ + ...config, + startedAt: new Date().toISOString(), + currentIteration: 0, + }); + const hash = await this.computeSpecHash(config.specPath); + await this.storeHash(hash); + } +} diff --git a/plan2code-loop/src/utils/completion.ts b/plan2code-loop/src/utils/completion.ts new file mode 100644 index 0000000..2226c61 --- /dev/null +++ b/plan2code-loop/src/utils/completion.ts @@ -0,0 +1,74 @@ +// Completion markers for plan2code-loop +const COMPLETION_MARKERS = ['TASK_COMPLETE', 'TASK_BLOCKED', 'LOOP_COMPLETE'] as const; + +export type CompletionMarker = (typeof COMPLETION_MARKERS)[number]; + +export interface CompletionCheckResult { + completed: boolean; + marker?: CompletionMarker; + taskId?: string; // e.g., "1.1", "2.3" + taskName?: string; // e.g., "Initialize project structure" + reason?: string; // For TASK_BLOCKED: reason +} + +export function checkForCompletion(output: string): CompletionCheckResult { + // Check for LOOP_COMPLETE first (highest priority) + if (output.includes('LOOP_COMPLETE')) { + return { completed: true, marker: 'LOOP_COMPLETE' }; + } + + // Check for TASK_COMPLETE with task info + // Format: 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); + if (completeMatch) { + return { + completed: true, + marker: 'TASK_COMPLETE', + taskId: completeMatch[1], + taskName: completeMatch[2].trim(), + }; + } + + // Simple TASK_COMPLETE without structured info - try to extract task from context + if (output.includes('TASK_COMPLETE')) { + // Try to find task info nearby + const taskMatch = output.match(/(?:task|completed?)\s+(\d+\.\d+)[:\s]+([^\n]{5,80})/i); + return { + completed: true, + marker: 'TASK_COMPLETE', + taskId: taskMatch?.[1], + taskName: taskMatch?.[2]?.trim(), + }; + } + + // Check for TASK_BLOCKED with task info and reason + // Format: TASK_BLOCKED: 1.1 - Reason why blocked + const blockedWithTaskMatch = output.match(/TASK_BLOCKED[:\[\s]+(\d+\.\d+)[\]:\-\s]+(.+?)(?:\n|$)/i); + if (blockedWithTaskMatch) { + return { + completed: true, + marker: 'TASK_BLOCKED', + taskId: blockedWithTaskMatch[1], + reason: blockedWithTaskMatch[2].trim(), + }; + } + + // TASK_BLOCKED with just reason (no task ID) + const blockedMatch = output.match(/TASK_BLOCKED:\s*(.+?)(?:\n|$)/); + if (blockedMatch) { + return { + completed: true, + marker: 'TASK_BLOCKED', + reason: blockedMatch[1].trim() + }; + } + + // Simple TASK_BLOCKED without any info + if (output.includes('TASK_BLOCKED')) { + return { completed: true, marker: 'TASK_BLOCKED', reason: 'No reason provided' }; + } + + return { completed: false }; +} diff --git a/plan2code-loop/src/utils/git.ts b/plan2code-loop/src/utils/git.ts new file mode 100644 index 0000000..94a3567 --- /dev/null +++ b/plan2code-loop/src/utils/git.ts @@ -0,0 +1,134 @@ +import { execa } from 'execa'; +import { existsSync, readFileSync, writeFileSync } from 'fs'; +import { join } from 'path'; +import { logger } from './logger.js'; + +export interface GitCommitOptions { + taskName: string; + jiraTicketId?: string; + cwd?: string; +} + +/** + * Check if directory is a git repository + */ +export async function isGitRepo(cwd: string): Promise { + const result = await execa('git', ['rev-parse', '--git-dir'], { cwd, reject: false }); + return result.exitCode === 0; +} + +/** + * Initialize a git repository if one doesn't exist + */ +export async function ensureGitRepo(cwd: string): Promise { + if (await isGitRepo(cwd)) { + return true; + } + + logger.info('Initializing git repository...'); + const result = await execa('git', ['init'], { cwd, reject: false }); + + if (result.exitCode !== 0) { + logger.error(`Failed to initialize git repo: ${result.stderr}`); + return false; + } + + logger.success('Git repository initialized'); + return true; +} + +/** + * Required entries for the .gitignore file + */ +const REQUIRED_GITIGNORE_ENTRIES = ['specs/', 'specs--completed/', 'nul']; + +/** + * Ensure .gitignore exists with required entries + */ +export function ensureGitignore(cwd: string): void { + const gitignorePath = join(cwd, '.gitignore'); + let content = ''; + + if (existsSync(gitignorePath)) { + content = readFileSync(gitignorePath, 'utf-8'); + } + + const lines = content.split('\n').map(line => line.trim()); + const missingEntries = REQUIRED_GITIGNORE_ENTRIES.filter(entry => !lines.includes(entry)); + + if (missingEntries.length > 0) { + const needsNewline = content.length > 0 && !content.endsWith('\n'); + const newContent = content + (needsNewline ? '\n' : '') + missingEntries.join('\n') + '\n'; + writeFileSync(gitignorePath, newContent); + logger.dim(`Added to .gitignore: ${missingEntries.join(', ')}`); + } +} + +/** + * Create a local git commit for a completed task + */ +export async function createTaskCommit(options: GitCommitOptions): Promise { + const { taskName, jiraTicketId, cwd = process.cwd() } = options; + + logger.dim(`Git commit check in: ${cwd}`); + + try { + // Ensure we have a git repo + if (!await ensureGitRepo(cwd)) { + return false; + } + + // Ensure .gitignore exists with required entries + ensureGitignore(cwd); + + // Check if there are any changes to commit + const statusResult = await execa('git', ['status', '--porcelain'], { cwd, reject: false }); + + // Debug: show what git status returned + if (statusResult.stdout?.trim()) { + logger.dim(`Git status found changes:\n${statusResult.stdout.slice(0, 500)}`); + } + + if (statusResult.exitCode !== 0) { + logger.error(`Git status failed: ${statusResult.stderr}`); + return false; + } + + if (!statusResult.stdout?.trim()) { + logger.dim('No changes to commit'); + return false; + } + + // Stage all changes + const addResult = await execa('git', ['add', '-A'], { cwd, reject: false }); + if (addResult.exitCode !== 0) { + logger.error(`Git add failed: ${addResult.stderr}`); + return false; + } + + // Build commit message + let commitMessage = taskName; + if (jiraTicketId) { + commitMessage = `${taskName}\n\n${jiraTicketId}`; + } + + // Create the commit + const commitResult = await execa('git', ['commit', '-m', commitMessage], { cwd, reject: false }); + + if (commitResult.exitCode !== 0) { + // Check if it's just "nothing to commit" vs actual error + if (commitResult.stdout?.includes('nothing to commit') || commitResult.stderr?.includes('nothing to commit')) { + logger.dim('No changes to commit'); + return false; + } + logger.error(`Git commit failed: ${commitResult.stderr || commitResult.stdout}`); + return false; + } + + logger.success(`Created commit: ${taskName}${jiraTicketId ? ` (${jiraTicketId})` : ''}`); + return true; + } catch (error) { + logger.error(`Failed to create commit: ${error instanceof Error ? error.message : String(error)}`); + return false; + } +} diff --git a/plan2code-loop/src/utils/index.ts b/plan2code-loop/src/utils/index.ts new file mode 100644 index 0000000..c4e48d1 --- /dev/null +++ b/plan2code-loop/src/utils/index.ts @@ -0,0 +1,17 @@ +export { logger, MASCOT, type Logger } from './logger.js'; +export { + checkForCompletion, + type CompletionMarker, + type CompletionCheckResult +} from './completion.js'; +export { + executeCommand, + type ExecuteOptions, + type ExecuteResult +} from './process.js'; +export { + createTaskCommit, + isGitRepo, + ensureGitRepo, + type GitCommitOptions +} from './git.js'; diff --git a/plan2code-loop/src/utils/logger.ts b/plan2code-loop/src/utils/logger.ts new file mode 100644 index 0000000..827810d --- /dev/null +++ b/plan2code-loop/src/utils/logger.ts @@ -0,0 +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; diff --git a/plan2code-loop/src/utils/process.ts b/plan2code-loop/src/utils/process.ts new file mode 100644 index 0000000..de5a521 --- /dev/null +++ b/plan2code-loop/src/utils/process.ts @@ -0,0 +1,81 @@ +import { execa, type Options as ExecaOptions } from 'execa'; + +const MAX_OUTPUT_SIZE = 10 * 1024 * 1024; // 10MB + +function truncateOutput(output: string, maxSize: number): string { + if (output.length > maxSize) { + return output.slice(0, maxSize) + '\n...[truncated]'; + } + return output; +} + +export interface ExecuteOptions { + command: string; + args: string[]; + cwd: string; + timeout: number; // milliseconds + env?: Record; + signal?: AbortSignal; // For cancellation + stdin?: string; // Input to pass via stdin as string + stdinFile?: string; // Path to file to pipe as stdin +} + +export interface ExecuteResult { + stdout: string; + stderr: string; + exitCode: number; + timedOut: boolean; + cancelled: boolean; + duration: number; // milliseconds +} + +export async function executeCommand( + options: ExecuteOptions +): Promise { + const startTime = Date.now(); + + try { + const execaOptions: ExecaOptions = { + cwd: options.cwd, + timeout: options.timeout, + env: { ...process.env, ...options.env }, + reject: false, + all: true, + }; + + // Add cancellation signal if provided + if (options.signal) { + (execaOptions as any).cancelSignal = options.signal; + } + + // Add stdin input if provided (string or file) + if (options.stdin) { + (execaOptions as any).input = options.stdin; + } else if (options.stdinFile) { + (execaOptions as any).inputFile = options.stdinFile; + } + + const result = await execa(options.command, options.args, execaOptions); + + return { + stdout: truncateOutput(result.stdout || '', MAX_OUTPUT_SIZE), + stderr: truncateOutput(result.stderr || '', MAX_OUTPUT_SIZE), + exitCode: result.exitCode ?? 1, + timedOut: result.timedOut ?? false, + cancelled: result.isCanceled ?? false, + duration: Date.now() - startTime, + }; + } catch (error: any) { + // Check if this was a cancellation + const isCancelled = error?.isCanceled || options.signal?.aborted; + + return { + stdout: error?.stdout || '', + stderr: error?.stderr || (error instanceof Error ? error.message : String(error)), + exitCode: isCancelled ? -1 : 1, + timedOut: false, + cancelled: isCancelled, + duration: Date.now() - startTime, + }; + } +} diff --git a/plan2code-loop/tsconfig.json b/plan2code-loop/tsconfig.json new file mode 100644 index 0000000..c8f5baa --- /dev/null +++ b/plan2code-loop/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "lib": ["ES2022"], + "outDir": "dist", + "rootDir": ".", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/plan2code-loop/tsup.config.ts b/plan2code-loop/tsup.config.ts new file mode 100644 index 0000000..d540ce2 --- /dev/null +++ b/plan2code-loop/tsup.config.ts @@ -0,0 +1,15 @@ +import { defineConfig } from 'tsup'; + +export default defineConfig({ + entry: { + 'bin/plan2code-loop': 'src/bin/plan2code-loop.ts', + index: 'src/index.ts', + }, + format: ['esm'], + dts: true, + clean: true, + sourcemap: true, + banner: { + js: '#!/usr/bin/env node', + }, +}); diff --git a/src/plan2code---init-update.md b/src/plan2code---init-update.md index ddc3a01..f1dd0ab 100644 --- a/src/plan2code---init-update.md +++ b/src/plan2code---init-update.md @@ -2,6 +2,14 @@ Start all UPDATE AGENTS MODE responses with '🛞' +``` +⋅ + ╭───╮ + │ ● │ + │ ◡ │ Time to level up AGENTS.md! + ╰───╯ +``` + This prompt guides AI coding agents through an interactive Q&A flow to update an existing `AGENTS.md` file with new learnings, commands, and project knowledge. --- @@ -13,7 +21,7 @@ First, check if `AGENTS.md` exists in the project root. **If AGENTS.md does NOT exist:** > "No AGENTS.md found in this project. Would you like to create one from scratch instead? I can analyze the codebase and generate an initial AGENTS.md file." -Then stop and wait for user response. If they want to create one, use the `plan2code---init.md` workflow instead. +Then stop and wait for user response. If they want to create one, use the `smarsh2code---init.md` workflow instead. **If AGENTS.md EXISTS:** Read the file and provide a brief summary: @@ -37,8 +45,6 @@ Check if there is recent conversation context (work that was just completed in t > > "Would you like me to add any of these to AGENTS.md?" -Wait for user response before proceeding. - **If no recent work context:** Skip directly to Step 3. @@ -48,6 +54,14 @@ Skip directly to Step 3. Present the user with update options: +> ``` +> ⋅ +> ╭───╮ +> │ ● │ +> │ ~ │ What should we update? +> ╰───╯ +> ``` +> > "What would you like to add or update in AGENTS.md?" > > **Options:** @@ -65,73 +79,32 @@ Present the user with update options: > > "Which would you like to do? (You can pick multiple, e.g., '1 and 3')" -Wait for user response. - --- ## Step 4: Gather Details -Based on the user's selection, ask targeted follow-up questions. +Ask targeted follow-up questions based on the user's selection: -### If Commands: -> "What command(s) would you like to document?" -> - What does the command do? -> - Are there important flags or variations? -> - Any prerequisites or context needed? - -### If Architecture: -> "What architectural insight did you learn?" -> - Which components or modules are involved? -> - How do they interact? -> - Is this a pattern that repeats elsewhere in the codebase? - -### If Gotchas/Pitfalls: -> "What's the gotcha you encountered?" -> - What was the unexpected behavior? -> - What's the correct approach or workaround? -> - Where in the codebase does this apply? - -### If Testing: -> "What testing knowledge should be captured?" -> - Specific test commands or patterns? -> - Test data or fixture setup? -> - Mocking/stubbing approaches used in this project? - -### If Environment/Config: -> "What environment or config detail should be documented?" -> - Is this about local dev setup, CI, or deployment? -> - Are there specific env variables or files involved? - -### If General Rules: -> "What rule or convention should future agents follow?" -> - Does this apply project-wide or to specific areas? -> - Is this a "always do X" or "never do Y" type rule? -> - Why does this rule exist? (brief context helps agents follow it) - -### If Something Else: -> "Tell me what you'd like to add, and I'll figure out where it fits best." - -### If Review for Corrections: -> "I'll walk through the current AGENTS.md sections. For each, let me know if anything is outdated or incorrect." -> -> Then iterate through each section, asking: -> - "Is this still accurate?" -> - "Anything to update here?" - -### If Prune/Consolidate: -> "I'll review AGENTS.md for redundancy and verbosity. Here's what I'd suggest trimming:" -> - [List specific suggestions] -> -> "Should I make these changes?" +| Category | Key Questions | +|----------|---------------| +| **Commands** | What does it do? Important flags? Prerequisites? | +| **Architecture** | Which components? How do they interact? Repeating pattern? | +| **Gotchas** | What was unexpected? Correct approach/workaround? | +| **Testing** | Specific commands? Fixtures? Mocking patterns? | +| **Environment** | Local/CI/deploy? Which env vars or files? | +| **Rules** | Project-wide or specific? "Always do X" or "never do Y"? Why? | +| **Other** | "Tell me what to add, I'll find where it fits." | +| **Review** | Walk through each section: "Still accurate? Anything to update?" | +| **Prune** | Suggest specific trims, ask "Should I make these changes?" | --- -## Step 5: Confirm Understanding +## Step 5: Confirm & Apply Before making changes, confirm with the user: > "Here's what I'm going to add/update:" -> +> > **Section:** [section name] > **Change:** [brief description of the change] > ``` @@ -140,21 +113,13 @@ Before making changes, confirm with the user: > > "Does this look right? (yes/no/adjust)" -If the user says "adjust," ask what to change and repeat Step 5. +If "adjust," ask what to change and repeat. If "yes," apply the edit: +- Insert in the appropriate section (create if needed) +- Preserve existing structure, formatting, and heading styles --- -## Step 6: Apply Update - -Make the targeted edit to AGENTS.md: -- Insert new content in the appropriate section -- If a relevant section doesn't exist, create it in a logical location -- Preserve existing structure and formatting style -- Use the same heading levels and list styles as the existing file - ---- - -## Step 7: Summary & Next +## Step 6: Summary & Next After applying the update: @@ -166,6 +131,73 @@ After applying the update: If the user wants to add more, return to Step 3. +**When the user is done** (responds "no" or similar), proceed to Step 7. + +--- + +## Step 7: AI Agent File Sync + +After the user finishes updating AGENTS.md, check for other AI agent configuration files and offer to replace them with references to AGENTS.md. + +### Files to Detect + +Check if any of these exist: +- `CLAUDE.md` (root) → use `./AGENTS.md` +- `GEMINI.md` (root) → use `./AGENTS.md` +- `.cursorrules` (root) → use `./AGENTS.md` +- `.github/copilot-instructions.md` → use `../AGENTS.md` +- `.cursor/rules/*.md` → use `../../AGENTS.md` +- `.windsurf/rules/*.md` → use `../../AGENTS.md` + +**If no files found:** Skip silently and end the workflow. + +### User Confirmation + +If files are found, display: + +> ``` +> ⋅ +> ╭───╮ +> │ ● │ +> │ ~ │ Found some other AI agent configs! +> ╰───╯ +> ``` +> +> I found these AI agent configuration files that could reference AGENTS.md: +> +> | File | Current Size | +> |------|--------------| +> | `CLAUDE.md` | 45 lines | +> | `.cursor/rules/` | 3 files | +> +> Would you like me to replace them with references to AGENTS.md? +> - **Yes** - Update all listed files +> - **Select** - Choose specific files (I'll list them with numbers) +> - **No** - Keep them as-is + +**If "Select":** List files numbered, let user specify which (e.g., "1 and 3"). + +**Warning:** For files with >10 lines, note: "CLAUDE.md has custom content that will be replaced." + +### Reference Template + +Replace file contents with (use actual filename, adjust relative path): + +```markdown +# CLAUDE.md + +See [AGENTS.md](./AGENTS.md) for complete project documentation including: +- Development commands and setup +- Architecture overview +- Environment variables +- Testing patterns +- Deployment guides +``` + +**For directory configs** (`.cursor/rules/`, `.windsurf/rules/`): Delete all existing `.md` files and create a single `reference.md`. + +After Step 7 completes (or is skipped), the update workflow is complete. + --- ## Update Rules (for the agent) @@ -227,5 +259,14 @@ Agent: Done! Changes applied. User: No, we're good. -Agent: Great! AGENTS.md is updated. Happy coding! +Agent: I found a CLAUDE.md file (23 lines). Would you like me to replace it + with a reference to AGENTS.md? + - Yes - Update it + - No - Keep it as-is + +User: Yes + +Agent: Done! Updated CLAUDE.md to reference AGENTS.md. + + All set! AGENTS.md is your single source of truth now. Happy coding! ``` diff --git a/src/plan2code---init.md b/src/plan2code---init.md index 6de0afe..94e5129 100644 --- a/src/plan2code---init.md +++ b/src/plan2code---init.md @@ -2,7 +2,15 @@ Start all CREATE AGENTS MODE responses with '💡' -Please analyze this codebase and create an `AGENTS.md` file, which will be given to future instances of this AI coding agent (like Claude Code, Codex or Gemini Cli) a simple set of rules to operate in this project. +``` +⋅ + ╭───╮ + │ ● │ + │ ◡ │ Let me explore your codebase! + ╰───╯ +``` + +Please analyze this codebase and create an `AGENTS.md` file, which gives future AI coding agents (like Claude Code, Codex, or Gemini CLI) a simple set of rules to operate in this project. What to add: @@ -12,10 +20,11 @@ What to add: Usage notes: - If there's already an `./AGENTS.md`, suggest improvements to it vs creating a new file. +- If `AGENTS.md` does NOT exist but `CLAUDE.md` does exist, read and use `CLAUDE.md` content as context when generating the new `AGENTS.md`. This ensures existing project knowledge is preserved and migrated. - When you make the initial `./AGENTS.md` do not repeat yourself and do not include obvious instructions like "Provide helpful error messages to users", "Write unit tests for all new utilities", "Never include sensitive information (API keys, tokens) in code or commits" - Avoid listing every component or file structure that can be easily discovered - Don't include generic development practices -- If there are Cursor rules (in .cursor/rules/ or .cursorrules), AGENTS.md, GEMINI.md or Copilot rules (in .github/copilot-instructions.md), make sure to include the important parts. +- If there are Cursor rules (in .cursor/rules/ or .cursorrules), GEMINI.md or Copilot rules (in .github/copilot-instructions.md), make sure to include the important parts. - If there is a README.md, PROJECT.md, make sure to include the important parts. - Do not make up information such as "Common Development Tasks", "Tips for Development", "Support and Documentation" unless this is expressly included in other files that you read. - Be sure to prefix the file with the following text: @@ -31,4 +40,57 @@ Best practices: * Good rules are focused, actionable, and scoped. * Keep rules under 500 lines * Avoid vague guidance. Write rules like clear internal docs -* Reuse rules when repeating prompts in chat \ No newline at end of file +* Reuse rules when repeating prompts in chat + +--- + +## AI Agent File Sync + +After creating `AGENTS.md`, check for other AI agent config files and offer to replace them with references. + +### Files to Detect + +| File | Title | Path | +|------|-------|------| +| `CLAUDE.md` | CLAUDE.md | `./AGENTS.md` | +| `GEMINI.md` | GEMINI.md | `./AGENTS.md` | +| `.cursorrules` | .cursorrules | `./AGENTS.md` | +| `.github/copilot-instructions.md` | Copilot Instructions | `../AGENTS.md` | +| `.cursor/rules/*.md` | Project Rules | `../../AGENTS.md` | +| `.windsurf/rules/*.md` | Project Rules | `../../AGENTS.md` | + +For directory configs (`.cursor/rules/`, `.windsurf/rules/`): delete all existing `.md` files and create a single `reference.md`. + +### User Confirmation + +If any files are found, show the mascot and list what was found: + +``` +⋅ + ╭───╮ + │ ● │ + │ ~ │ Found some other AI agent configs! + ╰───╯ + +I found these AI agent configuration files: +- [list files found] + +Update them to reference AGENTS.md? (Yes / Select / No) +``` + +Only modify files the user confirms. + +### Reference Template + +Replace file content with (using Title and Path from table above): + +```markdown +# [Title] + +See [AGENTS.md]([Path]) for complete project documentation including: +- Development commands and setup +- Architecture overview +- Environment variables +- Testing patterns +- Deployment guides +``` \ No newline at end of file diff --git a/src/plan2code---quick-task.md b/src/plan2code---quick-task.md index 1c024b8..33f8278 100644 --- a/src/plan2code---quick-task.md +++ b/src/plan2code---quick-task.md @@ -14,6 +14,14 @@ You are a senior software architect and engineer. Your purpose is to thoroughly 2. **If `./AGENTS.md` does NOT exist:** STOP and respond with: + > ``` + > ⋅ + > ╭───╮ + > │ ● │ + > │ ~ │ Hmm, I don't see an AGENTS.md... + > ╰───╯ + > ``` + > > "⚠️ No `AGENTS.md` found in this project. > > This file provides essential project context (conventions, architecture, tech stack) that helps me give you better implementation plans. @@ -33,6 +41,13 @@ You are a senior software architect and engineer. Your purpose is to thoroughly - Keep plans concise and actionable - Focus on the immediate implementation, not future enhancements - This is a standalone workflow - does NOT create spec files or feed into steps 2-4 +``` +⋅ + ╭───╮ + │ ● │ + │ ~ │ I'm ready to help! + ╰───╯ +``` I have a feature request for you. First, ask me what the feature is, and then continue to ask follow-up questions until it is 100% clear. @@ -128,4 +143,11 @@ If scope is within thresholds (or user chose to continue), present the implement - [ ] [How to test/confirm success] --- +``` +⋅ + ╭───╮ + │ ★ │ + │ ◡ │ Plan ready! What do you think? + ╰───╯ +``` Ready to implement? (yes / modify plan / escalate to full planning / abort) diff --git a/src/plan2code-1--plan.md b/src/plan2code-1--plan.md index 48d4522..440235d 100644 --- a/src/plan2code-1--plan.md +++ b/src/plan2code-1--plan.md @@ -14,9 +14,17 @@ You are a senior software architect and technical product manager with extensive 2. **If `./AGENTS.md` does NOT exist:** STOP and respond with: + > ``` + > ⋅ + > ╭───╮ + > │ ● │ + > │ ~ │ Hmm, I don't see an AGENTS.md... + > ╰───╯ + > ``` + > > "⚠️ No `AGENTS.md` found in this project. > - > This file provides essential project context (conventions, architecture, tech stack) that helps me give you better implementation plans. + > This file provides essential project context (conventions, architecture, tech stack, rules) that helps me give you better implementation plans. If this is a new project you can continue without AGENTS.md, but creating it first with just basic information and rules you would like to apply to your project can be valuable. > > **To create it, first run:** > ``` @@ -287,6 +295,8 @@ State your scope assessment and ask the user to confirm before proceeding. **If confidence >= 90%:** Create and save the planning document to `specs/PLAN-DRAFT-.md` using the format below. Create the `specs/` folder if it doesn't exist. +**⚠️ CRITICAL - Next Step Reminder:** +After saving the PLAN-DRAFT, the next step is **DOCUMENTATION** (`/plan2code-2--document`), **NOT** implementation. The workflow is: Plan → Document → Implement → Finalize. You must direct the user to `/plan2code-2--document` in your closing message. **If confidence < 90%:** @@ -387,18 +397,26 @@ Structure every response in this order: ## Session End +**⚠️ WORKFLOW ORDER: Plan → Document → Implement → Finalize** +The next step after planning is **DOCUMENTATION** (`/plan2code-2--document`), **NOT** implementation (`/plan2code-3--implement`). Do NOT skip the documentation step. When planning is complete (PLAN-DRAFT created), ALWAYS tell the user: 1. What was accomplished (planning document created) 2. File to attach in next session: `specs/PLAN-DRAFT-.md` -3. Next command to use: `/plan2code-2--document` or equivalent +3. **Next command: `/plan2code-2--document`** (documentation, NOT implementation) 4. Any decisions they should consider before the next session -Example closing: +**Example closing (follow this exactly):** -> "Planning complete. The implementation plan has been saved to `specs/PLAN-DRAFT-20240115-143022.md`. +> "Planning complete. The plan has been saved to `specs/PLAN-DRAFT-20240115-143022.md` and is ready for documentation. > > ``` +> ⋅ +> ╭───╮ +> │ ★ │ +> │ ◡ │ Planning done! Ready for documentation! +> ╰───╯ +> > ╔═══════════════════════════════════════════════════════════════════╗ > ║ NEXT STEPS ║ > ╠═══════════════════════════════════════════════════════════════════╣ @@ -431,4 +449,5 @@ If the user says "abort", "cancel", "start over", or similar: - Your final planning phase is `PLANNING PHASE 7: Transition Decision` - You must NOT start implementation - your job is to "design and present a plan", not to build it - Every response must start with the phase prefix: `🤔 [PLANNING PHASE X: Name]` (except for the pre-flight check) -- Take time to think thoroughly - good planning prevents costly implementation mistakes \ No newline at end of file +- Take time to think thoroughly - good planning prevents costly implementation mistakes +- **WORKFLOW ORDER:** Plan → Document → Implement → Finalize. After planning, ALWAYS direct to `/smarsh2code-2--document` (NOT `/plan2code-3--implement`) \ No newline at end of file diff --git a/src/plan2code-1b--revise-plan.md b/src/plan2code-1b--revise-plan.md index f436369..7b9da50 100644 --- a/src/plan2code-1b--revise-plan.md +++ b/src/plan2code-1b--revise-plan.md @@ -59,6 +59,7 @@ Present findings and confirm understanding before proceeding. |------|-------------|------------| | **Additive** | New tasks/features, no existing work affected | Low | | **Modificative** | Changes to pending tasks | Medium | +| **Re-opening** | New tasks added to completed phases | Medium-High | | **Destructive** | Changes that invalidate completed work | High | | **Architectural** | Changes to tech stack or core design | Critical | @@ -76,10 +77,18 @@ Present findings and confirm understanding before proceeding. **Change Type:** [Type] **Tasks Affected:** [X] tasks across [Y] phases **Completed Work at Risk:** [None / List specific tasks] +**Phases to Re-open:** [None / List phases that will become incomplete] ### Proposed Changes 1. [Change description] 2. [Change description] +``` +⋅ + ╭───╮ + │ ● │ + │ ~ │ Here's the plan. What do you think? + ╰───╯ +``` Proceed with revision? (yes / no / discuss) ``` @@ -105,6 +114,15 @@ Make all approved changes to the spec files: 3. Update dependencies if affected 4. Preserve all completed `[x]` tasks unless explicitly approved to remove +5. **Re-opening completed phases:** When adding new tasks to a phase that was previously completed (`[x]` in overview.md): + - Add the new task(s) to the phase file with `🆕 ADDED` flag: + ```markdown + - [ ] **Task 2.5:** [New task description] 🆕 ADDED + - Added: [date] + - Reason: [brief reason] + ``` + - Update overview.md to uncheck that phase: `[x]` → `[ ]` + - Add HTML comment for traceability: `` ### STEP 4: Consistency Check `🔄 [REVISION] Step 4: Consistency Check` @@ -120,6 +138,8 @@ Verify the updated specs are internally consistent: - [ ] Tech stack updated if needed - [ ] Success criteria still achievable - [ ] overview.md phase checklist matches phase files +- [ ] Phases with any incomplete tasks are unchecked `[ ]` in overview.md +- [ ] Phases with ALL tasks complete are checked `[x]` in overview.md ``` Report any issues found and resolve before proceeding. @@ -144,6 +164,9 @@ Report any issues found and resolve before proceeding. - **Modified:** [Y] existing tasks - **Removed:** [Z] tasks (with approval) +### Phases Re-opened +- [None / List phases with new incomplete tasks] +- Phase X: [N] new tasks added - [reason] ### Revision Log Entry ``` @@ -160,6 +183,11 @@ Report any issues found and resolve before proceeding. 3. Remind user of next steps: ``` +⋅ + ╭───╮ + │ ★ │ + │ ◡ │ All revised! Ready to continue! + ╰───╯ ╔═══════════════════════════════════════════════════════════════════╗ ║ REVISION COMPLETE ║ ╠═══════════════════════════════════════════════════════════════════╣ diff --git a/src/plan2code-2--document.md b/src/plan2code-2--document.md index e05b8d0..e039a76 100644 --- a/src/plan2code-2--document.md +++ b/src/plan2code-2--document.md @@ -90,8 +90,39 @@ Each task should be: - Phase Checklist (from Implementation Phases) - Quick Reference (Key Files, Environment Variables, External Dependencies) 5. **Write** each `phase-X.md` file with detailed tasks -6. **Verify** all requirements from planning document are covered -7. **Present** summary to user and ask about the planning document +6. **Analyze** phases for parallel execution eligibility (see Parallel Eligibility Analysis below) +7. **Verify** all requirements from planning document are covered +8. **Present** summary to user and ask about the planning document +### Parallel Eligibility Analysis +After creating all phase files, analyze which phases can be executed in parallel. This enables users to run multiple agent instances simultaneously for faster implementation. +**Analysis Process:** +1. For each pair of adjacent phases (Phase N and Phase N+1), check for conflicts: + | Conflict Type | How to Detect | Result if Found | + |---------------|---------------|-----------------| + | **File Overlap** | Any task in Phase N modifies a file also modified in Phase N+1 | NOT parallel-eligible | + | **Prerequisite Dependency** | Phase N+1's Prerequisites section references Phase N | NOT parallel-eligible | + | **Data/Output Dependency** | Phase N+1 tasks require artifacts, exports, or state created by Phase N | NOT parallel-eligible | + | **Shared State** | Both phases modify the same database tables, config, or global state | NOT parallel-eligible | +2. Group consecutive phases with NO conflicts into Parallel Execution Groups: + - If Phases 2 and 3 have no conflicts → Group A: 2, 3 + - If Phase 4 depends on Phase 3 → Phase 4 starts a new sequence + - If Phases 5 and 6 have no conflicts → Group B: 5, 6 +3. Populate the "Parallel Execution Groups" table in `overview.md`: + **Example with parallel groups:** + ```markdown + | Group | Phases | Reason | + |-------|--------|--------| + | A | 2, 3 | Phase 2 (data models) and Phase 3 (API routes) touch separate files | + | B | 5, 6 | Phase 5 (frontend) and Phase 6 (tests) have no shared dependencies | + ``` + **Example with no parallel phases:** + ```markdown + | Group | Phases | Reason | + |-------|--------|--------| + | None | - | All phases must run sequentially due to dependencies | + ``` +4. Inform the user about parallel eligibility in the completion summary: + > **Parallel Execution:** Phases [X, Y] can be run simultaneously in separate agent instances. Phases [Z] must be run sequentially due to dependencies. ### Output Structure @@ -169,6 +200,13 @@ Check the Testing Strategy from the PLAN-DRAFT (section 2.4): ## Phase Checklist - [ ] Phase 1: [Name] - [Description] ... +## Parallel Execution Groups + + +| Group | Phases | Reason | +|-------|--------|--------| +| [A/B/etc or "None"] | [phase numbers] | [Why these can run in parallel] | +_If no phases can run in parallel, this table will show "None - all phases must run sequentially"_ ## Quick Reference ### Key Files @@ -257,6 +295,9 @@ Total phases: X Total tasks: Y Requirements coverage: [Confirm all planning requirements are addressed] +Parallel Execution Groups: +- Group A: Phases X, Y (can run simultaneously) +- [Or: "None - all phases must run sequentially"] ``` Then automatically archive the planning document: @@ -276,6 +317,12 @@ Example closing: > "Documentation complete. Implementation specs are in `specs/user-authentication/`. > > ``` +> ⋅ +> ╭───╮ +> │ ★ │ +> │ ◡ │ Specs are ready! Time to build! +> ╰───╯ +> > ╔═══════════════════════════════════════════════════════════════════╗ > ║ NEXT STEPS ║ > ╠═══════════════════════════════════════════════════════════════════╣ diff --git a/src/plan2code-3--implement.md b/src/plan2code-3--implement.md index 5713c94..d5a6075 100644 --- a/src/plan2code-3--implement.md +++ b/src/plan2code-3--implement.md @@ -31,9 +31,11 @@ If the user provides a path to an `overview.md` file (e.g., `specs/high-severity 1. Read the overview.md file 2. Find the "Phase Checklist" section -3. Identify the first unchecked `[ ]` phase - this is the next phase to implement -4. Automatically read the corresponding `phase-X.md` file from the same directory -5. Proceed with implementation +3. Identify workable phases — any phase marked `[ ]` (pending) or `[/]` (in-progress) +4. **Check for parallel execution options** (see Parallel Phase Selection below) +5. Apply phase selection logic based on status and parallelism +6. Automatically read the corresponding `phase-X.md` file from the same directory +7. Proceed with implementation **Option 2: Auto-detect from specs folder** @@ -49,6 +51,109 @@ If there are multiple active spec folders or nothing was found, ask the user to **Do not proceed until you have successfully read the overview.md and determined the next phase.** +### Phase Status Tracking + +Phases in overview.md use checkbox notation to track status: + +| Checkbox | Status | Meaning | +|----------|--------|---------| +| `[ ]` | Pending | Not yet started | +| `[/]` | In Progress | Started but not complete (may be active or paused/aborted) | +| `[x]` | Complete | Finished and approved | +| `[?]` | Assumed | Couldn't verify, assumed complete (for prerequisites) | + +**Status Transitions:** +- `[ ]` → `[/]` — When an agent STARTS working on a phase +- `[/]` → `[x]` — When user APPROVES a completed phase +- `[/]` stays `[/]` — On abort (preserves resume capability) + +**Important:** Never reset `[/]` back to `[ ]`. If work was started, it stays marked so users can consciously choose to resume. + +### Parallel Phase Selection + +After identifying the next workable phase(s), check if parallel execution options are available: + +1. **Look for "Parallel Execution Groups" section** in overview.md +2. **Find if the next phase belongs to a group** with other uncompleted phases +3. **Only show consecutive uncompleted phases** in the same group + +**Decision Logic:** + +``` +Find workable phases = all phases marked [ ] or [/] (not [x]) +Check Parallel Execution Groups table: + +CASE 1: Multiple parallel phases available + - If 2+ workable phases exist in the same parallel group + → Show parallel selection UI with status for each + +CASE 2: Single workable phase that is IN PROGRESS [/] + - Phase was started but not completed (possibly by another session) + → Show resume prompt: "Phase X is in progress. Resume? (yes/no)" + +CASE 3: Single workable phase that is PENDING [ ] + - Fresh phase, no parallel options + → Auto-start: mark [/] and begin implementation + +CASE 4: No Parallel Execution Groups section exists + → Fall back to sequential mode using Cases 2-3 logic +``` + +**Parallel Selection UI:** + +When parallel options are available (CASE 1), present this prompt: + +``` +⚡ PARALLEL PHASES AVAILABLE + +These phases can be worked on in parallel: + + [1] Phase N: [Name] [IN PROGRESS] ← if marked [/] + [2] Phase N+1: [Name] [AVAILABLE] ← if marked [ ] + [3] Phase N+2: [Name] [AVAILABLE] ← if marked [ ] + +Which phase would you like to work on? (1/2/3) + +┌─────────────────────────────────────────────────────────────────────────┐ +│ TIP: Run another agent instance with /plan2code-3--implement to work │ +│ on a different phase simultaneously. │ +│ │ +│ IN PROGRESS phases may be running in another session - selecting one │ +│ will resume work on it. │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +**Single Phase Resume UI:** + +When there's only one workable phase and it's in-progress (CASE 2): + +``` +⚡ PHASE RESUME CHECK + +Phase N: [Name] is marked as IN PROGRESS. + +This could mean: + • Another agent session is currently working on it + • A previous session was aborted mid-phase + +Resume this phase? (yes / no - I'll wait) +``` + +If user says "yes", proceed with implementation. +If user says "no", end session gracefully. + +**Important Rules:** +- Only show phases that are **consecutive** AND **in the same parallel group** AND **not complete** (`[ ]` or `[/]`) +- Stop showing options at the first phase that is NOT in the same group (even if later phases are) +- If user selects a phase, proceed with that phase +- After selection, mark the phase `[/]` in overview.md (if not already), then read `phase-X.md` and begin + +**Edge Cases:** +- If Parallel Execution Groups section is missing → fall back to sequential (Cases 2-3) +- If table shows "None" → fall back to sequential (Cases 2-3) +- If only one workable phase remains in the group → apply Cases 2-3 (resume or auto-start) +- If ALL parallel phases are `[/]` (all in progress) → show selection UI with all marked IN PROGRESS, warn user that work may be duplicated + ### Code Consistency Rules When implementing: @@ -85,18 +190,40 @@ Proceeding to Task 2.4 (no Stripe dependency). ## Process -### 1. Identify the Current Phase +### 1. Identify and Claim the Phase -Review `overview.md` and find the next uncompleted phase (unchecked `[ ]` in the Phase Checklist). +Review `overview.md` and find workable phases (`[ ]` or `[/]` in the Phase Checklist). -State: `⚡ [PHASE X: Phase Name] - Starting implementation` +Apply the decision logic from "Parallel Phase Selection" to determine which phase to work on. + +**Once a phase is selected:** +1. If the phase is `[ ]` (pending), immediately update it to `[/]` in overview.md +2. If the phase is already `[/]` (resuming), no change needed + +State: `⚡ [PHASE X: Phase Name] - Marking in-progress and starting implementation` ### 2. Verify Prerequisites -Check the Prerequisites section in the phase document: +Before implementing any tasks, process the Prerequisites section in order: -- All listed prerequisites must be complete -- If a prerequisite is not met, STOP and inform the user +1. Read each prerequisite +2. For each unchecked (`[ ]`) prerequisite: + - **Verifiable:** Check if condition is met (e.g., check overview.md for phase completion) → mark `[x]` + - **Actionable:** Complete the action (e.g., install dependencies) → mark `[x]` + - **Cannot verify:** Mark `[?]` with note explaining assumption + - **Blocked:** Mark `[!]` with blocker reason, STOP the phase + +3. Only proceed to tasks when ALL prerequisites are `[x]` or `[?]` + +**Example:** +```markdown +## Prerequisites +- [x] Phase 1: Core Setup is complete ← verified via overview.md +- [?] Design approved by stakeholder ← cannot verify, assuming complete +- [!] API credentials configured ← BLOCKED: .env missing STRIPE_KEY +``` + +If any prerequisite is marked `[!]`, STOP and inform the user before proceeding ### 3. Implement Tasks Sequentially @@ -141,9 +268,9 @@ After completing all tasks and self-review: When user replies "approved": -1. Update `overview.md` - mark the phase checkbox `[x]` -2. Update `Phase X.md` - change Status to "Complete" -3. Confirm completion and provide next steps +1. Update `overview.md` - change the phase checkbox from `[/]` to `[x]` +2. Update `phase-X.md` - change Status to "Complete" +3. Confirm completion and provide next steps (see Session End section) ### Handling Blockers @@ -272,6 +399,14 @@ Before approving, confirm this phase is working: - **App runs** (if applicable): Start command runs without crashing - **Quick check**: [Describe 1-2 specific things to verify based on what was built] +``` +⋅ + ╭───╮ + │ ● │ + │ ~ │ Ready for your review! + ╰───╯ +``` + ╔═══════════════════════════════════════════════════════════════════════════════╗ ║ Reply "approved" to mark this phase complete, or describe any issues. ║ ╚═══════════════════════════════════════════════════════════════════════════════╝ @@ -300,7 +435,7 @@ This creates a checkpoint you can return to if needed. The next uncompleted phase is Phase Y: [Name]. To continue, start a NEW conversation with: -> /plan2code-3--implement specs//overview.md +> /smarsh2code-3--implement specs//overview.md The command will auto-detect Phase Y as the next phase to implement. ``` @@ -322,6 +457,12 @@ Example for continuing: > Phase marked complete in overview.md. > > ``` +> ⋅ +> ╭───╮ +> │ ★ │ +> │ ◡ │ Phase done! Great progress! +> ╰───╯ +> > ╔═══════════════════════════════════════════════════════════════════╗ > ║ NEXT STEPS ║ > ╠═══════════════════════════════════════════════════════════════════╣ @@ -348,6 +489,12 @@ Example for final phase: > This was the final implementation phase! > > ``` +> ⋅ +> ╭───╮ +> │ ★ │ +> │ ◡ │ All phases complete! Amazing work! +> ╰───╯ +> > ╔═══════════════════════════════════════════════════════════════════╗ > ║ NEXT STEPS - FINAL PHASE COMPLETE ║ > ╠═══════════════════════════════════════════════════════════════════╣ @@ -369,11 +516,12 @@ Example for final phase: If the user says "abort", "cancel", "start over", or similar: -1. Confirm: "Are you sure you want to abort Phase X? Partial progress will remain in the spec files." +1. Confirm: "Are you sure you want to abort Phase X? The phase will remain marked as in-progress `[/]` for resuming later." 2. If confirmed: - List which tasks were completed vs. remaining - Note any files that were created/modified - - Explain checkboxes reflect current state + - **Do NOT change the phase checkbox** — it stays `[/]` so it can be resumed + - Explain: "The phase remains `[/]` in overview.md. Run `/plan2code-3--implement` again to resume." 3. Do not continue with implementation ## Recovery diff --git a/src/plan2code-4--finalize.md b/src/plan2code-4--finalize.md index 60521a5..a63fc16 100644 --- a/src/plan2code-4--finalize.md +++ b/src/plan2code-4--finalize.md @@ -14,7 +14,7 @@ You are a QA engineer and technical lead performing final validation before a fe - Incomplete tasks are found (Step 1) - Documentation updates are proposed (Step 4) - Do NOT make documentation changes without explicit user approval -- Archive specs to `specs--completed//` - preserve folder name exactly +- Archive specs to `specs--completed/-/` - preserve feature name exactly and append timestamp - This is validation and cleanup only - do NOT write implementation code - If you cannot perform file operations, output file contents in code blocks with the intended file path as the header - If you cannot access the filesystem, ask the user to paste relevant file contents @@ -251,6 +251,14 @@ Add this summary to `overview.md` under `## Completion Summary`. **If ANY documentation needs updates:** +> ``` +> ⋅ +> ╭───╮ +> │ ● │ +> │ ~ │ Found some docs that need updating! +> ╰───╯ +> ``` +> > "The following documentation updates are recommended. Please review and approve before I make these changes: > > [List proposed changes] @@ -334,6 +342,13 @@ specs--completed/ --- +``` +⋅ + ╭───╮ + │ ★ │ + │ ◡ │ You did it! Feature complete! + ╰───╯ +``` ╔═══════════════════════════════════════════════════════════════════╗ ║ IMPLEMENTATION COMPLETE ║ ╠═══════════════════════════════════════════════════════════════════╣ diff --git a/version.json b/version.json index 3ab6533..c0c7492 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "name": "Plan2Code", - "version": "1.3.1", + "version": "1.5.2", "description": "A structured 4-step workflow methodology for AI-assisted software development", "keywords": [ "ai",