mirror of
https://github.com/jparkerweb/plan2code.git
synced 2026-07-21 18:33:22 -07:00
Compare commits
28 Commits
v1.5.4
..
aedfe944b2
| Author | SHA1 | Date | |
|---|---|---|---|
| aedfe944b2 | |||
| 9b3c06b498 | |||
| 8fe1bfa805 | |||
| 68542fd778 | |||
| 4656a6ebb7 | |||
| 7e9de755f1 | |||
| 4b20f93bb9 | |||
| 5b3b84f0a9 | |||
| 1015b99a6a | |||
| 20a9e2f040 | |||
| 28bda05cbf | |||
| 5274bdbd51 | |||
| 25d874d87d | |||
| 5e48e98293 | |||
| 8e0b1a2ab2 | |||
| c2579a7b6a | |||
| 9d1104d105 | |||
| 72fed09aab | |||
| 37311a7e68 | |||
| c92865c395 | |||
| 63656d339d | |||
| 628e688ab9 | |||
| 157e55ef2e | |||
| 02fd4efa40 | |||
| 348c8ed7b2 | |||
| 5c2f70a37c | |||
| de46275b51 | |||
| b7b2595fe1 |
@@ -0,0 +1,120 @@
|
||||
# Architecture
|
||||
> Part of [AGENTS.md](../AGENTS.md) — project guidance for AI coding agents.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
plan2code/
|
||||
├── src/ # Source workflow prompts (9 markdown files)
|
||||
│ └── plan2code-review-references/ # Reference files for review skill
|
||||
│ ├── verification-protocol.md # Deep verification + confidence calibration
|
||||
│ ├── dimensions.md # 11 dimensions with detailed checklists
|
||||
│ └── false-positives.md # Known false-positive patterns
|
||||
├── plan2code-loop/ # Autonomous loop CLI tool (Node.js/TypeScript)
|
||||
│ ├── src/ # TypeScript source
|
||||
│ └── dist/ # Built output (tsup)
|
||||
├── plan2code-metrics/ # Recursive self-improvement toolchain
|
||||
│ ├── src/ # TypeScript source
|
||||
│ │ └── prompts/ # Internal AI prompt templates (no char limit)
|
||||
│ └── dist/ # Built output (tsup)
|
||||
├── src/statusline-claude/ # Claude CLI status line (Node.js, zero deps, single file)
|
||||
│ ├── statusline.js # Self-contained: config, git, formatters, render
|
||||
│ └── statusline-config.json # Default config template
|
||||
├── scripts/ # Development scripts
|
||||
│ └── validate-char-count.js # Pre-commit character count validator
|
||||
├── dist/ # Generated distribution files (auto-generated)
|
||||
│ ├── global-commands/ # For global installation (~/.claude/, etc.)
|
||||
│ └── local-commands/ # For per-project installation (.claude/, etc.)
|
||||
├── .husky/ # Git hooks (husky)
|
||||
│ └── pre-commit # Runs character count validation
|
||||
├── .claude/ # Repo-local Claude Code config (NOT installed by install.js)
|
||||
│ └── skills/ # Maintainer-only dev skills, e.g. plan2code-publish/
|
||||
├── docs/ # Documentation and assets
|
||||
├── specs/ # Feature specs (if any in-progress)
|
||||
├── install.js # Interactive installer (Node.js)
|
||||
├── package.json # Root package (husky only, private: true)
|
||||
├── 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") |
|
||||
| `scripts/validate-char-count.js` | Pre-commit validator ensuring all source prompts ≤ 11,000 chars |
|
||||
| `version.json` | Version metadata (name, version, description) |
|
||||
| `QUICK-REFERENCE.md` | User quick-reference card |
|
||||
| `src/statusline-claude/` | Claude CLI status bar (included in `A` Install All + dev tools; also via Custom → S) |
|
||||
|
||||
## Workflow Prompts (in `src/`)
|
||||
|
||||
| File | Step | Purpose |
|
||||
|------|------|---------|
|
||||
| `plan2code-init.md` | Init | Generate AGENTS.md as index + `.agents-docs/` section files (progressive discovery) |
|
||||
| `plan2code-init-update.md` | Update | Update AGENTS.md with learnings; detects and routes edits to `.agents-docs/` files |
|
||||
| `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-review.md` | review | Post-implementation comprehensive review |
|
||||
| `plan2code-4-finalize.md` | 4 | Validate, summarize, feedback, archive (7 steps) |
|
||||
| `plan2code-handoff.md` | handoff | Compact the conversation into a self-contained handoff document |
|
||||
|
||||
## Naming Convention
|
||||
|
||||
Workflow files follow a strict naming pattern:
|
||||
- **Utilities:** `plan2code-<name>.md` (single dash)
|
||||
- **Numbered steps:** `plan2code-<N>-<name>.md` (single dash, number, single dash)
|
||||
|
||||
Examples:
|
||||
- `plan2code-init.md` (utility)
|
||||
- `plan2code-1-plan.md` (step 1)
|
||||
- `plan2code-1b-revise-plan.md` (step 1b)
|
||||
|
||||
## Reference Files
|
||||
|
||||
Some workflows use companion reference files for depth that exceeds the 11k char limit. The orchestrator (main workflow file) loads them via `Read` directives during execution.
|
||||
|
||||
**Pattern:** `src/<source-filename-without-extension>-references/` (e.g., `plan2code-review-references/`)
|
||||
|
||||
**How the installer handles them:**
|
||||
- **Skill-directory platforms** (Claude Code, Agents, Crush, Devin): reference files are nested as `<skill-name>/references/`. Read paths use canonical `references/<file>.md`.
|
||||
- **Flat-file platforms** (Windsurf, Cursor, Copilot, Continue): reference files are placed as a sibling directory. The installer rewrites Read paths to the sibling directory name (e.g., `plan2code-review-references/<file>.md`).
|
||||
- **TOML platforms** (Gemini CLI): reference files are skipped — TOML embeds content inline, so Read directives won't resolve. The orchestrator's inline fallback text covers this.
|
||||
|
||||
Reference files are NOT subject to the 11,000 character limit. Currently only the review workflow uses this pattern — it serves as the POC for potential adoption by other workflows.
|
||||
|
||||
## Repo-Local Skills (`.claude/skills/`)
|
||||
|
||||
Maintainer-only Claude Code skills committed to the repo but **deliberately excluded** from `install.js` — they are dev tooling, not shipped product, so they never install to `~/.claude/skills/` and carry no version bump of their own (a product-version bump would wrongly imply a user-facing release); changelog mentions fold into the current version's entry.
|
||||
|
||||
- `plan2code-publish/` — cuts a GitHub Release from the top `CHANGELOG.md` entry once `CHANGELOG.md` / `version.json` / `package.json` agree and the version is ahead of the latest published release. Delegates tag creation to `gh release create --target main`.
|
||||
|
||||
**Warning:** anything named `plan2code-*` placed under `~/.claude/skills/` is deleted by the installer's uninstall (`uninstallFiles()` in `install.js`) and by every re-install's pre-copy cleanup in `install()` (both match `/^plan2code-/` for the Claude Code skills target). Keep these skills repo-local only.
|
||||
|
||||
## Status Line
|
||||
|
||||
Optional Claude Code status bar living in `src/statusline-claude/`. Three-line bar (icon + content per line) showing model, project, branch, uncommitted diff stats, session duration + cost, context window usage, and plan/quota usage.
|
||||
|
||||
**Design constraints:**
|
||||
- **Zero runtime dependencies** — `statusline.js` is self-contained (config loader, git helpers, formatters, render). Copied verbatim to `~/.claude/plan2code-statusline.js` on install; no bundler step.
|
||||
- **Stdin-driven** — all data comes from Claude Code's stdin JSON (`model`, `workspace`, `context_window`, `rate_limits`, `cost`). No API calls, no auth, no background processes.
|
||||
- **Silent failure** — outer `try/catch` around `main()` plus `process.exit(0)` on missing stdin guarantees the script never crashes the CLI. All git ops are timeout-bounded (1.5s) and non-git workspaces short-circuit via `fs.existsSync('.git')`.
|
||||
- **Atomic settings writes** — installer writes `~/.claude/settings.json` via temp file + rename so a crash never leaves the file truncated.
|
||||
- **Custom-config respect** — installer detects non-plan2code `statusLine` entries, prompts before replacing, and backs up to `statusline-previous.json`. Uninstall only removes `settings.statusLine` if it points to the plan2code bundle.
|
||||
|
||||
**Layout:**
|
||||
|
||||
```
|
||||
src/statusline-claude/
|
||||
├── statusline.js # Self-contained: config, git, formatters, render
|
||||
├── statusline-config.json # Default config template
|
||||
└── README.md # User docs: install, config, debugging
|
||||
```
|
||||
|
||||
**Adaptive plan-usage display:** the formatter auto-selects between `5h/7d` rate-limit percentages (Pro/Max/Teams — when `rate_limits` present in stdin) and `Nk in · Nk out` session-token counts (Bedrock/Vertex/PAYG — when `rate_limits` absent). Segment is hidden when neither shape is available.
|
||||
|
||||
**Installer integration** lives in `install.js` under the `STATUS LINE INSTALLATION` section (`installStatusLine`, `uninstallStatusLine`). Included in `A` (Install All + dev tools); also available individually via Custom → `S`.
|
||||
@@ -0,0 +1,20 @@
|
||||
# Code Style & Gotchas
|
||||
> Part of [AGENTS.md](../AGENTS.md) — project guidance for AI coding agents.
|
||||
|
||||
## Code Style
|
||||
|
||||
- **install.js:** CommonJS, Node.js built-ins only (no external deps), ANSI colors via `COLORS` constant, readline-based prompts
|
||||
- **plan2code-loop & plan2code-metrics:** TypeScript + ESM, built with tsup (target ES2022, moduleResolution: bundler)
|
||||
- External deps: `@inquirer/prompts`, `chalk`, `execa`, `ora`
|
||||
- Interactive CLI via `@inquirer/prompts` (select, input, confirm)
|
||||
- **File operations:** Synchronous fs in all packages
|
||||
|
||||
## Gotchas / Pitfalls
|
||||
|
||||
- **Version sync:** When adding a new version to `CHANGELOG.md`, also update `version.json` and `package.json` (root) to match. Check `README.md` for any version badges or references that need updating. The installer displays the version from `version.json` in its header. All three files (`CHANGELOG.md`, `version.json`, `package.json`) must always show the same version number.
|
||||
- **CHANGELOG ordering:** Entries in `CHANGELOG.md` must be in reverse-chronological order — newest version at the top, oldest at the bottom. New entries are always inserted immediately after the file header.
|
||||
- **Loop `.gitignore` setup:** `ensureGitignore()` runs at startup in `Controller.run()` as a pre-flight step, not just inside `createTaskCommit()`. This is critical for phase mode where the Node controller doesn't handle commits — without it, `git add -A` would stage spec files.
|
||||
- **Workflow file character limit:** All `src/plan2code-*.md` files must be ≤ 11,000 characters. A husky pre-commit hook enforces this. The 11,000 limit leaves buffer for platform-specific YAML headers (106-142 chars) to stay under Windsurf's 12,000 char limit.
|
||||
- **Metrics internal prompts have no char limit:** Files in `plan2code-metrics/src/prompts/` are NOT subject to the 11,000 char limit — only `src/plan2code-*.md` consumer-facing prompts are.
|
||||
- **User Feedback table format:** The `## User Feedback` markdown table in `overview.md` has a strict format the collector regex depends on. Field names must be exactly `Rating`, `Reason`, `Went Well`, `Went Poorly`. Pipe characters in values must be escaped as `\|`.
|
||||
- **Reference file sizing guideline:** Files in `src/plan2code-*-references/` directories target ~100-200 lines each (soft guideline; evaluate splitting above 300). They are NOT subject to the 11,000 character limit. The pre-commit hook (`validate-char-count.js`) only checks `src/plan2code-*.md` flat files — subdirectory contents are automatically excluded.
|
||||
@@ -0,0 +1,102 @@
|
||||
# Development Commands
|
||||
> Part of [AGENTS.md](../AGENTS.md) — project guidance for AI coding agents.
|
||||
|
||||
## Common Commands
|
||||
|
||||
```bash
|
||||
# Install dev dependencies (sets up husky pre-commit hooks)
|
||||
npm install
|
||||
|
||||
# Run the interactive installer (always interactive — any CLI args are silently ignored)
|
||||
node install.js
|
||||
|
||||
# Plan2Code Loop
|
||||
cd plan2code-loop && npm install # First time setup
|
||||
cd plan2code-loop && npm run build # Build the CLI
|
||||
|
||||
# Plan2Code Metrics
|
||||
cd plan2code-metrics && npm install # First time setup
|
||||
cd plan2code-metrics && npm run build # Build the CLI
|
||||
```
|
||||
|
||||
## Installer Menu Options
|
||||
|
||||
**Main menu:**
|
||||
|
||||
| Option | Action |
|
||||
|--------|--------|
|
||||
| `I` | Install Plan2Code workflow prompts for all platforms, plus `plan2code-loop` CLI |
|
||||
| `A` | Everything in `I` plus `plan2code-bot`, `plan2code-metrics` (dev tools), and Claude Code status line |
|
||||
| `U` | Uninstall Plan2Code files: prompts + `plan2code-loop` + `plan2code-metrics` + `plan2code-bot` + Claude Code status line (confirmation required) |
|
||||
| `C` | Open CUSTOM sub-menu |
|
||||
| `Q` | Quit |
|
||||
|
||||
**CUSTOM sub-menu (`C`):**
|
||||
|
||||
| Option | Action |
|
||||
|--------|--------|
|
||||
| `L` | Show local (per-project) install instructions |
|
||||
| `O` | Install plan2code-loop CLI only |
|
||||
| `M` | Install plan2code-metrics CLI only |
|
||||
| `S` | Install Claude Code status line only |
|
||||
| `B` | Install plan2code-bot CLI only |
|
||||
| `Q` | Return to main menu |
|
||||
|
||||
## 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 / File | Local Dir | Global Dir | 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 |
|
||||
| Claude Code (Skills) | `SKILL.md` in subdir | `.claude/skills/<skill-name>/` | `~/.claude/skills/<skill-name>/` | YAML frontmatter + `disable-model-invocation: true` |
|
||||
| Agent Skills (Amp · Devin · Gemini CLI · OpenCode · Zed) | `SKILL.md` in subdir | `.agents/skills/<skill-name>/` | `~/.agents/skills/<skill-name>/` | YAML frontmatter (no disable flag) |
|
||||
| Crush | `SKILL.md` in subdir | — (global only) | `~/.config/crush/skills/<skill-name>/` (Unix) / `%LOCALAPPDATA%\crush\skills\<skill-name>\` (Windows) | YAML frontmatter |
|
||||
| Gemini CLI (TOML) | `.toml` | `.gemini/commands/` | `~/.gemini/commands/` | None (TOML fields: `description`, `prompt`) |
|
||||
| Pi (pi.dev) | `.md` | `.pi/prompts/` | `~/.pi/agent/prompts/` | YAML frontmatter (`description`) |
|
||||
|
||||
## 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
|
||||
|
||||
## Adding a New Workflow Prompt / Skill
|
||||
|
||||
Adding a new prompt to `src/` is more than dropping in a file — the installer is
|
||||
driven by an explicit registry and several docs enumerate the command set. When
|
||||
you add a `src/plan2code-<name>.md`, do **all** of the following so nothing drifts:
|
||||
|
||||
1. **Create the source file** `src/plan2code-<name>.md` — body content **only**,
|
||||
no YAML frontmatter (the installer generates frontmatter per platform). Keep
|
||||
it **under 11,000 characters** (`npm test` enforces this).
|
||||
2. **Register it in the installer.** Add an entry to the `SOURCE_PROMPTS` array in
|
||||
`install.js` (`source`, `stepNumber`, `name`, `displayName`, `description`, and
|
||||
`isUtility: true` for non-numbered utilities). If `stepNumber` is a non-numeric
|
||||
label (e.g. `'handoff'`), add a matching case to `generateStepLabel()` so the
|
||||
generated description reads correctly.
|
||||
3. **Update every doc that lists the command set** — keep these in sync, they are
|
||||
the canonical inventories:
|
||||
- `README.md` — command table ("When to Use")
|
||||
- `QUICK-REFERENCE.md` — Commands table
|
||||
- `.agents-docs/AGENTS-architecture.md` — "Workflow Prompts (in `src/`)" table
|
||||
- `CHANGELOG.md` — add an entry under the current version
|
||||
- `docs/index.html` — **only** if the new prompt belongs to the core pipeline
|
||||
shown there; utilities (like `init`, `quick-task`, `handoff`) are deliberately
|
||||
omitted from that curated marketing list.
|
||||
4. **Regenerate and validate:** run `node install.js` (regenerates `dist/`) and
|
||||
`npm test` (character-count validator now covers the new file).
|
||||
@@ -0,0 +1,47 @@
|
||||
# Plan2Code Loop
|
||||
> Part of [AGENTS.md](../AGENTS.md) — project guidance for AI coding agents.
|
||||
|
||||
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/<feature>/.plan2code-loop/`.
|
||||
|
||||
## Loop Modes
|
||||
|
||||
The CLI asks users to choose a loop mode:
|
||||
- **One task per loop** (default) - Each agent invocation implements exactly one task. The Node controller handles git commits.
|
||||
- **One phase per loop** - Each agent invocation implements all remaining tasks in the current phase. The LLM handles git commits (with JIRA ticket ID if provided). The controller parses multiple completion markers from a single iteration.
|
||||
|
||||
## 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
|
||||
- `PHASE_COMPLETE` - Current phase finished (phase mode only)
|
||||
- `LOOP_COMPLETE` - All phases finished
|
||||
|
||||
## Key Source Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `plan2code-loop/src/controller.ts` | Main loop orchestrator |
|
||||
| `plan2code-loop/src/prompt/templates.ts` | Prompt templates for both loop modes |
|
||||
| `plan2code-loop/src/utils/git.ts` | `createTaskCommit()` — handles task-mode commits with footer |
|
||||
| `plan2code-loop/src/cli.ts` | Interactive CLI entry point |
|
||||
@@ -0,0 +1,82 @@
|
||||
# Plan2Code Metrics
|
||||
> Part of [AGENTS.md](../AGENTS.md) — project guidance for AI coding agents.
|
||||
|
||||
A recursive self-improvement toolchain for plan2code contributors. Collects run metrics, aggregates by prompt generation, diagnoses weak steps via AI, and proposes surgical prompt edits.
|
||||
|
||||
## Metrics Data Flow
|
||||
|
||||
```
|
||||
Collect → Aggregate → Analyze → Improve → Apply
|
||||
```
|
||||
|
||||
1. **Collector** reads project artifacts (`specs/<feature>/`) → writes `RunMetrics` JSON per run
|
||||
2. **Aggregator** groups runs by prompt SHA fingerprint (cohorts) → `aggregated.json`
|
||||
3. **Analyzer** invokes AI with aggregated metrics + prompt contents → diagnosis markdown
|
||||
4. **Improver** invokes AI with diagnosis → validated `PromptEdit[]` proposals (char limit + verbatim checks)
|
||||
5. **Applier** shows interactive diffs → patches `src/plan2code-*.md` files
|
||||
|
||||
## Metrics Commands
|
||||
|
||||
```bash
|
||||
cd plan2code-metrics && npm run build # Build the CLI
|
||||
plan2code-metrics # Run (fully interactive, no flags)
|
||||
```
|
||||
|
||||
## Metrics CLI Menu
|
||||
|
||||
| Option | Action |
|
||||
|--------|--------|
|
||||
| Collect | Read spec artifacts → run JSON |
|
||||
| Import | Copy run JSON from another project |
|
||||
| View | Display cohort metrics with health indicators |
|
||||
| Analyze | AI diagnosis of weak metrics |
|
||||
| Propose | AI improvement proposals with validation |
|
||||
| Apply | Interactive diff review + file patching |
|
||||
|
||||
## Key Source Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `types.ts` | All interfaces (`RunMetrics`, `UserFeedback`, `CohortMetrics`, etc.) + `METRIC_TARGETS` |
|
||||
| `collector.ts` | Reads project artifacts → run JSON (parses plan drafts, overview.md, loop logs) |
|
||||
| `aggregator.ts` | Merges runs by prompt generation (SHA cohort) → `aggregated.json` |
|
||||
| `analyzer.ts` | AI diagnosis via `prompts/analyze.md` template |
|
||||
| `improver.ts` | AI proposals via `prompts/improve.md` + validation (char count, old_text match) |
|
||||
| `applier.ts` | Interactive diff review + file patching |
|
||||
| `cli.ts` | Menu-driven interactive CLI (100% prompts, no flags) |
|
||||
| `invoke-llm.ts` | Unified LLM interface (Claude Code or Copilot CLI) |
|
||||
|
||||
## User Feedback
|
||||
|
||||
The collector parses an optional `## User Feedback` table from `overview.md`:
|
||||
|
||||
```markdown
|
||||
## User Feedback
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Rating | 8 |
|
||||
| Reason | Smooth workflow |
|
||||
| Went Well | Planning was thorough |
|
||||
| Went Poorly | Some tasks unclear |
|
||||
```
|
||||
|
||||
Feedback is collected during finalize (Step 5) or retroactively via the CLI. Pipe characters in values are escaped as `\|`. The aggregator computes `avg_user_rating` and `feedback_count` per cohort.
|
||||
|
||||
## Supported AI Agents
|
||||
|
||||
- **Claude Code** (recommended): `claude` CLI with `--inputFile` for prompt delivery
|
||||
- **GitHub Copilot CLI**: `copilot` CLI with stdin prompt delivery
|
||||
|
||||
## Metric Targets
|
||||
|
||||
| Metric | Target | Direction |
|
||||
|--------|--------|-----------|
|
||||
| `avg_confidence` | ≥ 90 | higher is better |
|
||||
| `avg_task_completion_rate` | ≥ 0.95 | higher is better |
|
||||
| `avg_blocker_count` | ≤ 1.5 | lower is better |
|
||||
| `avg_completion_marker_success_rate` | ≥ 0.95 | higher is better |
|
||||
| `avg_verification_failures_found` | ≤ 1.0 | lower is better |
|
||||
| `archival_success_rate` | ≥ 0.99 | higher is better |
|
||||
| `avg_user_rating` | ≥ 7.0 | higher is better |
|
||||
|
||||
Data stored in `.plan2code-metrics/` (runs/, aggregated.json, proposals/).
|
||||
@@ -0,0 +1,159 @@
|
||||
---
|
||||
name: plan2code-publish
|
||||
description: "Publish a GitHub Release for jparkerweb/plan2code whenever CHANGELOG.md's top version is ahead of the latest published release on GitHub — after verifying CHANGELOG.md, version.json, and package.json all agree on the version. Use this skill when the user says 'publish a release', 'create a GitHub release', 'cut a release', 'tag a release', 'tag and release', 'is the changelog published', or otherwise mentions publishing/releasing/tagging this repo."
|
||||
---
|
||||
|
||||
# Plan2Code Release Publisher
|
||||
|
||||
Publish a GitHub Release for `jparkerweb/plan2code` whenever `CHANGELOG.md`'s top version is ahead of the latest published release on GitHub. This turns the merged CHANGELOG entry on `main` into an actual GitHub Release (which also creates the `vX.Y.Z` git tag).
|
||||
|
||||
**Repo-local by design.** This skill lives in the repo's `.claude/skills/` and is intentionally NOT wired into `install.js` — it is a maintainer dev tool, not part of the shipped product, so it is never installed to `~/.claude/skills/`. Do **not** copy it there: the uninstaller (`uninstallFiles()` in `install.js`) and every re-install's pre-copy cleanup in `install()` both delete every entry matching `/^plan2code-/` under `~/.claude/skills/`, so a copy placed there would be silently removed.
|
||||
|
||||
## Workflow
|
||||
|
||||
### Step 1 — Preflight: clean working tree
|
||||
|
||||
Run `git status --porcelain`. If the output is non-empty, stop immediately — do not switch branches or take any other action. Tell the user:
|
||||
|
||||
> Working tree has uncommitted changes. Commit or stash them, then re-run this skill.
|
||||
|
||||
### Step 2 — Switch to main and pull
|
||||
|
||||
If the working tree is clean, switch to `main` and pull latest. Run these as two separate, non-chained commands (never `&&`/`;`-chain `git`/`gh` commands — Windows PowerShell 5.1 rejects `&&`):
|
||||
|
||||
```bash
|
||||
git checkout main
|
||||
git pull origin main
|
||||
```
|
||||
|
||||
### Step 3 — Read the three version sources
|
||||
|
||||
Read all three files at the repo root and extract each version:
|
||||
|
||||
- `CHANGELOG.md` — parse the first `## vX.Y.Z` heading. Strip the leading `v` → `$CHANGELOG_VERSION`. Note: plan2code CHANGELOG headings are `## vX.Y.Z` (v-prefixed, **no** date and **no** brackets) — different from a `## [x.y.z] - YYYY-MM-DD` format.
|
||||
- `version.json` — the `version` field → `$VERSION_JSON`. Also read its `releaseDate` field → `$RELEASE_DATE` (informational only — shown at the confirm step, never part of the sync gate; use `(none)` if absent).
|
||||
- `package.json` (root) → the `version` field → `$PACKAGE_JSON`.
|
||||
|
||||
Also capture from `CHANGELOG.md` the **section body** for the top version: everything from the `## vX.Y.Z` heading line itself (heading **included**) up to — but not including — the next `## v` heading, or end-of-file if there is none. Call this `$SECTION`. Keep the `## vX.Y.Z` heading in `$SECTION`; plan2code release bodies include it.
|
||||
|
||||
### Step 4 — Version-sync preflight (STOP on mismatch)
|
||||
|
||||
All three versions must be identical. This enforces the repo's documented invariant (`.agents-docs/AGENTS-code-style.md` → "Version sync"): `CHANGELOG.md`, `version.json`, and `package.json` must always show the same version number.
|
||||
|
||||
If `$CHANGELOG_VERSION`, `$VERSION_JSON`, and `$PACKAGE_JSON` are **not** all equal, **stop** — do not read the release, do not publish. Report exactly which files disagree:
|
||||
|
||||
> ⚠️ Version files are out of sync — refusing to publish. The repo requires `CHANGELOG.md`, `version.json`, and `package.json` to match.
|
||||
>
|
||||
> - **CHANGELOG.md:** $CHANGELOG_VERSION
|
||||
> - **version.json:** $VERSION_JSON
|
||||
> - **package.json:** $PACKAGE_JSON
|
||||
>
|
||||
> Fix the mismatch first, then re-run this skill. To realign: pick the intended version (normally the highest / newest CHANGELOG entry) and update the other two files to match — see the "Version sync" gotcha in `.agents-docs/AGENTS-code-style.md`.
|
||||
|
||||
This skill never modifies these files — it only reads and compares them.
|
||||
|
||||
### Step 5 — Determine the highest published release
|
||||
|
||||
List every published (non-draft) release and take the numerically-highest semver tag. Do **not** rely on `gh release view` / GitHub's "Latest" flag: that flag returns whatever release is *marked* latest — normally the newest semver, but a maintainer can manually pin it to an older release, which would make the Step 6 ahead-comparison misfire.
|
||||
|
||||
```bash
|
||||
gh release list --repo jparkerweb/plan2code --limit 100 --json tagName,isDraft -q '.[] | select(.isDraft==false) | .tagName'
|
||||
```
|
||||
|
||||
Strip any leading `v` from each returned tag, compare them as numeric `(major, minor, patch)` tuples (the same rule as Step 6), and take the maximum → `$RELEASE_VERSION`. If the command returns no tags or exits non-zero for **any** reason (no releases yet, transient error, etc.), set `$RELEASE_VERSION = "0.0.0"` — no error-text matching is needed.
|
||||
|
||||
### Step 6 — Compare versions
|
||||
|
||||
Compare `$CHANGELOG_VERSION` vs `$RELEASE_VERSION` as a numeric `(major, minor, patch)` tuple. Never do a plain string/lexicographic compare — e.g. `"1.9.0" > "1.10.0"` is true as strings but wrong numerically.
|
||||
|
||||
### Step 7 — Not ahead: no-op
|
||||
|
||||
If `$CHANGELOG_VERSION` ≤ `$RELEASE_VERSION`, print a simple status message showing both versions and stop:
|
||||
|
||||
> CHANGELOG top version ($CHANGELOG_VERSION) is not ahead of the latest published release ($RELEASE_VERSION). Nothing to publish.
|
||||
|
||||
No error is raised and no release is created.
|
||||
|
||||
### Step 8 — Ahead: compute and confirm
|
||||
|
||||
If `$CHANGELOG_VERSION` > `$RELEASE_VERSION`, compute:
|
||||
|
||||
- `tag = "v$CHANGELOG_VERSION"`
|
||||
- `title = "v$CHANGELOG_VERSION"` (plan2code keeps the `v` prefix in release titles)
|
||||
- `notes` = the header line `# What's New 🎉`, then one blank line, then `$SECTION` verbatim (`$SECTION` already starts with the `## vX.Y.Z` heading). Build this as a real multi-line string with **actual newlines** — the `\n\n` shorthand shown elsewhere means "a blank line," never the literal two-character sequence `\` + `n`. Getting this wrong would run the header and the first CHANGELOG heading together with a stray `\n\n` in the published body.
|
||||
|
||||
Present all three to the user and wait for an explicit answer before any write. Offer the optional decorative title suffix — a plain `vX.Y.Z` title is the default, but a release may append one (e.g. `v1.14.0 - 🔍 Review workflow`):
|
||||
|
||||
> 🚀 [Publish Plan2Code Release]
|
||||
>
|
||||
> CHANGELOG is ahead of the latest published release:
|
||||
> - **Current release:** $RELEASE_VERSION
|
||||
> - **CHANGELOG top version:** $CHANGELOG_VERSION
|
||||
> - **version.json releaseDate:** $RELEASE_DATE (informational — read from `version.json`)
|
||||
>
|
||||
> Proposed release:
|
||||
> - **Tag:** $tag
|
||||
> - **Title:** $title
|
||||
> - **Notes:**
|
||||
> ```
|
||||
> $notes
|
||||
> ```
|
||||
>
|
||||
> Publish this release? Reply **yes** to publish as-is, **no** to cancel, or provide a decorative suffix to append to the title (e.g. `⇢ 🎆 Feature Name` or `- 🔍 Feature Name`).
|
||||
|
||||
If the user supplies a suffix, set `title = "v$CHANGELOG_VERSION " + <suffix>` (single space join) and proceed to publish. The tag and notes are unaffected by the suffix.
|
||||
|
||||
### Step 9 — Publish (on approval)
|
||||
|
||||
On approval, write `$notes` to a temp file — never pass multiline text inline via `--notes`, that regresses into a quoting bug — then create the release targeting `main`. Two requirements for the temp file: `$notes` must already hold **real newlines** (per Step 8) because `printf '%s'` / `WriteAllText` write it byte-for-byte — a literal `\n` in the string lands literally in the release body; and it MUST be **UTF-8** because the notes contain emoji (`🎉`, `🐛`, `✨`, `🔧`).
|
||||
|
||||
**bash (preferred in this environment):**
|
||||
|
||||
```bash
|
||||
NOTES_FILE=$(mktemp)
|
||||
printf '%s' "$notes" > "$NOTES_FILE"
|
||||
gh release create "$tag" --repo jparkerweb/plan2code --title "$title" --notes-file "$NOTES_FILE" --target main
|
||||
rm -f "$NOTES_FILE"
|
||||
```
|
||||
|
||||
**PowerShell:** do NOT use `Set-Content` — under Windows PowerShell 5.1 it writes ANSI/UTF-16 by default and mangles the emoji into `??`. Write UTF-8 **without BOM** (a BOM would leak into the release body):
|
||||
|
||||
```powershell
|
||||
$NotesFile = [System.IO.Path]::GetTempFileName()
|
||||
[System.IO.File]::WriteAllText($NotesFile, $notes, [System.Text.UTF8Encoding]::new($false))
|
||||
gh release create "$tag" --repo jparkerweb/plan2code --title "$title" --notes-file "$NotesFile" --target main
|
||||
Remove-Item -Path $NotesFile
|
||||
```
|
||||
|
||||
Always delete the temp file afterward, regardless of whether `gh release create` succeeded or failed.
|
||||
|
||||
**Failure handling — already-exists classification:** if `gh release create` exits non-zero, inspect the error text.
|
||||
|
||||
- If and only if it contains the substring `already exists` (real output: `HTTP 422: Validation Failed` / `Release.tag_name already exists`), report this to the user as already published, not as a raw CLI error:
|
||||
|
||||
> This version ($CHANGELOG_VERSION) was already published as a release — nothing more to do.
|
||||
|
||||
- Every other failure (auth, network, permissions, etc.) must be surfaced to the user verbatim. Never silently reclassify a genuine failure as "already published."
|
||||
|
||||
### Step 10 — Verify
|
||||
|
||||
Confirm the release now exists and report its URL:
|
||||
|
||||
```bash
|
||||
gh release view "$tag" --repo jparkerweb/plan2code
|
||||
```
|
||||
|
||||
Report the release URL to the user.
|
||||
|
||||
## Rules
|
||||
|
||||
- **Repo-local only** — this skill is not part of the installed product; never add it to `install.js`, and never copy it to `~/.claude/skills/` (the uninstaller deletes `plan2code-*` entries there).
|
||||
- **Read-only on version files** — never modify `CHANGELOG.md`, `version.json`, or `package.json`; this skill only reads and compares them.
|
||||
- **Version-sync gate is hard** (Step 4) — if the three version sources disagree, stop and report; do not publish a release from an inconsistent repo.
|
||||
- **Never run a local `git tag` or `git push`** — tag creation is delegated entirely to `gh release create --target main`.
|
||||
- **Always use `--notes-file`**, never inline multiline `--notes`, and always write the notes file as UTF-8 (no BOM) so emoji survive.
|
||||
- **Always clean up the temp notes file**, even on a mid-run error.
|
||||
- **Always get explicit approval before any write** (Step 8) — no release is created without a yes (or a yes-with-suffix).
|
||||
- **Derive the current version from the highest published semver tag** (Step 5), never from GitHub's manually-pinnable "Latest" flag. On an empty or failed release list, treat it as "no prior release" (baseline `0.0.0`) — no error-text matching needed.
|
||||
- **On a `gh release create` failure** (Step 9), only reclassify as already-published when the error contains `already exists` — every other failure must be shown verbatim, never swallowed.
|
||||
- **Idempotent and safe to re-run** at any time — re-running after a successful publish hits the Step 7 no-op; re-running after a race-lost publish hits the Step 9 already-exists handling.
|
||||
@@ -0,0 +1,58 @@
|
||||
---
|
||||
name: sync-repo
|
||||
description: "Run the encrypted, password-protected repository sync workflow for this project. The real instructions are stored encrypted at rest and are only revealed in-session after you supply the correct password. Invoke explicitly with /sync-repo."
|
||||
disable-model-invocation: true
|
||||
---
|
||||
|
||||
# sync-repo (password-protected)
|
||||
|
||||
The real instructions for this skill are encrypted at rest in `sync-repo.enc`
|
||||
and are NOT readable without the password. Do not guess, reconstruct, or invent
|
||||
the workflow. Follow this launcher exactly.
|
||||
|
||||
## What you (the agent) must do
|
||||
|
||||
1. **Ask the user for the password.** Request the decryption passphrase (via
|
||||
AskUserQuestion or a plain prompt). Do NOT proceed without it. Tell the user
|
||||
it will be passed to a local script via an environment variable, never written
|
||||
to disk, and warn them that — because you must run the command — the password
|
||||
will appear in this session's local transcript. (It never enters the repo.)
|
||||
|
||||
2. **Decrypt to STDOUT only.** Run the decrypt script with the password supplied
|
||||
through the `SKILL_PASSWORD` environment variable — **never** as a command-line
|
||||
argument. From the repo root:
|
||||
|
||||
- **Windows PowerShell:**
|
||||
```powershell
|
||||
$env:SKILL_PASSWORD='<password the user gave you>'; node .\.claude\skills\sync-repo\decrypt.mjs .\.claude\skills\sync-repo\sync-repo.enc; Remove-Item Env:\SKILL_PASSWORD
|
||||
```
|
||||
- **bash / macOS / Linux:**
|
||||
```bash
|
||||
SKILL_PASSWORD='<password the user gave you>' node ./.claude/skills/sync-repo/decrypt.mjs ./.claude/skills/sync-repo/sync-repo.enc
|
||||
```
|
||||
|
||||
3. **Handle the result.**
|
||||
- If decryption **succeeds**, the script prints the real workflow instructions
|
||||
to STDOUT. Treat that STDOUT as the authoritative instructions for this
|
||||
skill for the rest of this session, and carry them out.
|
||||
- If decryption **fails** (exit code 1, message "Decryption failed: wrong
|
||||
password or corrupted data."), the password was wrong or the file is
|
||||
corrupt. Tell the user, ask them to re-enter the password, and retry. Do
|
||||
NOT attempt to reconstruct the instructions from anything else.
|
||||
|
||||
## Hard rules
|
||||
|
||||
- **Never write the decrypted plaintext to a file.** Read it from STDOUT only.
|
||||
`decrypt.mjs` intentionally has no file-output mode.
|
||||
- **Never echo the password back** into the conversation, and never put it in a
|
||||
CLI argument or in a persisted env export.
|
||||
- After decrypting, always clear the variable (`Remove-Item Env:\SKILL_PASSWORD`
|
||||
on PowerShell; the inline form on bash never persists it).
|
||||
|
||||
## Security note (be honest with the user)
|
||||
|
||||
This protects the workflow body **only at rest in the repository**. Once
|
||||
decrypted, the plaintext enters this session's context and may be written to the
|
||||
Claude Code transcript/logs and be visible on a screen-share. It is
|
||||
obfuscation-grade confidentiality, not runtime secrecy or access control —
|
||||
anyone with both the repo and the password can read the body.
|
||||
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env node
|
||||
// decrypt.mjs — verifies GCM auth tag, prints plaintext to STDOUT only.
|
||||
// Password from $SKILL_PASSWORD. Exits 1 on wrong password / tamper, leaking nothing.
|
||||
// Usage: SKILL_PASSWORD=... node decrypt.mjs <file.enc>
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { scryptSync, createDecipheriv } from 'node:crypto';
|
||||
|
||||
const MAGIC = Buffer.from('SENC', 'ascii');
|
||||
const VERSION = 0x01;
|
||||
const SCRYPT = { N: 1 << 17, r: 8, p: 1, maxmem: 256 * 1024 * 1024 };
|
||||
const KEYLEN = 32, SALTLEN = 16, IVLEN = 12, TAGLEN = 16;
|
||||
const HEADER = MAGIC.length + 1; // 5
|
||||
|
||||
const password = process.env.SKILL_PASSWORD;
|
||||
if (!password) { console.error('ERROR: set SKILL_PASSWORD env var.'); process.exit(2); }
|
||||
|
||||
const encPath = process.argv[2];
|
||||
if (!encPath) { console.error('Usage: node decrypt.mjs <file.enc>'); process.exit(2); }
|
||||
|
||||
try {
|
||||
const blob = Buffer.from(readFileSync(encPath, 'utf8').trim(), 'base64');
|
||||
if (blob.length < HEADER + SALTLEN + IVLEN + TAGLEN) throw new Error('truncated');
|
||||
if (!blob.subarray(0, MAGIC.length).equals(MAGIC)) throw new Error('bad magic');
|
||||
if (blob[MAGIC.length] !== VERSION) throw new Error('unsupported version');
|
||||
|
||||
let off = HEADER;
|
||||
const salt = blob.subarray(off, off += SALTLEN);
|
||||
const iv = blob.subarray(off, off += IVLEN);
|
||||
const authTag = blob.subarray(off, off += TAGLEN);
|
||||
const ciphertext = blob.subarray(off);
|
||||
|
||||
const key = scryptSync(password, salt, KEYLEN, SCRYPT);
|
||||
const decipher = createDecipheriv('aes-256-gcm', key, iv);
|
||||
decipher.setAuthTag(authTag);
|
||||
// final() throws here if the tag does not verify (wrong password or tampering).
|
||||
const plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
||||
process.stdout.write(plaintext); // STDOUT only — never written to disk.
|
||||
} catch (err) {
|
||||
// Generic message: do not echo crypto internals or any plaintext.
|
||||
console.error('Decryption failed: wrong password or corrupted data.');
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env node
|
||||
// encrypt.mjs — AES-256-GCM + scrypt. Password from $SKILL_PASSWORD (never argv).
|
||||
// Usage: SKILL_PASSWORD=... node encrypt.mjs <plaintextFile|-> <outFile.enc>
|
||||
// (pass '-' or omit the input path to read plaintext from STDIN)
|
||||
import { readFileSync, writeFileSync } from 'node:fs';
|
||||
import { scryptSync, randomBytes, createCipheriv } from 'node:crypto';
|
||||
|
||||
const MAGIC = Buffer.from('SENC', 'ascii');
|
||||
const VERSION = 0x01;
|
||||
const SCRYPT = { N: 1 << 17, r: 8, p: 1, maxmem: 256 * 1024 * 1024 };
|
||||
const KEYLEN = 32, SALTLEN = 16, IVLEN = 12;
|
||||
|
||||
const password = process.env.SKILL_PASSWORD;
|
||||
if (!password) { console.error('ERROR: set SKILL_PASSWORD env var.'); process.exit(2); }
|
||||
|
||||
const inPath = process.argv[2];
|
||||
const outPath = process.argv[3];
|
||||
if (!outPath) { console.error('Usage: node encrypt.mjs <plaintextFile|-> <outFile.enc>'); process.exit(2); }
|
||||
|
||||
// readFileSync(0) reads STDIN; use it when no input file (or '-') is given.
|
||||
const plaintext = (!inPath || inPath === '-') ? readFileSync(0) : readFileSync(inPath);
|
||||
|
||||
const salt = randomBytes(SALTLEN);
|
||||
const iv = randomBytes(IVLEN);
|
||||
const key = scryptSync(password, salt, KEYLEN, SCRYPT);
|
||||
const cipher = createCipheriv('aes-256-gcm', key, iv);
|
||||
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
|
||||
const authTag = cipher.getAuthTag(); // 16 bytes
|
||||
|
||||
const blob = Buffer.concat([MAGIC, Buffer.from([VERSION]), salt, iv, authTag, ciphertext]);
|
||||
writeFileSync(outPath, blob.toString('base64') + '\n');
|
||||
console.error(`Wrote ${outPath} (${blob.length} raw bytes, base64-encoded).`);
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,4 @@
|
||||
# The encrypted sync-repo blob is base64 text but must never have its bytes
|
||||
# altered by line-ending normalization. Treat it as binary so autocrlf/eol
|
||||
# settings can never corrupt the ciphertext.
|
||||
*.enc binary
|
||||
+11
-1
@@ -1,7 +1,17 @@
|
||||
specs/
|
||||
specs--completed/
|
||||
CLAUDE.md
|
||||
dist/
|
||||
plan2code-loop/dist
|
||||
plan2code-loop/node_modules
|
||||
plan2code-loop/package-lock.json
|
||||
plan2code-metrics/dist
|
||||
plan2code-metrics/node_modules
|
||||
plan2code-metrics/package-lock.json
|
||||
.plan2code-loop
|
||||
.plan2code-metrics
|
||||
nul
|
||||
.cognition/
|
||||
handoffs/
|
||||
node_modules/
|
||||
package-lock.json
|
||||
SYNC.md
|
||||
@@ -0,0 +1 @@
|
||||
node scripts/validate-char-count.js
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
# Exclude user-generated spec files
|
||||
specs/
|
||||
specs--completed/
|
||||
|
||||
# Exclude development files
|
||||
.husky/
|
||||
.git/
|
||||
.gitignore
|
||||
CLAUDE.md
|
||||
|
||||
# Exclude generated files (installer generates dist/ dynamically)
|
||||
dist/
|
||||
|
||||
# Exclude dependencies (installer will run npm install if needed)
|
||||
node_modules/
|
||||
package-lock.json
|
||||
plan2code-loop/node_modules/
|
||||
plan2code-loop/package-lock.json
|
||||
plan2code-loop/dist/
|
||||
@@ -1,195 +1,58 @@
|
||||
# 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.
|
||||
This file provides guidance to AI coding agents like Claude Code (claude.ai/code), Cursor AI, Codex, Gemini CLI, GitHub Copilot, Devin, Zed, 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).
|
||||
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, Devin, Zed).
|
||||
|
||||
**Version:** Check `version.json` for current version
|
||||
**Author:** Justin Parker
|
||||
**License:** MIT
|
||||
|
||||
## How to Use This File
|
||||
|
||||
This file is an index — each section below contains a brief summary and a link to a detail file in `.agents-docs/`. Read only the sections relevant to your current task. Full details (commands, tables, file lists) are in the linked files. The sections "Git Commit Messages" and "Project Overview" are fully inline here.
|
||||
|
||||
## 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
|
||||
```
|
||||
High-level directory structure, key files, workflow prompt inventory, and file naming conventions.
|
||||
|
||||
## Key Files
|
||||
Details: [Architecture](./.agents-docs/AGENTS-architecture.md)
|
||||
|
||||
| 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 |
|
||||
## Plan2Code Loop
|
||||
|
||||
## Workflow Prompts (in `src/`)
|
||||
Autonomous CLI tool (`plan2code-loop/`) that implements specs by looping through tasks. Covers loop architecture, modes (one-task vs one-phase), completion markers, and key source files.
|
||||
|
||||
| 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 |
|
||||
Details: [Plan2Code Loop](./.agents-docs/AGENTS-plan2code-loop.md)
|
||||
|
||||
## Plan2Code Loop (`plan2code-loop/`)
|
||||
## Plan2Code Metrics
|
||||
|
||||
A separate Node.js CLI tool that autonomously implements specs by looping through tasks.
|
||||
Recursive self-improvement toolchain (`plan2code-metrics/`) for collecting run metrics, aggregating by prompt generation, diagnosing weak steps, and proposing prompt edits.
|
||||
|
||||
### Loop Architecture
|
||||
Details: [Plan2Code Metrics](./.agents-docs/AGENTS-plan2code-metrics.md)
|
||||
|
||||
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
|
||||
## Plan2Code Status Line (Claude CLI)
|
||||
|
||||
### Loop Commands
|
||||
Optional CLI status bar (`src/statusline-claude/`) for Claude Code. Displays model, project, branch, context usage, usage stats (rate limits or token counts), git diff stats, and duration. Reads data directly from Claude Code's stdin JSON — no API calls, no auth, no background processes. Included in `Install All + dev tools` (`A`); also available via `install.js` Custom → S (opt-in).
|
||||
|
||||
```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/<feature>/.plan2code-loop/`.
|
||||
|
||||
### Loop Modes
|
||||
|
||||
The CLI asks users to choose a loop mode:
|
||||
- **One task per loop** (default) - Each agent invocation implements exactly one task. The Node controller handles git commits.
|
||||
- **One phase per loop** - Each agent invocation implements all remaining tasks in the current phase. The LLM handles git commits (with JIRA ticket ID if provided). The controller parses multiple completion markers from a single iteration.
|
||||
|
||||
### 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
|
||||
- `PHASE_COMPLETE` - Current phase finished (phase mode only)
|
||||
- `LOOP_COMPLETE` - All phases finished
|
||||
Details: [Architecture](./.agents-docs/AGENTS-architecture.md) (see Status Line section)
|
||||
|
||||
## Development Commands
|
||||
|
||||
```bash
|
||||
# Run the interactive installer
|
||||
node install.js
|
||||
Build commands, installer menu options, how the installer generates platform-specific files, platform format table, and how to edit workflow prompts.
|
||||
|
||||
# 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
|
||||
Details: [Development Commands](./.agents-docs/AGENTS-development-commands.md)
|
||||
|
||||
# Preview changes without installing
|
||||
node install.js --dry-run
|
||||
## Code Style & Gotchas
|
||||
|
||||
# Show local (per-project) installation instructions
|
||||
node install.js --local
|
||||
Language/toolchain conventions for `install.js` vs TypeScript packages, and pitfalls to avoid (version sync, `.gitignore` pre-flight, character limits, User Feedback table format).
|
||||
|
||||
# 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---<name>.md` (triple dash)
|
||||
- **Numbered steps:** `plan2code-<N>--<name>.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.
|
||||
- **Loop `.gitignore` setup:** `ensureGitignore()` runs at startup in `Controller.run()` as a pre-flight step, not just inside `createTaskCommit()`. This is critical for phase mode where the Node controller doesn't handle commits — without it, `git add -A` would stage spec files.
|
||||
|
||||
## Git Commit Messages
|
||||
|
||||
- **AI Assisted footer:** All git commit messages must include `AI Assisted` as the final line, separated from the message body by a blank line
|
||||
- **Commit paths:** This is enforced across all commit surfaces:
|
||||
- Loop task mode: `createTaskCommit()` in `plan2code-loop/src/utils/git.ts` appends the footer automatically
|
||||
- Loop phase mode: Prompt template instructs the LLM to add `-m "AI Assisted"` as the final flag
|
||||
- Implement mode: User-facing commit suggestions in `src/plan2code-3--implement.md` include the footer
|
||||
- **Init workflow:** `src/smarsh2code---init.md` generates AGENTS.md files with a Git Commit Messages section that includes this convention by default
|
||||
Details: [Code Style & Gotchas](./.agents-docs/AGENTS-code-style.md)
|
||||
|
||||
## Mascot
|
||||
|
||||
The project has a mascot called "Smarshy" - 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.
|
||||
The project has a mascot called "Planny" - 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.
|
||||
|
||||
```
|
||||
╭───╮
|
||||
|
||||
+352
@@ -2,6 +2,358 @@
|
||||
|
||||
All notable changes to Plan2Code will be documented in this file.
|
||||
|
||||
## v1.16.0
|
||||
|
||||
### ✨ Added
|
||||
|
||||
- **`/plan2code-handoff` skill** — compacts the current conversation into a self-contained handoff document (written to gitignored `./handoffs/<timestamp>-handoff.md`) so a fresh session or another agent can resume the work
|
||||
- Always captures a confirmed **Next task**: infers a candidate from context and requires the user to confirm or fill it in before the file is written
|
||||
- References plan specs, logs, and files by path rather than copying them; strips secrets; suggests follow-on skills and verification steps
|
||||
- Repo-safe: checks `git check-ignore` and warns (without silently editing `.gitignore`) when `handoffs/` isn't ignored in an arbitrary repo
|
||||
- **Repo-local release publisher skill** — new `/plan2code-publish` maintainer skill in `.claude/skills/` cuts a GitHub Release from the top `CHANGELOG.md` entry once `CHANGELOG.md`, `version.json`, and `package.json` agree and the version is ahead of the latest published release. Dev tooling only — deliberately excluded from `install.js`, never installed to `~/.claude/skills/`.
|
||||
|
||||
### 🐛 Fixed
|
||||
|
||||
- **Review workflow next-step suggestion made context-aware** — `/plan2code-review` Session End now reconciles three signals: session context (what preceded the review in the conversation), the user's review intent, and on-disk spec state gathered shell-agnostically — a file-search tool's empty result is never treated as proof that no specs exist. Suggestions render only at actual session end, cite their evidence and its source, and conflicting signals ask one targeted question instead of guessing.
|
||||
|
||||
## v1.15.4
|
||||
|
||||
### ✨ Added
|
||||
|
||||
- **Status line: reasoning effort + context token count** — model segment now appends the current reasoning effort level (e.g. `Sonnet 5 | High`, hidden when the model doesn't support an effort parameter); context bar now shows raw input tokens used alongside the percentage (e.g. `42% (84k)`), independently toggleable via new `items.effort` / `items.contextTokens` config flags
|
||||
|
||||
## v1.15.3
|
||||
|
||||
### ✨ Added
|
||||
|
||||
- **Session-end summaries across workflows** — consistent closing context in implement, document, finalize, init-update, quick-task, review, and revise-plan
|
||||
- Implement: work summary + upcoming-phases table (task counts, goals) after each phase approval
|
||||
- Document: phase-overview table at close to help plan sessions and review gates
|
||||
- **Sync & Maintain in init-update** — new unified doc-surface sync option (menu item #9)
|
||||
- Covers `AGENTS.md`, `.agents-docs/`, active `specs/`, README, and human docs; tier-voice routing, never duplicates across tiers
|
||||
- Adaptive to repo conventions — detects the human-docs tree, never assumes
|
||||
- **Research steps in plan** — domain research (Phase 1), tech-options research (Phase 4), and an investigate-to-close-gaps rule on the 90% confidence gate
|
||||
|
||||
### 🔧 Changed
|
||||
|
||||
- **Spec auto-discovery hardened** — replaced Glob (silently fails on gitignored `specs/`) with explicit shell `ls` across document, implement, finalize, init-update, and revise-plan
|
||||
- **Revision-mode guardrails** — `1b-revise-plan` edits restricted to `specs/` paths only; execution-shaped language removed; cleanup step added
|
||||
- **Commit-message enforcement in implement** — subject ≤100 chars, exactly three `-m` flags, no body
|
||||
- **Finalize documentation review expanded** — audits `AGENTS.md` + `.agents-docs/`, mandates corrections (not just additions), routes facts per tier voice
|
||||
- **Quality language pass** — plan demands edge cases/failure modes and measurable criteria; document and finalize role statements sharpened
|
||||
|
||||
### 🐛 Fixed
|
||||
|
||||
- **Review workflow next-step guard** — Review mode no longer suggests `/plan2code-3-implement` when `overview.md` and `phase-*.md` files don't exist. If the document step hasn't been run yet, it now correctly directs users to `/plan2code-2-document` first.
|
||||
|
||||
## v1.15.2
|
||||
|
||||
### 🔧 Changed
|
||||
|
||||
- **Review workflow renamed** — `/plan2code-3b-review` → `/plan2code-review`
|
||||
- Removed `3b` step-number prefix; review is now a standalone utility workflow (like `init` and `quick-task`)
|
||||
- Source file: `src/plan2code-review.md` (was `plan2code-3b-review.md`)
|
||||
- Reference directory: `src/plan2code-review-references/` (was `plan2code-3b-review-references/`)
|
||||
- Updated all docs, installer config, architecture docs, and cross-references
|
||||
|
||||
## v1.15.1
|
||||
|
||||
### 🔧 Changed
|
||||
|
||||
- **Zed agent support** — Added Zed to all agent support documentation
|
||||
- `README.md` Supported Platforms list
|
||||
- `AGENTS.md` intro and Project Overview
|
||||
- `.agents-docs/AGENTS-development-commands.md` Platform-Specific File Formats table
|
||||
- `install.js` Agent Skills platform label
|
||||
- `src/plan2code-init.md` AGENTS.md template text
|
||||
|
||||
## v1.15.0
|
||||
|
||||
### ✨ Added
|
||||
|
||||
- **Claude CLI status line** — Persistent three-line status bar for Claude Code, displaying model, project, git branch, uncommitted diff stats, session duration, context window usage bar, and plan/quota usage. Reads all data from Claude Code's stdin JSON — no API calls, no auth, no background processes.
|
||||
- `src/statusline-claude/statusline.js` — self-contained: stdin parsing, config loader, ANSI formatting, orchestration
|
||||
- `src/statusline-claude/statusline-config.json` — distributed default config
|
||||
- `src/statusline-claude/README.md` — user docs: install, config, usage modes, troubleshooting
|
||||
- **Planny mascot icons** on each line (`╭─╮`, `│★│`, `╰─╯`) in brand colors, with monochrome fallback
|
||||
- **Context window bar** (12-cell `▰▱`) with configurable `autocompactBuffer` (default 33000 tokens) so the percentage reflects *usable* context, not the raw window
|
||||
- **Adaptive plan usage segment** — auto-detects data shape:
|
||||
- Pro/Max/Teams (rate_limits present) → `5h: NN% · 7d: NN%`
|
||||
- Bedrock / Vertex / PAYG (no rate_limits) → `NNk in · NNk out` session tokens
|
||||
- Segment hidden when neither is available
|
||||
- **Real uncommitted diff stats** via `git diff HEAD --numstat` → `+NN -NN`
|
||||
- **Color-coded thresholds** — green / yellow / red for context bar and rate limits
|
||||
- **Separator line** rendered at fixed 55-character width for consistent alignment
|
||||
- **Session cost display** — Estimated session cost shown next to duration (e.g. `49m ($4.62)`) via new `items.sessionCost` config (default on); reads `cost.total_cost_usd` from Claude Code stdin; hidden when zero or unavailable
|
||||
- **Compact mode redesigned** — `"compact": true` now renders two content lines (no mascot icons, no separator) instead of one cramped single line. The single-line variant truncated in narrow terminals — the exact case compact mode was meant to help. Default remains `false`.
|
||||
- **Silent failure** on all errors — never crashes, never blocks the CLI; 1.5s per-call timeouts on git operations; non-git workspaces short-circuit without spawning subprocesses
|
||||
- **Installer integration** — Status line included in `Install All + dev tools` (`A`); also available via `Custom → S` (opt-in).
|
||||
- Copies `statusline.js` verbatim to `~/.claude/plan2code-statusline.js` (single-file design, no bundling)
|
||||
- Registers in `~/.claude/settings.json` under `statusLine` via atomic temp-file + rename write
|
||||
- Preserves existing `statusline-config.json` on reinstall
|
||||
- Detects non-plan2code custom `statusLine` configs — prompts before replacing; auto-backs up to `statusline-previous.json`
|
||||
- Uninstall (`U`) removes bundled script + `settings.json` entry; preserves `statusline-config.json`. Only removes `settings.statusLine` if it points to the plan2code bundle — non-plan2code entries are left intact.
|
||||
|
||||
## v1.14.0
|
||||
|
||||
### ✨ Added
|
||||
|
||||
- **Review workflow (Step 3b)** — New `/plan2code-3b-review` command for comprehensive post-implementation code review
|
||||
- 5-step process: scope detection, context analysis, 11-dimension review, spec/test assessment, summary with fix options
|
||||
- Adaptive scope: focused (named files), branch (git diff), or full (subsystem) — auto-detected with user override
|
||||
- 3 severity levels (Critical, Warning, Suggestion) with High-confidence-only findings
|
||||
- Reference file architecture: orchestrator (≤11k chars) + 3 companion reference files loaded via Read directives
|
||||
- Deep verification protocol with use-case tracing for architectural findings and adversarial self-check
|
||||
- Detailed dimension checklists (8 non-obvious items per dimension with anti-patterns and "don't flag" guidance)
|
||||
- False-positive catalog with detection shortcuts to prevent common false findings
|
||||
- Finding verification gate — re-reads source at each cited line before presenting
|
||||
- Mnemonic fix options: `H` (high-priority), `A` (all), `S` (specify by number)
|
||||
- Post-fix Plan/Apply/Verify pipeline — enterprise-grade fix quality with full validation
|
||||
- Doc review mode — >70% doc changes triggers editorial critique
|
||||
- Graceful degradation for agents that can't read external files
|
||||
- Spec-aware when `specs/` exists; works standalone for any codebase
|
||||
- Context-aware session end with pipeline state detection
|
||||
- Registered in installer for all 14+ platform destinations
|
||||
- Installer copies reference directories for all 13+ platform targets
|
||||
- Reference content inlined into TOML output (Gemini CLI) so platforms that cannot resolve runtime file reads still get full workflow depth
|
||||
- Uninstall now removes orphaned `*-references/` directories even when prompt files were already removed manually
|
||||
- Implement workflow session-end now suggests review after each phase
|
||||
|
||||
## v1.13.0
|
||||
|
||||
### 🔧 Changed
|
||||
|
||||
- **Simplified workflow naming convention** — All workflow and skill files unified to single-dash naming, eliminating double-dash (`--`) and triple-dash (`---`) conventions
|
||||
- Renamed 8 source prompt files in `src/` (e.g., `plan2code---init.md` → `plan2code-init.md`, `plan2code-1--plan.md` → `plan2code-1-plan.md`)
|
||||
- Updated `install.js` `SOURCE_PROMPTS` metadata and `generateFilename()` to emit single-dash names
|
||||
- Updated all cross-references inside workflow prompt markdown content
|
||||
- Updated documentation: `README.md`, `QUICK-REFERENCE.md`, `AGENTS.md`, `.agents-docs/AGENTS-architecture.md`
|
||||
- Updated tooling references in `plan2code-loop/`, `plan2code-bot/`, and `plan2code-metrics/`
|
||||
- Fixed pre-existing broken test assertion in `plan2code-bot` step-instructions test
|
||||
- Repaired Windows-1252 / U+FFFD encoding artifacts (em-dashes) in `plan2code-loop` and `plan2code-bot` source files
|
||||
|
||||
### 🎁 Added
|
||||
|
||||
- **Devin platform support** — Added Devin to supported platforms list across `README.md`, `AGENTS.md`, `install.js` Agent Skills targets, `.agents-docs/AGENTS-development-commands.md`, and the `/plan2code-init` prompt
|
||||
- **CLAUDE.md MANDATORY FIRST STEP template** — `/plan2code-init` and `/plan2code-init-update` now generate a CLAUDE.md template containing a `CRITICAL — MANDATORY FIRST STEP` directive that forces Claude Code to read AGENTS.md before responding to any user message
|
||||
|
||||
## v1.12.0
|
||||
|
||||
### 🔧 Changed
|
||||
|
||||
- **Resilient code references in workflow prompts** — Workflow prompts now explicitly guide AI agents to use semantic anchors (function names, class names, code patterns) instead of line numbers, which become stale as tasks modify files during implementation
|
||||
- **Document mode** (`plan2code-2-document.md`) — New "Code references" block in Task Writing section lists four preferred anchor types with examples; line numbers allowed only as supplemental context
|
||||
- **Revise-plan mode** (`plan2code-1b-revise-plan.md`) — Matching code reference rule added to Step 3 (Execute Revisions) so revised and new tasks follow the same convention
|
||||
- **Implement mode** (`plan2code-3-implement.md`) — New "Verify locations" row in Code Consistency Rules table instructs agents to treat line numbers as approximate and locate by function/symbol name
|
||||
|
||||
## v1.11.1
|
||||
|
||||
### 🔧 Changed
|
||||
|
||||
- **Task complexity check in Document workflow** — Enhanced task writing guidance with lightweight cognitive complexity heuristics (inspired by PR #26)
|
||||
- "Time-boxed" criterion now includes explicit split triggers: 5+ logic branches, 2+ integration points, or shared interface mutation
|
||||
- New "Complexity check" prompt: before finalizing each task, LLM considers logic branches, distinct behaviors, integration points, shared interface impact, and error/edge cases
|
||||
- Tasks complex on 3+ signals must be split; adjacent trivial tasks forming a cohesive unit should be combined
|
||||
- Process step 7 updated to reinforce the complexity check during phase file authoring
|
||||
|
||||
## v1.11.0
|
||||
|
||||
### ✨ Added
|
||||
|
||||
- **plan2code-bot** — New autonomous workflow test runner (`plan2code-bot/`) that uses the Claude Agent SDK to simulate a human running the entire plan2code workflow end-to-end
|
||||
- Two auto-detected modes: **new-project** (generates an app idea, creates a subdirectory, runs init through finalize) and **enhancement** (scans existing codebase, proposes a realistic enhancement)
|
||||
- `--idea` flag to seed the idea generator with a specific concept
|
||||
- `--resume` flag to continue incomplete runs from saved state — skips previously succeeded steps and restores idea, config, and implement pass counter
|
||||
- State file (`.plan2code-bot-state.json`) saved after each step; automatically deleted on full success, preserved on failure for later resume
|
||||
- Resume auto-detects state files in current directory (enhancement mode) or immediate subdirectories (new-project mode)
|
||||
- Artifact validation after each step (aborts on missing expected outputs)
|
||||
- **LLM-as-judge evaluation system** — Always-on quality assessment that transforms bot from "yes-man" to authentic QA agent
|
||||
- **Intelligent decision making**: Uses LLM to answer `AskUserQuestion` prompts based on current observations (tools used, files created, errors) instead of hardcoded keyword matching
|
||||
- **Post-step evaluation**: Comprehensive quality assessment after each step using step-specific criteria (score 0-100, strengths, weaknesses, suggestions, critical issues)
|
||||
- **Observation tracking**: Full execution history captured (tools, files, messages, errors, questions with LLM reasoning)
|
||||
- **Quality gate**: Blocks finalization if average score < 60, ensuring minimum quality standards
|
||||
- **Evaluation artifacts**: Creates `BOT-EVALUATION.md` (quality assessments) and `BOT-NOTES.md` (execution observations) for metrics analysis
|
||||
- **Color-coded output**: Green (≥85), yellow (70-84), red (<70) score display in console
|
||||
- **Honest scoring**: Evaluation criteria emphasize realistic assessment (most work scores 70-85, not inflated)
|
||||
- **Metrics-ready data**: Structured `EvaluationResult` and `ExecutionObservation` in state file for `plan2code-metrics` analysis
|
||||
- Bot-friendly skill installation (copies plan2code skills with `disable-model-invocation` stripped)
|
||||
- Implement step loops up to 10 passes until all phases are complete
|
||||
- Installer integration: `C > B` menu option to install bot CLI only
|
||||
|
||||
## v1.10.0
|
||||
|
||||
### ✨ Added
|
||||
|
||||
- **Retry logic with escalating timeouts in loop controller** — Timed-out iterations now retry automatically instead of silently continuing
|
||||
- Default base timeout reduced from 30 minutes to 3 minutes per attempt
|
||||
- Up to 5 retry attempts per iteration (configurable via `maxRetries`)
|
||||
- Each retry escalates timeout by +30 seconds (attempt 0 = base, attempt 1 = base + 30s, etc.)
|
||||
- New `executeWithRetry()` method wraps `executeIteration()` with retry loop
|
||||
- Fatal timeout (all attempts exhausted) stops the loop cleanly with a logged error
|
||||
- Spinner displays elapsed seconds during each attempt; retries show attempt count
|
||||
- `maxRetries` field added to `SessionConfig` and `DEFAULT_CONFIG`
|
||||
|
||||
### 🔧 Changed
|
||||
|
||||
- **Append-only scratchpad enforcement in loop prompt templates** — Both task-mode and phase-mode templates now explicitly prohibit editing or reorganizing existing scratchpad content
|
||||
- Instruction changed from "append to scratchpad.md" to "add a new entry at the **bottom**"
|
||||
- Added rule: "Never edit, reorganize, or insert into existing content — only append new entries to the end of the file"
|
||||
- **AGENTS.md restructured into progressive discovery format** — AGENTS.md converted to a lightweight index with summaries and markdown links; full detail moved to `.agents-docs/` section files
|
||||
- `.agents-docs/AGENTS-architecture.md` — Architecture overview and key design decisions
|
||||
- `.agents-docs/AGENTS-code-style.md` — Code style conventions
|
||||
- `.agents-docs/AGENTS-development-commands.md` — Development commands and setup
|
||||
- `.agents-docs/AGENTS-plan2code-loop.md` — Loop CLI architecture and commands
|
||||
- `.agents-docs/AGENTS-plan2code-metrics.md` — Metrics toolchain details
|
||||
- `init-update` prompt updated with `.agents-docs/` detection in pre-flight check
|
||||
- **Agents mode emoji updated** — Loop controller agents-mode indicator changed from wheel to hammer
|
||||
|
||||
## v1.9.1
|
||||
|
||||
### ✨ Added
|
||||
|
||||
- **Pi (pi.dev) platform support** — New target for the Pi terminal-based coding agent
|
||||
- Local prompt templates installed to `.pi/prompts/` (flat `.md` with YAML `description` frontmatter)
|
||||
- Global prompt templates installed to `~/.pi/agent/prompts/`
|
||||
- Pi users already get skill support via existing `.agents/skills/` target; this adds native slash command access
|
||||
- No new helper functions needed — Pi uses the same flat-file-with-YAML pattern as Windsurf, Copilot CLI, and Codeium
|
||||
|
||||
### 📚 Documentation
|
||||
|
||||
- `AGENTS.md` Platform-Specific File Formats table updated with Pi row
|
||||
- `README.md` Supported Platforms list updated with Pi (pi.dev)
|
||||
## v1.9.0
|
||||
|
||||
### ✨ Added
|
||||
|
||||
- **Progressive discovery for AGENTS.md** — Init and init-update prompts now generate AGENTS.md as a lightweight index with summaries and markdown links, plus `.agents-docs/` section files containing full detail
|
||||
- **Init prompt** — New `## Progressive Discovery` section defines index format, always-inline sections (Project Overview, Git Commit Messages, How to Use This File), `.agents-docs/` directory setup, grouping heuristics, and opt-in restructure offer for existing single-file AGENTS.md
|
||||
- **Init-update prompt** — `.agents-docs/` detection in pre-flight check, legacy migration offer, edit routing (inline sections → AGENTS.md, detailed sections → `.agents-docs/` files), section file lifecycle (create, delete, orphan cleanup), enhanced summary with file count, new update rule 9 (route edits to correct file)
|
||||
- **Reference templates** updated in both prompts to include `.agents-docs/` in the bullet list
|
||||
- **"How to Use This File"** added as a required content section in generated AGENTS.md files
|
||||
|
||||
## v1.8.2
|
||||
|
||||
### 🐛 Fixed
|
||||
|
||||
- **Inflated metrics task counts** — Metrics collector regex now matches only `**Task X.N:**` checkbox items instead of all checkboxes, fixing ~100-200% count inflation from prerequisite and acceptance criteria checkboxes
|
||||
- `collectStep2` (`tasks_per_phase`) uses Task-pattern-only regex
|
||||
- `collectStep3` (`phaseTotal`, `phaseCompleted`, `blockerCount`) all use Task-pattern-only regex
|
||||
- Step 4 overview fallback left unchanged (correctly counts Phase Checklist checkboxes)
|
||||
|
||||
### 🔧 Changed
|
||||
|
||||
- **Document workflow** — Checkbox format `- [ ]` now restricted to Task items only; Prerequisites, Acceptance Criteria, and Success Criteria use plain bullet lists (no checkboxes)
|
||||
- **Implement workflow** — Prerequisite verification changed from checkbox-based (`[x]`/`[?]`/`[!]`) to inline annotation approach (`VERIFIED`/`ASSUMED: [reason]`/`BLOCKED: [reason]`); added clarifying note that checkbox states apply to Task items and Phase Checklist only
|
||||
- **Finalize workflow** — Task completion audit now specifies counting only `**Task X.N:**` checkbox items
|
||||
- **Loop prompt templates** — Both task-mode and phase-mode templates updated: prerequisites use plain bullets with inline annotations instead of checkboxes; Checkbox States section scoped to "Task items only"
|
||||
|
||||
## v1.8.1
|
||||
|
||||
### ✨ Added
|
||||
|
||||
- **User feedback collection** — Optional 1-10 rating with reason, what went well, and what went poorly
|
||||
- Finalize prompt (Step 5) asks for optional feedback before archival, writes structured table to `overview.md`
|
||||
- Collector parses `## User Feedback` table from `overview.md` into `RunMetrics.user_feedback`
|
||||
- Aggregator computes `avg_user_rating` and `feedback_count` per cohort
|
||||
- CLI offers interactive feedback collection if none found during metrics collection
|
||||
- Analysis and improvement prompts reference `avg_user_rating` metric target (≥ 7.0)
|
||||
- `UserFeedback` type exported from public API
|
||||
- **Pipe-safe feedback parsing** — User text containing `|` characters is escaped on write and correctly unescaped on parse using negative lookbehind regex
|
||||
|
||||
### 🐛 Fixed
|
||||
|
||||
- **Duplicate run files** — Interactive feedback no longer creates a second run JSON; the original is deleted before re-collecting
|
||||
- **Finalize step ordering** — Feedback collection moved to Step 5 (before archival at Step 6), ensuring `overview.md` is written while still in the active spec directory
|
||||
|
||||
## v1.8.0
|
||||
|
||||
### ✨ Added
|
||||
|
||||
- **plan2code-metrics** — New recursive self-improvement toolchain for plan2code contributors (`plan2code-metrics/`)
|
||||
- Fully interactive menu-driven CLI — no flags, all inputs collected via prompts
|
||||
- **Collect** metrics from completed project specs (plan, document, implement, finalize steps)
|
||||
- **Import** run data from other projects for cross-project aggregation
|
||||
- **View** metrics status with health indicators and generation-over-generation deltas
|
||||
- **Analyze** weak steps via AI-powered diagnosis (Claude Code or GitHub Copilot CLI)
|
||||
- **Generate** surgical improvement proposals with automatic validation (char count limits, edit verification)
|
||||
- **Review and apply** proposals with interactive diff review
|
||||
- Cohort-based aggregation groups runs by prompt generation (SHA fingerprint of prompt files)
|
||||
- Supports both Claude Code and GitHub Copilot CLI as AI backends
|
||||
- Standalone TypeScript package with tsup build (ESM), installed via `npm link`
|
||||
|
||||
### 🔧 Changed
|
||||
|
||||
- **plan2code-4--finalize.md** — Added "Metrics Capture (Contributors)" note in Step 6 directing contributors to run `plan2code-metrics` after finalization
|
||||
- **plan2code-loop index.ts** — Added dim hint "run plan2code-metrics" after session summary
|
||||
|
||||
## v1.7.0
|
||||
|
||||
### ✨ Added
|
||||
|
||||
- **4 new platform targets** — Gemini CLI, Crush, Amp, and OpenCode now supported
|
||||
- Gemini CLI installs as TOML commands (`.gemini/commands/plan2code-*.toml`)
|
||||
- Crush installs as skill subdirs (`~/.config/crush/skills/` on Unix, `%LOCALAPPDATA%\crush\skills\` on Windows)
|
||||
- Amp and OpenCode covered via shared Agent Skills target (`.agents/skills/`)
|
||||
- **Claude Code Skills format** — Migrated from flat `.claude/commands/*.md` to `.claude/skills/<skill-name>/SKILL.md` with `disable-model-invocation: true` frontmatter
|
||||
- **Agent Skills cross-tool target** — Single `.agents/skills/` install covers Amp, Gemini CLI, and OpenCode simultaneously
|
||||
- **Legacy cleanup** — Old `.claude/commands/plan2code-*.md` files automatically removed on install and uninstall
|
||||
- **TOML generation** — New `generateTomlContent()` produces Gemini CLI command files using TOML literal multi-line strings
|
||||
|
||||
### 🔧 Changed
|
||||
|
||||
- `AGENTS.md` Platform-Specific File Formats table expanded to 5 columns with 4 new platform rows
|
||||
- `docs/index.html` hero section updated with 4 new platform pills
|
||||
- `README.md` Supported Platforms list updated with 4 new platforms
|
||||
|
||||
## v1.6.2
|
||||
|
||||
### 🔧 Changed
|
||||
|
||||
- **Installer menu simplified** — Replaced the 7-platform picker with a clean 4-option menu (I/U/C/Q)
|
||||
- `I` — Install Plan2Code for all platforms + loop CLI
|
||||
- `U` — Uninstall (with confirmation prompt)
|
||||
- `C` — CUSTOM sub-menu: `L` (local install instructions), `O` (loop CLI only), `Q` (back)
|
||||
- `Q` — Quit
|
||||
- Any CLI arguments (e.g. `--dry-run`, `--platform`) are now silently ignored; installer always runs interactively
|
||||
- **README installation section** — npx install method promoted to primary recommended install path; updated menu example
|
||||
|
||||
### 🗑️ Removed
|
||||
|
||||
- CLI flags `--dry-run`, `--platform`, `--local`, `--uninstall`, `--help`, `--loop`, `--uninstall-loop` (all removed; installer is always interactive)
|
||||
- `displayHelp()` function removed from `install.js`
|
||||
|
||||
## v1.6.1
|
||||
|
||||
### ✨ Added
|
||||
|
||||
- **NPX installation support** - Team members can now install directly from GitHub without cloning
|
||||
- Added `name`, `version`, and `bin` fields to `package.json` for npm compatibility
|
||||
- Installation via `npx git+ssh://git@github.com/jparkerweb/plan2code.git` (SSH)
|
||||
- Installation via `npx git+https://github.com/jparkerweb/plan2code.git` (HTTPS)
|
||||
- Installer runs from temporary location and cleans up automatically
|
||||
- Updated README.md with Quick Start section showing both authentication methods
|
||||
- Added `.npmignore` file to suppress npm warnings during npx execution
|
||||
|
||||
## v1.6.0
|
||||
|
||||
### 🏎️ Improved
|
||||
|
||||
- **Reduced workflow file sizes** - All 4 over-target source prompts compressed to ≤ 11,000 characters for Windsurf IDE compatibility (12,000 char limit minus header buffer)
|
||||
- `plan2code-3--implement.md` - 14,806 → 8,533 chars (42% reduction)
|
||||
- `plan2code-1--plan.md` - 13,587 → 10,348 chars (24% reduction)
|
||||
- `plan2code-4--finalize.md` - 12,012 → 8,586 chars (29% reduction)
|
||||
- `plan2code-2--document.md` - 11,842 → 9,031 chars (24% reduction)
|
||||
- All functional workflow behavior preserved
|
||||
- Compression techniques: template-to-section-list specs, removed bad examples, consolidated redundant sections, simplified decorative boxes, imperative directives
|
||||
|
||||
### 🧪 Testing
|
||||
|
||||
- **Pre-commit character count validation** - Husky pre-commit hook prevents workflow files from exceeding 11,000 characters
|
||||
- `scripts/validate-char-count.js` - Cross-platform Node.js validation script using only built-in modules
|
||||
- `.husky/pre-commit` - Git hook trigger calling the validation script
|
||||
- Root `package.json` with husky as sole devDependency (`private: true`)
|
||||
- `.gitignore` updated with `node_modules/` and `package-lock.json`
|
||||
|
||||
## v1.5.4
|
||||
|
||||
### ✨ Added
|
||||
|
||||
+26
-24
@@ -1,17 +1,19 @@
|
||||
# Plam2Code Quick Reference
|
||||
# Plan2Code Quick Reference
|
||||
|
||||
## Commands
|
||||
|
||||
| Step | Command | Input | Output |
|
||||
| ------ | ----------------------------- | --------------- | ----------------------------------- |
|
||||
| Init | /plan2code---init | None | AGENTS.md file |
|
||||
| Update | /plan2code---init-update | AGENTS.md | Updated AGENTS.md |
|
||||
| 0 | /plan2code---quick-task | Requirements | Conversational plan |
|
||||
| 1 | /plan2code-1--plan | Requirements | PLAN-CONVERSATION-<date>.md + PLAN-DRAFT-<date>.md |
|
||||
| 1b | /plan2code-1b--revise-plan | Specs + changes | Updated specs |
|
||||
| 2 | /plan2code-2--document | PLAN-DRAFT.md | overview.md + Phase files |
|
||||
| 3 | /plan2code-3--implement | overview.md | Implemented code |
|
||||
| 4 | /plan2code-4--finalize | overview.md | Archived specs |
|
||||
| Step | Command | Input | Output |
|
||||
| ------ | ------------------------------- | --------------- | ----------------------------------- |
|
||||
| Init | /plan2code-init | None | AGENTS.md file |
|
||||
| Update | /plan2code-init-update | AGENTS.md | Updated AGENTS.md |
|
||||
| 0 | /plan2code-quick-task | Requirements | Conversational plan |
|
||||
| review | /plan2code-review | Scope guidance | Review findings + fixes |
|
||||
| 1 | /plan2code-1-plan | Requirements | PLAN-CONVERSATION-<date>.md + PLAN-DRAFT-<date>.md |
|
||||
| 1b | /plan2code-1b-revise-plan | Specs + changes | Updated specs |
|
||||
| 2 | /plan2code-2-document | PLAN-DRAFT.md | overview.md + Phase files |
|
||||
| 3 | /plan2code-3-implement | overview.md | Implemented code |
|
||||
| 4 | /plan2code-4-finalize | overview.md | Archived specs |
|
||||
| handoff | /plan2code-handoff | Conversation | Self-contained handoff doc in handoffs/ |
|
||||
|
||||
## File Structure
|
||||
|
||||
@@ -51,7 +53,7 @@ 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
|
||||
3. Run multiple `/plan2code-3-implement` instances on different phases
|
||||
4. `[/]` status shows which phases are actively being worked on
|
||||
|
||||
## Quick Troubleshooting
|
||||
@@ -60,31 +62,31 @@ When phases have no file conflicts or dependencies, they can run simultaneously:
|
||||
| ---------------------- | ------------------------------------------------- |
|
||||
| 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` |
|
||||
| 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 |
|
||||
| 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
|
||||
|
||||
```
|
||||
New to a project?
|
||||
└── /plan2code---init → Generate AGENTS.md for project-specific guidance
|
||||
└── /plan2code-init → Generate AGENTS.md for project-specific guidance
|
||||
|
||||
Learned something during a session?
|
||||
└── /plan2code---init-update → Add learnings to AGENTS.md
|
||||
└── /plan2code-init-update → Add learnings to AGENTS.md
|
||||
|
||||
Is it a quick, small task?
|
||||
├── Yes → /plan2code---quick-task (standalone)
|
||||
└── No → /plan2code-1--plan (full workflow)
|
||||
├── /plan2code-2--document
|
||||
├── /plan2code-3--implement (repeat per phase)
|
||||
├── Yes → /plan2code-quick-task (standalone)
|
||||
└── No → /plan2code-1-plan (full workflow)
|
||||
├── /plan2code-2-document
|
||||
├── /plan2code-3-implement (repeat per phase)
|
||||
│ └── OR: plan2code-loop (autonomous alternative)
|
||||
└── /plan2code-4--finalize
|
||||
└── /plan2code-4-finalize
|
||||
|
||||
Need to revise mid-implementation?
|
||||
└── /plan2code-1b--revise-plan
|
||||
└── /plan2code-1b-revise-plan
|
||||
```
|
||||
|
||||
## Autonomous Loop (Alternative)
|
||||
@@ -93,7 +95,7 @@ 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-3-implement` | You want interactive control per phase |
|
||||
| `plan2code-loop` | You want hands-off autonomous execution |
|
||||
|
||||
```bash
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
A structured 4-step workflow for developing features and projects with AI assistance. This methodology emphasizes thorough planning before implementation, ensuring well-documented, maintainable code.
|
||||
|
||||
<img src="docs/plan2code.jpg" alt="Plan2Code Workflow" height="400">
|
||||
<img src="docs/desk.jpg" alt="Plan2Code Workflow" height="275">
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -15,16 +15,18 @@ A structured 4-step workflow for developing features and projects with AI assist
|
||||
New Chat New Chat New Chat (per phase) New Chat
|
||||
```
|
||||
|
||||
| Command | Use When |
|
||||
|---------|----------|
|
||||
| `/plan2code---init` | Generate AGENTS.md file for new/existing projects |
|
||||
| `/plan2code---init-update` | Update AGENTS.md with new learnings from coding sessions |
|
||||
| `/plan2code---quick-task` | Small, quick tasks that don't need full workflow |
|
||||
| `/plan2code-1--plan` | Starting a new feature (full planning) |
|
||||
| `/plan2code-1b--revise-plan` | Requirements change mid-implementation |
|
||||
| `/plan2code-2--document` | After planning, create implementation specs |
|
||||
| `/plan2code-3--implement` | Execute implementation (one phase per conversation) |
|
||||
| `/plan2code-4--finalize` | All phases complete, ready to archive |
|
||||
| Command | When to Use |
|
||||
|----------------------------------|----------------------------------------------------------|
|
||||
| `/plan2code-init` | Generate AGENTS.md file for new/existing projects |
|
||||
| `/plan2code-init-update` | Update AGENTS.md with new learnings from coding sessions |
|
||||
| `/plan2code-quick-task` | Small, quick tasks that don't need full workflow |
|
||||
| `/plan2code-review` | Post-phase code review (after key features or milestones)|
|
||||
| `/plan2code-1-plan` | Starting a new feature (full planning) |
|
||||
| `/plan2code-1b-revise-plan` | Requirements change mid-implementation |
|
||||
| `/plan2code-2-document` | After planning, create implementation specs |
|
||||
| `/plan2code-3-implement` | Execute implementation (one phase per conversation) |
|
||||
| `/plan2code-4-finalize` | All phases complete, ready to archive |
|
||||
| `/plan2code-handoff` | Compact the conversation into a handoff doc for a fresh session |
|
||||
|
||||
**Key Rules:**
|
||||
- Start NEW conversation for each step (and each implementation phase)
|
||||
@@ -40,13 +42,13 @@ See [QUICK-REFERENCE.md](QUICK-REFERENCE.md) for full reference card.
|
||||
|
||||
Plan2Code includes an interactive installer that generates and installs workflow files for all major AI coding assistants.
|
||||
|
||||
<img src="docs/install-script.jpg" width="582">
|
||||
<img src="docs/install-script.jpg" width="600">
|
||||
|
||||
### Prerequisites
|
||||
|
||||
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/)
|
||||
1. Download from [nodejs.org](https://nodejs.org/) (recommended)
|
||||
2. Or use a package manager:
|
||||
- **macOS:** `brew install node`
|
||||
- **Windows:** `winget install OpenJS.NodeJS` or `choco install nodejs`
|
||||
@@ -61,41 +63,82 @@ The install script requires **Node.js** (v14 or later). If you don't have Node.j
|
||||
- Codeium (IntelliJ)
|
||||
- GitHub Copilot CLI
|
||||
- VS Code GitHub Copilot
|
||||
- Gemini CLI
|
||||
- Crush
|
||||
- Pi (pi.dev)
|
||||
- Amp
|
||||
- OpenCode
|
||||
- Devin
|
||||
- Zed
|
||||
|
||||
### Quick Start
|
||||
### Install via npx (Recommended — No Clone Required)
|
||||
|
||||
Run the interactive installer directly using `npx` with your preferred GitHub authentication method:
|
||||
|
||||
**If you use SSH keys:**
|
||||
```bash
|
||||
npx git+ssh://git@github.com/jparkerweb/plan2code.git
|
||||
```
|
||||
|
||||
**If you use HTTPS authentication:**
|
||||
```bash
|
||||
npx git+https://github.com/jparkerweb/plan2code.git
|
||||
```
|
||||
|
||||
This downloads the installer to a temporary location, runs it, installs the workflow files to your machine, and cleans up automatically. The installed workflows remain on your system and work independently. To update or reinstall, simply run the command again.
|
||||
|
||||
### Standard Installation (Clone Method)
|
||||
|
||||
#### No Clone Required
|
||||
|
||||
Run the interactive installer directly using `npx` with your preferred GitHub authentication method:
|
||||
|
||||
**If you use SSH keys:**
|
||||
```bash
|
||||
npx git+ssh://git@github.com/jparkerweb/plan2code.git
|
||||
```
|
||||
|
||||
**If you use HTTPS authentication:**
|
||||
```bash
|
||||
npx git+https://github.com/jparkerweb/plan2code.git
|
||||
```
|
||||
|
||||
This downloads the installer to a temporary location, runs it, installs the workflow files to your machine, and cleans up automatically. The installed workflows remain on your system and work independently. To update or reinstall, simply run the command again.
|
||||
|
||||
#### Standard Installation (Clone Method)
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://github.com/plan/plan2code.git
|
||||
git clone https://github.com/jparkerweb/plan2code.git
|
||||
cd plan2code
|
||||
|
||||
# Run the interactive installer
|
||||
node install.js
|
||||
|
||||
# OPTIONAL: Install dev dependencies (ONLY if you plan to modify/contribute to Plan2Code)
|
||||
npm install
|
||||
```
|
||||
|
||||
The installer will display an interactive menu:
|
||||
The installer displays an interactive menu:
|
||||
|
||||
```
|
||||
Available platforms:
|
||||
╔════════════════════════════════════════════════════════════════╗
|
||||
║ INSTALL PLAN2CODE ║
|
||||
╠════════════════════════════════════════════════════════════════╣
|
||||
║ I. INSTALL Install Plan2Code for all platforms ║
|
||||
║ U. UNINSTALL Remove Plan2Code files ║
|
||||
║ C. CUSTOM Advanced options ║
|
||||
║ Q. QUIT Exit ║
|
||||
╚════════════════════════════════════════════════════════════════╝
|
||||
|
||||
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
|
||||
|
||||
Enter choice (1-7, A, O, L, U, Q, or comma-separated like 1,3,5):
|
||||
SELECT OPTION (I, U, C, Q) [I]:
|
||||
```
|
||||
|
||||
For per-project installation or additional options, run `node install.js --help`.
|
||||
---
|
||||
|
||||
## Status Line — Claude CLI (Optional)
|
||||
|
||||
A persistent three-line status bar for Claude Code showing model, project, git branch, uncommitted diff stats, context usage, and plan quota. Included in `node install.js` → `A` (Install All + dev tools), or available individually via Custom → Status Line. See [src/statusline-claude/README.md](src/statusline-claude/README.md) for details.
|
||||
|
||||
---
|
||||
|
||||
@@ -198,6 +241,31 @@ The `overview.md` includes a "Parallel Execution Groups" section that identifies
|
||||
|
||||
---
|
||||
|
||||
### Review Mode 🔬 (Optional)
|
||||
|
||||
**Purpose:** Comprehensive post-implementation code review with adaptive scope and spec compliance checking.
|
||||
|
||||
**AI Role:** Critical review specialist -- independent second opinion
|
||||
|
||||
**When to use:** After completing implementation phases, especially key features or milestones. Can be run after any phase, not just before finalization.
|
||||
|
||||
**What it does:**
|
||||
|
||||
1. Determines review scope — detects conversation context, user-specified scope, or gathers git changes as fallback
|
||||
2. Understands context and determines review strategy
|
||||
3. Analyzes across 11 dimensions (correctness, security, performance, spec compliance, etc.)
|
||||
4. Generates findings ranked by severity (Critical, Warning, Suggestion)
|
||||
5. Offers to fix issues, then provides commit guidance
|
||||
|
||||
**Key Behaviors:**
|
||||
|
||||
- Uses companion reference files for deep verification, detailed dimension checklists, and false-positive detection (loaded automatically during review)
|
||||
- Reviews all changed files across 11 dimensions; prioritizes by risk when batches exceed 50 files
|
||||
- Spec-aware when `specs/` exists; works standalone without specs
|
||||
- Review phase is read-only; fixes only on user request with verification
|
||||
|
||||
---
|
||||
|
||||
### Step 4: Finalization Mode 🧹
|
||||
|
||||
**Purpose:** Validate implementation, create summaries, and archive documentation.
|
||||
@@ -219,10 +287,11 @@ The `overview.md` includes a "Parallel Execution Groups" section that identifies
|
||||
After running `node install.js`, use the slash commands directly in your AI tool:
|
||||
|
||||
```
|
||||
/smarsh2code-1--plan # Start planning a new feature
|
||||
/smarsh2code-2--document # Create implementation docs from plan
|
||||
/smarsh2code-3--implement # Begin/continue implementation
|
||||
/smarsh2code-4--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-review # Post-phase code review (optional)
|
||||
/plan2code-4-finalize # Wrap up after all phases complete
|
||||
```
|
||||
|
||||
---
|
||||
@@ -361,6 +430,7 @@ The `[/]` status enables parallel execution - multiple agents can work on differ
|
||||
| Step 1 (Plan) | None (describe your feature/project) |
|
||||
| Step 2 (Document) | `specs/<feature>/PLAN-DRAFT-<date>.md` or planning conversation |
|
||||
| Step 3 (Implement) | `specs/<feature>/overview.md` (auto-detects phase) |
|
||||
| Review (Optional) | Scope guidance (e.g., "review last 2 phases", "just the auth module", "whole PR"). Auto-detects changes and specs if no scope given. |
|
||||
| Step 4 (Finalize) | `specs/<feature>/overview.md` |
|
||||
|
||||
---
|
||||
@@ -369,13 +439,13 @@ The `[/]` status enables parallel execution - multiple agents can work on differ
|
||||
|
||||
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.
|
||||
> **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-3-implement` | Interactive control, reviewing each phase, complex logic requiring human judgment |
|
||||
| `plan2code-loop` | Straightforward implementations, batch processing, overnight runs |
|
||||
|
||||
### Installing the Loop
|
||||
@@ -384,10 +454,10 @@ For hands-off implementation, Plan2Code includes an optional autonomous loop CLI
|
||||
# From the plan2code root directory:
|
||||
|
||||
# Option 1: Install everything (recommended)
|
||||
node install.js # Select option A
|
||||
node install.js # Select I at the menu
|
||||
|
||||
# Option 2: Install loop only
|
||||
node install.js # Select option O
|
||||
node install.js # Select C, then O at the menu
|
||||
```
|
||||
|
||||
### Using the Loop
|
||||
|
||||
+25
-5
@@ -985,6 +985,25 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="step">
|
||||
<div class="step-content">
|
||||
<div class="step-header">
|
||||
<div class="step-number headline">review</div>
|
||||
<div class="step-icon" style="color: var(--accent);">🔬</div>
|
||||
</div>
|
||||
<h2 class="step-title subheadline">Review</h2>
|
||||
<p class="step-description">
|
||||
Optional post-implementation code review. An independent senior-engineer second opinion that questions assumptions and surfaces defects with fresh eyes — recommended after key features or milestones.
|
||||
</p>
|
||||
<ul class="step-points">
|
||||
<li>Adaptive scope: focused files, branch changes, or a full subsystem</li>
|
||||
<li>Analyzes across 11 dimensions (correctness, security, performance, spec compliance, and more)</li>
|
||||
<li>Findings ranked by severity with concrete, evidence-based fixes</li>
|
||||
<li>Read-only by default; applies fixes only on request, then verifies</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="step">
|
||||
<div class="step-content">
|
||||
<div class="step-header">
|
||||
@@ -1080,7 +1099,7 @@
|
||||
<div class="start-step">
|
||||
<div class="start-step-number headline">02</div>
|
||||
<div class="start-step-content">
|
||||
<h3 class="subheadline">Start a new chat with <span class="code-line">/plan2code-1--plan</span></h3>
|
||||
<h3 class="subheadline">Start a new chat with <span class="code-line">/plan2code-1-plan</span></h3>
|
||||
<p>Describe your feature or project. The AI will work through planning phases until reaching 90% confidence in the solution.</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1090,10 +1109,11 @@
|
||||
<h3 class="subheadline">Follow the 4-step workflow</h3>
|
||||
<p>New conversation for each step:</p>
|
||||
<ul class="code-list">
|
||||
<li><span class="code-line">/plan2code-1--plan</span></li>
|
||||
<li><span class="code-line">/plan2code-2--document</span></li>
|
||||
<li><span class="code-line">/plan2code-3--implement</span> <span class="gray">(per phase)</span></li>
|
||||
<li><span class="code-line">/plan2code-4--finalize</span></li>
|
||||
<li><span class="code-line">/plan2code-1-plan</span></li>
|
||||
<li><span class="code-line">/plan2code-2-document</span></li>
|
||||
<li><span class="code-line">/plan2code-3-implement</span> <span class="gray">(per phase)</span></li>
|
||||
<li><span class="code-line">/plan2code-review</span> <span class="gray">(optional)</span></li>
|
||||
<li><span class="code-line">/plan2code-4-finalize</span></li>
|
||||
</ul>
|
||||
<br>
|
||||
<p>Clean context, maximum clarity!</p>
|
||||
|
||||
+1403
-440
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "plan2code",
|
||||
"version": "1.16.0",
|
||||
"private": true,
|
||||
"bin": {
|
||||
"plan2code": "./install.js"
|
||||
},
|
||||
"scripts": {
|
||||
"prepare": "husky",
|
||||
"test": "node scripts/validate-char-count.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"husky": "^9.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
# Changelog
|
||||
|
||||
## 1.1.0
|
||||
|
||||
### Resume support
|
||||
- Add `--resume` flag to continue incomplete runs from saved state
|
||||
- Skip previously succeeded steps when resuming (init, plan, document, implement, finalize)
|
||||
- Restore idea name, description, project directory, and implement pass counter from state
|
||||
- Auto-detect state files in current directory (enhancement mode) or subdirectories (new-project mode)
|
||||
- Delete state file automatically after a fully successful run
|
||||
- Preserve state file on failure for later resume
|
||||
- Add `deleteState()` and `findExistingState()` utilities to bot-state module
|
||||
- Add bot-state unit tests (saveState, loadState, deleteState, findExistingState)
|
||||
|
||||
### Idea generation improvements
|
||||
- Expand idea categories from binary CLI/web-app coin flip to 12 diverse categories (games, dashboards, browser extensions, desktop utilities, etc.)
|
||||
- Add guidance to avoid defaulting to developer-centric tools (git analyzers, code formatters)
|
||||
- Strengthen `--idea` seed clause so the LLM stays aligned with the user's theme instead of ignoring it
|
||||
- Update system prompt to encourage creative, cross-domain ideas
|
||||
|
||||
### Init step overhaul (new projects)
|
||||
- Init now creates a minimal AGENTS.md stub (name, description, status) instead of running the full `/plan2code-init` skill
|
||||
- Prevents hallucinated architecture, commands, and `.agents-docs/` files before the plan step runs
|
||||
- Init evaluation criteria updated to reward minimalism and penalize premature detail
|
||||
|
||||
### Implement step overhaul
|
||||
- Implement step now works directly with Read/Write/Edit/Glob/Grep tools instead of delegating to Skill sub-session
|
||||
- Inlined step-by-step process: find specs, pick phase, implement tasks, mark checkboxes
|
||||
- Fixes issue where Skill sub-sessions did all work invisibly, causing zero tool observations
|
||||
|
||||
### Observation tracking fix
|
||||
- Capture `tool_use` blocks from the assistant message stream in session-runner as a fallback when `canUseTool` callback doesn't fire
|
||||
- Add deduplication in ObservationCollector to prevent double-counting from both sources
|
||||
- Fixes all steps reporting 0 tools used / 0 files created in BOT-NOTES and evaluations
|
||||
|
||||
### Evaluator improvements
|
||||
- Increase evaluator `maxTurns` from 3 to 30 so it has room for tool calls before producing the scored response
|
||||
- Add warning log when evaluation parser can't find SCORE in output (was silently defaulting to 50)
|
||||
|
||||
## 1.0.0
|
||||
|
||||
- Initial release
|
||||
- Two auto-detected modes: new-project and enhancement
|
||||
- `--idea` flag to seed the idea generator
|
||||
- Full workflow execution: init → plan → document → implement → finalize
|
||||
- Artifact validation after each step
|
||||
- State persistence to `.plan2code-bot-state.json`
|
||||
- Auto-responder for autonomous Claude Agent SDK sessions
|
||||
- Bot-friendly skill installation (strips `disable-model-invocation`)
|
||||
@@ -0,0 +1,234 @@
|
||||
# LLM-as-Judge Evaluation System
|
||||
|
||||
## Overview
|
||||
|
||||
The plan2code-bot now includes an **always-on LLM-as-judge evaluation system** that transforms it from a "yes-man" into an authentic QA agent. This provides realistic quality signals for `plan2code-metrics` to analyze and drive recursive self-improvement.
|
||||
|
||||
## Key Features
|
||||
|
||||
### 1. Intelligent Decision Making (Real-Time)
|
||||
|
||||
**What:** During execution, when `AskUserQuestion` is called, the bot uses an LLM to make thoughtful decisions based on current observations.
|
||||
|
||||
**How it works:**
|
||||
- Collects observations up to the current point (tools used, files created, errors)
|
||||
- Queries LLM with context: "Given what you've seen, should you approve this plan?"
|
||||
- LLM inspects current artifacts using Read/Glob/Grep
|
||||
- Returns evidence-based answer with reasoning
|
||||
- All decisions are recorded for metrics analysis
|
||||
|
||||
**Example:**
|
||||
```
|
||||
Question: "Approve this plan?"
|
||||
Observations: Created PLAN-DRAFT.md, 3 phases, 42s duration, no errors
|
||||
LLM reads PLAN-DRAFT.md, evaluates quality
|
||||
LLM decides: "Yes, approve - phases are well-scoped and realistic"
|
||||
```
|
||||
|
||||
### 2. Post-Step Evaluation
|
||||
|
||||
**What:** After each step completes, the bot evaluates quality using step-specific criteria.
|
||||
|
||||
**How it works:**
|
||||
- Collects complete execution observations
|
||||
- Queries LLM with evaluation criteria for the step
|
||||
- LLM inspects final artifacts
|
||||
- Returns structured evaluation (score, strengths, weaknesses, suggestions)
|
||||
- Writes `specs/<feature>/BOT-EVALUATION.md` and `specs/<feature>/BOT-NOTES.md` (falls back to project root if no spec folder exists yet, e.g. during `init`)
|
||||
|
||||
**Example output:**
|
||||
```markdown
|
||||
# Evaluation: plan Step
|
||||
|
||||
**Score:** 78/100
|
||||
|
||||
## Strengths
|
||||
- Clear phase breakdown with realistic scope
|
||||
- Tech stack choices appropriate
|
||||
|
||||
## Weaknesses
|
||||
- Phase 3 description too vague
|
||||
- No testing strategy mentioned
|
||||
|
||||
## Suggestions
|
||||
- Expand Phase 3 with concrete tasks
|
||||
- Add explicit testing phase
|
||||
```
|
||||
|
||||
### 3. Quality Gate
|
||||
|
||||
**What:** Before finalize, checks that average quality score is acceptable.
|
||||
|
||||
**How it works:**
|
||||
- Calculates average score across all evaluated steps
|
||||
- If average < 60, blocks finalization
|
||||
- Displays clear message about quality issues
|
||||
- User must review `specs/<feature>/BOT-EVALUATION.md` and fix problems
|
||||
|
||||
## Files Created
|
||||
|
||||
### New Files
|
||||
|
||||
1. **`src/observation-collector.ts`**
|
||||
- Tracks execution details (tools, files, messages, errors, questions)
|
||||
- Provides snapshots for real-time decisions
|
||||
- Captures complete history for evaluation
|
||||
|
||||
2. **`src/intelligent-responder.ts`**
|
||||
- Replaces hardcoded auto-responder
|
||||
- Uses LLM to answer AskUserQuestion prompts
|
||||
- Provides reasoning for all decisions
|
||||
- Falls back gracefully if LLM unavailable
|
||||
|
||||
3. **`src/prompts/evaluation-criteria.ts`**
|
||||
- Step-specific evaluation criteria (init, plan, document, implement, finalize)
|
||||
- Quality checks, common pitfalls, scoring guidance
|
||||
- Emphasizes honest scoring (most work should score 70-85)
|
||||
|
||||
4. **`src/evaluator.ts`**
|
||||
- Post-step evaluation using LLM-as-judge
|
||||
- Queries LLM with observations and criteria
|
||||
- Parses structured evaluation output
|
||||
- Writes `specs/<feature>/BOT-EVALUATION.md` and `specs/<feature>/BOT-NOTES.md`
|
||||
|
||||
### Modified Files
|
||||
|
||||
1. **`src/types.ts`**
|
||||
- Added interfaces: `ToolObservation`, `QuestionContext`, `ExecutionObservation`, `EvaluationResult`
|
||||
- Extended `StepResult` with `evaluation` and `observations` fields
|
||||
|
||||
2. **`src/session-runner.ts`**
|
||||
- Added `collector` parameter to `SessionOptions`
|
||||
- Returns `observations` in `SessionResult`
|
||||
- Records all messages for observation tracking
|
||||
- Uses intelligent responder instead of auto-responder
|
||||
|
||||
3. **`src/cli.ts`**
|
||||
- Creates `ObservationCollector` for each step
|
||||
- Always runs evaluation after successful steps
|
||||
- Displays scores with color coding (green/yellow/red)
|
||||
- Implements quality gate before finalize
|
||||
- Shows evaluation summary in step output
|
||||
|
||||
4. **`src/bin/plan2code-bot.ts`**
|
||||
- Updated help text to mention LLM-as-judge evaluation
|
||||
- No new CLI flags (evaluation is always on)
|
||||
|
||||
### Deleted Files
|
||||
|
||||
1. **`src/auto-responder.ts`** - Replaced by intelligent-responder.ts
|
||||
2. **`src/auto-responder.test.ts`** - No longer needed
|
||||
|
||||
## Output Files (Created During Execution)
|
||||
|
||||
Both files are written to `specs/<feature>/` so they stay co-located with the feature they describe. If no spec folder exists yet (e.g. during `init`), they fall back to the project root.
|
||||
|
||||
### BOT-EVALUATION.md
|
||||
|
||||
Contains evaluation results for each step:
|
||||
- Score (0-100)
|
||||
- Strengths identified
|
||||
- Weaknesses found
|
||||
- Suggestions for improvement
|
||||
- Critical issues (if any)
|
||||
- Full reasoning from LLM
|
||||
|
||||
### BOT-NOTES.md
|
||||
|
||||
Contains execution observations:
|
||||
- Duration, tool counts, file changes
|
||||
- Questions asked and LLM reasoning for answers
|
||||
- Tool usage timeline
|
||||
- Files created/modified
|
||||
- Assistant output summary
|
||||
|
||||
## Data Structure for Metrics
|
||||
|
||||
All evaluation data is structured in `StepResult`:
|
||||
|
||||
```typescript
|
||||
{
|
||||
step: 'plan',
|
||||
success: true,
|
||||
duration: 42000,
|
||||
evaluation: {
|
||||
score: 78,
|
||||
strengths: ["Clear phases", "Realistic scope"],
|
||||
weaknesses: ["Phase 3 too vague"],
|
||||
suggestions: ["Add specific tasks to Phase 3"],
|
||||
criticalIssues: [],
|
||||
reasoning: "...",
|
||||
timestamp: 1234567890,
|
||||
evaluatorModel: 'claude-sonnet-4-5'
|
||||
},
|
||||
observations: {
|
||||
tools: [{ toolName, input, output, timestamp }, ...],
|
||||
questionsAsked: [
|
||||
{
|
||||
question: "Approve plan?",
|
||||
selectedAnswer: "Yes, approve",
|
||||
llmReasoning: "Phases are well-scoped...",
|
||||
timestamp: 1234567890
|
||||
}
|
||||
],
|
||||
filesCreated: [...],
|
||||
filesModified: [...],
|
||||
errors: []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Benefits for Recursive Improvement
|
||||
|
||||
1. **Authentic Signals:** Real quality scores identify actual problem areas
|
||||
2. **Detailed Context:** Observations + reasoning explain WHY failures happen
|
||||
3. **Correlation Analysis:** Link patterns (tool usage, duration, errors) to quality
|
||||
4. **Continuous Loop:** Better metrics → improved workflows → higher scores → repeat
|
||||
|
||||
## Usage
|
||||
|
||||
No special flags needed - evaluation is always on:
|
||||
|
||||
```bash
|
||||
# New project
|
||||
plan2code-bot --idea "todo app"
|
||||
|
||||
# Enhancement
|
||||
cd my-project && plan2code-bot
|
||||
|
||||
# Resume with evaluation data preserved
|
||||
plan2code-bot --resume
|
||||
```
|
||||
|
||||
## Verification
|
||||
|
||||
After running the bot, check:
|
||||
|
||||
1. **`specs/<feature>/BOT-EVALUATION.md`** - Should show realistic scores (not all 100s)
|
||||
2. **`specs/<feature>/BOT-NOTES.md`** - Should show LLM reasoning for decisions
|
||||
3. **Console output** - Should display color-coded scores after each step
|
||||
4. **State file** (`.plan2code-bot-state.json`) - Should include evaluation data
|
||||
|
||||
## Trade-offs
|
||||
|
||||
### Latency
|
||||
- Adds ~2-3s per AskUserQuestion call (~15-20s total per run)
|
||||
- Worth it for authentic evaluation
|
||||
|
||||
### Token Cost
|
||||
- ~20-26K tokens per run (~$0.60 with Opus 4.6)
|
||||
- Investment pays off through metrics-driven improvement
|
||||
|
||||
### Determinism
|
||||
- LLM decisions vary between runs (non-deterministic)
|
||||
- Realistic - humans vary too
|
||||
- Metrics average over many runs
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
Potential improvements:
|
||||
- Model selection per step (use Haiku for simple decisions)
|
||||
- Configurable quality gate threshold
|
||||
- Historical score tracking across runs
|
||||
- Comparison with previous evaluations
|
||||
- More sophisticated scoring (weighted by step importance)
|
||||
@@ -0,0 +1,111 @@
|
||||
# plan2code-bot
|
||||
|
||||
Autonomous workflow test runner for plan2code. Uses the Claude Agent SDK to simulate a human running through the entire plan2code workflow (init, plan, document, implement, finalize) end-to-end.
|
||||
|
||||
## Two Modes (Auto-Detected)
|
||||
|
||||
1. **New Project Mode** — No `AGENTS.md` in cwd: generates an app idea, creates a subdirectory, writes IDEA.md, runs init, then all 4 steps.
|
||||
2. **Enhancement Mode** — `AGENTS.md` exists in cwd: scans the existing codebase and proposes a realistic enhancement, writes IDEA.md, then runs plan through finalize.
|
||||
|
||||
## Installation
|
||||
|
||||
From the plan2code root:
|
||||
|
||||
```bash
|
||||
node install.js
|
||||
# Select C > B to install bot only, or I to install everything
|
||||
```
|
||||
|
||||
Or manually:
|
||||
|
||||
```bash
|
||||
cd plan2code-bot
|
||||
npm install
|
||||
npm run build
|
||||
npm link
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```bash
|
||||
# New project mode (run from an empty directory)
|
||||
mkdir /tmp/test-bot && cd /tmp/test-bot
|
||||
plan2code-bot
|
||||
|
||||
# Enhancement mode (run from an existing project with AGENTS.md)
|
||||
cd my-project
|
||||
plan2code-bot
|
||||
|
||||
# Seed the idea generator with a specific concept
|
||||
plan2code-bot --idea "web app that displays the current weather as vector images"
|
||||
|
||||
# Resume a previous incomplete run
|
||||
plan2code-bot --resume
|
||||
```
|
||||
|
||||
### `--idea`
|
||||
|
||||
Pass a quoted string after `--idea` to seed the idea generator with a specific concept. The AI will use it as inspiration rather than generating a completely random idea. Wrap the value in double quotes so the shell treats it as a single argument.
|
||||
|
||||
```bash
|
||||
# Specific app concept
|
||||
plan2code-bot --idea "web app that displays the current weather as vector images"
|
||||
|
||||
# Short keyword to nudge the category
|
||||
plan2code-bot --idea "markdown editor"
|
||||
|
||||
# Detailed constraint
|
||||
plan2code-bot --idea "CLI tool that converts CSV files to SQLite databases with type inference"
|
||||
|
||||
# Works in enhancement mode too — guides what kind of enhancement to propose
|
||||
cd my-existing-project
|
||||
plan2code-bot --idea "add dark mode support"
|
||||
```
|
||||
|
||||
Without `--idea`, the bot picks a random category (CLI tool or web app) and invents something on its own.
|
||||
|
||||
### `--resume`
|
||||
|
||||
Resume a previous incomplete run. The bot searches for a `.plan2code-bot-state.json` file in the current directory (enhancement mode) or in immediate subdirectories (new-project mode). If found, it restores the idea, config, and progress — skipping steps that already succeeded and continuing from where it left off.
|
||||
|
||||
```bash
|
||||
# A run failed at the implement step — resume it
|
||||
plan2code-bot --resume
|
||||
|
||||
# Can combine with --idea (idea is ignored when resuming since it's restored from state)
|
||||
plan2code-bot --resume --idea "ignored when state exists"
|
||||
```
|
||||
|
||||
If no state file is found, the bot starts a fresh run.
|
||||
|
||||
## How It Works
|
||||
|
||||
1. Detects mode based on presence of `AGENTS.md`
|
||||
2. Generates an idea (new app or enhancement) via Claude, optionally guided by `--idea` seed
|
||||
3. Writes `IDEA.md` to the project directory
|
||||
4. Installs bot-friendly copies of plan2code skills (strips `disable-model-invocation` so sessions can invoke them)
|
||||
5. Runs each workflow step as a separate Claude Agent SDK session:
|
||||
- **init** — generates `AGENTS.md` (new-project mode only)
|
||||
- **plan** — creates plan draft in `specs/<feature>/`
|
||||
- **document** — produces `overview.md` and `phase-*.md` files
|
||||
- **implement** — loops until all phases are complete (max 10 passes)
|
||||
- **finalize** — validates and archives to `specs--completed/`
|
||||
6. Validates expected artifacts after each step (aborts on missing artifacts)
|
||||
7. Moves `IDEA.md` into `specs/<feature>/` after the plan step so it stays with its feature
|
||||
8. Auto-responds to `AskUserQuestion` prompts (approvals, testing gates, name questions)
|
||||
9. Saves state to `.plan2code-bot-state.json` after each step
|
||||
|
||||
## State File
|
||||
|
||||
After each step, the bot saves its state to `.plan2code-bot-state.json` in the project directory. This includes the config, all step results, and progress tracking.
|
||||
|
||||
- **On full success** — the state file is automatically deleted (clean finish)
|
||||
- **On failure/incomplete** — the state file is preserved so you can `--resume` later
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
npm run build # Build with tsup
|
||||
npm run dev # Watch mode
|
||||
npm test # Run tests (vitest)
|
||||
```
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"name": "plan2code-bot",
|
||||
"version": "1.1.0",
|
||||
"description": "Plan2Code Bot - Autonomous workflow runner for testing plan2code end-to-end",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"bin": {
|
||||
"plan2code-bot": "./dist/bin/plan2code-bot.js"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"dev": "tsup --watch",
|
||||
"start": "node dist/bin/plan2code-bot.js",
|
||||
"test": "vitest run",
|
||||
"prepublishOnly": "npm run build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/claude-agent-sdk": "^0.2.63",
|
||||
"chalk": "^5.6.2",
|
||||
"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",
|
||||
"vitest": "^4.0.18"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
import { runCLI } from '../cli.js';
|
||||
|
||||
function showHelp(): void {
|
||||
console.log(`
|
||||
+----------------------------------------------------------------+
|
||||
— PLAN2CODEDE-BOT —
|
||||
—----------------------------------------------------------------—
|
||||
— Autonomous workflow test runner foplan2codede —
|
||||
— Features LLM-as-judge for honest quality evaluation —
|
||||
+----------------------------------------------------------------+
|
||||
|
||||
Usage:
|
||||
plan2code-bot [options]
|
||||
|
||||
Options:
|
||||
--help Show this help message
|
||||
--idea <string> Seed the idea generator with a specific concept
|
||||
Example: --idea "web app for weather"
|
||||
Example: --idea="CLI tool for CSV conversion"
|
||||
--resume Resume a previous incomplete run
|
||||
|
||||
Modes:
|
||||
— New Project Mode - Run from empty directory
|
||||
The bot generates an app idea, creates a subdirectory, writes
|
||||
IDEA.md, runs init, then all 4 workflow steps.
|
||||
|
||||
— Enhancement Mode - Run from directory with AGENTS.md
|
||||
The bot scans the existing codebase, proposes an enhancement,
|
||||
writes IDEA.md, then runs plan through finalize.
|
||||
|
||||
Evaluation:
|
||||
The bot acts as an authentic QA agent, using LLM-based decision
|
||||
making during execution and providing honest quality assessments
|
||||
after each step. Results are written to specs/<feature>/BOT-EVALUATION.md
|
||||
and specs/<feature>/BOT-NOTES.md for metrics analysis.
|
||||
|
||||
Examples:
|
||||
# New project (from empty directory)
|
||||
plan2code-bot
|
||||
|
||||
# Enhancement (from existing project)
|
||||
cd my-project && plan2code-bot
|
||||
|
||||
# With specific idea
|
||||
plan2code-bot --idea "markdown editor with live preview"
|
||||
|
||||
# Resume incomplete run
|
||||
plan2code-bot --resume
|
||||
|
||||
Documentation:
|
||||
https://jparkerweb.github.io/plan2code
|
||||
`);
|
||||
}
|
||||
|
||||
function stripQuotes(str: string): string {
|
||||
// Remove surrounding quotes if present (both single and double)
|
||||
if ((str.startsWith('"') && str.endsWith('"')) ||
|
||||
(str.startsWith("'") && str.endsWith("'"))) {
|
||||
return str.slice(1, -1);
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
function parseArgs(): { idea?: string; resume?: boolean; help?: boolean } {
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
// Check for --help
|
||||
if (args.includes('--help') || args.includes('-h')) {
|
||||
return { help: true };
|
||||
}
|
||||
|
||||
// Parse --idea (supports both --idea="value" and --idea "value")
|
||||
let idea: string | undefined;
|
||||
for (let i = 0; i < args.length; i++) {
|
||||
const arg = args[i];
|
||||
|
||||
// Format: --idea="value"
|
||||
if (arg.startsWith('--idea=')) {
|
||||
idea = stripQuotes(arg.substring('--idea='.length));
|
||||
break;
|
||||
}
|
||||
|
||||
// Format: --idea "value"
|
||||
if (arg === '--idea' && i + 1 < args.length) {
|
||||
idea = stripQuotes(args[i + 1]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse --resume
|
||||
const resume = args.includes('--resume');
|
||||
|
||||
return { idea, resume: resume || undefined };
|
||||
}
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
const { idea, resume, help } = parseArgs();
|
||||
|
||||
if (help) {
|
||||
showHelp();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
await runCLI({ idea, resume });
|
||||
process.exit(0);
|
||||
} catch (err) {
|
||||
if (err instanceof Error && err.message.includes('User force closed')) {
|
||||
process.exit(0);
|
||||
}
|
||||
console.error(err instanceof Error ? err.message : String(err));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,121 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
import { saveState, loadState, deleteState, findExistingState } from './bot-state.js';
|
||||
import type { BotState } from './types.js';
|
||||
|
||||
function makeTmpDir(): string {
|
||||
return fs.mkdtempSync(path.join(os.tmpdir(), 'bot-state-test-'));
|
||||
}
|
||||
|
||||
function makeState(projectDir: string): BotState {
|
||||
return {
|
||||
config: {
|
||||
workDir: path.dirname(projectDir),
|
||||
projectDir,
|
||||
ideaName: 'test-idea',
|
||||
ideaDescription: 'A test idea',
|
||||
mode: 'new-project',
|
||||
},
|
||||
steps: [],
|
||||
currentStep: null,
|
||||
implementPasses: 0,
|
||||
allPhasesComplete: false,
|
||||
};
|
||||
}
|
||||
|
||||
describe('bot-state', () => {
|
||||
let tmpDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = makeTmpDir();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.removeSync(tmpDir);
|
||||
});
|
||||
|
||||
describe('saveState', () => {
|
||||
it('writes valid JSON', () => {
|
||||
const projectDir = path.join(tmpDir, 'project');
|
||||
fs.ensureDirSync(projectDir);
|
||||
const state = makeState(projectDir);
|
||||
|
||||
saveState(state);
|
||||
|
||||
const filePath = path.join(projectDir, '.plan2code-bot-state.json');
|
||||
expect(fs.existsSync(filePath)).toBe(true);
|
||||
const parsed = fs.readJsonSync(filePath);
|
||||
expect(parsed.config.ideaName).toBe('test-idea');
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadState', () => {
|
||||
it('returns state from file', () => {
|
||||
const projectDir = path.join(tmpDir, 'project');
|
||||
fs.ensureDirSync(projectDir);
|
||||
const state = makeState(projectDir);
|
||||
saveState(state);
|
||||
|
||||
const loaded = loadState(projectDir);
|
||||
expect(loaded).not.toBeNull();
|
||||
expect(loaded!.config.ideaName).toBe('test-idea');
|
||||
expect(loaded!.implementPasses).toBe(0);
|
||||
});
|
||||
|
||||
it('returns null when file does not exist', () => {
|
||||
const result = loadState(path.join(tmpDir, 'nonexistent'));
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteState', () => {
|
||||
it('removes the file', () => {
|
||||
const projectDir = path.join(tmpDir, 'project');
|
||||
fs.ensureDirSync(projectDir);
|
||||
const state = makeState(projectDir);
|
||||
saveState(state);
|
||||
|
||||
const filePath = path.join(projectDir, '.plan2code-bot-state.json');
|
||||
expect(fs.existsSync(filePath)).toBe(true);
|
||||
|
||||
deleteState(projectDir);
|
||||
expect(fs.existsSync(filePath)).toBe(false);
|
||||
});
|
||||
|
||||
it('is a no-op when file does not exist', () => {
|
||||
// Should not throw
|
||||
deleteState(path.join(tmpDir, 'nonexistent'));
|
||||
});
|
||||
});
|
||||
|
||||
describe('findExistingState', () => {
|
||||
it('finds state in workDir (enhancement mode)', () => {
|
||||
const state = makeState(tmpDir);
|
||||
state.config.projectDir = tmpDir;
|
||||
state.config.mode = 'enhancement';
|
||||
saveState(state);
|
||||
|
||||
const found = findExistingState(tmpDir);
|
||||
expect(found).not.toBeNull();
|
||||
expect(found!.config.ideaName).toBe('test-idea');
|
||||
});
|
||||
|
||||
it('finds state in a subdirectory (new-project mode)', () => {
|
||||
const projectDir = path.join(tmpDir, 'my-app');
|
||||
fs.ensureDirSync(projectDir);
|
||||
const state = makeState(projectDir);
|
||||
saveState(state);
|
||||
|
||||
const found = findExistingState(tmpDir);
|
||||
expect(found).not.toBeNull();
|
||||
expect(found!.config.projectDir).toBe(projectDir);
|
||||
});
|
||||
|
||||
it('returns null when no state exists', () => {
|
||||
const found = findExistingState(tmpDir);
|
||||
expect(found).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import type { BotState } from './types.js';
|
||||
|
||||
const STATE_FILE = '.plan2code-bot-state.json';
|
||||
|
||||
export function saveState(state: BotState): void {
|
||||
const filePath = path.join(state.config.projectDir, STATE_FILE);
|
||||
fs.writeJsonSync(filePath, state, { spaces: 2 });
|
||||
}
|
||||
|
||||
export function loadState(projectDir: string): BotState | null {
|
||||
const filePath = path.join(projectDir, STATE_FILE);
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return null;
|
||||
}
|
||||
return fs.readJsonSync(filePath) as BotState;
|
||||
}
|
||||
|
||||
export function deleteState(projectDir: string): void {
|
||||
const filePath = path.join(projectDir, STATE_FILE);
|
||||
if (fs.existsSync(filePath)) {
|
||||
fs.removeSync(filePath);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Search for an existing state file in workDir (enhancement mode)
|
||||
* or in immediate subdirectories (new-project mode).
|
||||
*/
|
||||
export function findExistingState(workDir: string): BotState | null {
|
||||
// Enhancement mode: state is in workDir directly
|
||||
const direct = loadState(workDir);
|
||||
if (direct) return direct;
|
||||
|
||||
// New-project mode: state is in a subdirectory
|
||||
try {
|
||||
for (const entry of fs.readdirSync(workDir, { withFileTypes: true })) {
|
||||
if (entry.isDirectory()) {
|
||||
const sub = loadState(path.join(workDir, entry.name));
|
||||
if (sub) return sub;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// workDir not readable — ignore
|
||||
}
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import fs from 'fs-extra';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import { validateStepArtifacts, installSkillsForBot } from './cli.js';
|
||||
|
||||
let tmpDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'p2c-cli-test-'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.removeSync(tmpDir);
|
||||
});
|
||||
|
||||
// ── validateStepArtifacts ───────────────────────────────────────────
|
||||
|
||||
describe('validateStepArtifacts', () => {
|
||||
describe('init', () => {
|
||||
it('valid when AGENTS.md exists', () => {
|
||||
fs.writeFileSync(path.join(tmpDir, 'AGENTS.md'), '# Agents');
|
||||
const result = validateStepArtifacts('init', tmpDir);
|
||||
expect(result.valid).toBe(true);
|
||||
expect(result.missing).toEqual([]);
|
||||
});
|
||||
|
||||
it('missing when AGENTS.md does not exist', () => {
|
||||
const result = validateStepArtifacts('init', tmpDir);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.missing).toContain('AGENTS.md');
|
||||
});
|
||||
});
|
||||
|
||||
describe('plan', () => {
|
||||
it('valid when specs/feature/PLAN-DRAFT-*.md exists', () => {
|
||||
const specDir = path.join(tmpDir, 'specs', 'my-feature');
|
||||
fs.ensureDirSync(specDir);
|
||||
fs.writeFileSync(path.join(specDir, 'PLAN-DRAFT-v1.md'), '# Plan');
|
||||
const result = validateStepArtifacts('plan', tmpDir);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
it('missing when specs/ is absent', () => {
|
||||
const result = validateStepArtifacts('plan', tmpDir);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.missing).toContain('specs/ directory');
|
||||
});
|
||||
|
||||
it('missing when specs/ has dirs but no plan draft files', () => {
|
||||
const specDir = path.join(tmpDir, 'specs', 'my-feature');
|
||||
fs.ensureDirSync(specDir);
|
||||
fs.writeFileSync(path.join(specDir, 'notes.md'), '# Notes');
|
||||
const result = validateStepArtifacts('plan', tmpDir);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.missing).toContain('specs/*/PLAN-DRAFT-*.md');
|
||||
});
|
||||
});
|
||||
|
||||
describe('document', () => {
|
||||
it('valid when overview.md and phase-*.md exist', () => {
|
||||
const specDir = path.join(tmpDir, 'specs', 'my-feature');
|
||||
fs.ensureDirSync(specDir);
|
||||
fs.writeFileSync(path.join(specDir, 'overview.md'), '# Overview');
|
||||
fs.writeFileSync(path.join(specDir, 'phase-1.md'), '# Phase 1');
|
||||
const result = validateStepArtifacts('document', tmpDir);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
it('missing overview.md when absent', () => {
|
||||
const specDir = path.join(tmpDir, 'specs', 'my-feature');
|
||||
fs.ensureDirSync(specDir);
|
||||
fs.writeFileSync(path.join(specDir, 'phase-1.md'), '# Phase 1');
|
||||
const result = validateStepArtifacts('document', tmpDir);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.missing).toContain('specs/*/overview.md');
|
||||
});
|
||||
|
||||
it('missing phase files when absent', () => {
|
||||
const specDir = path.join(tmpDir, 'specs', 'my-feature');
|
||||
fs.ensureDirSync(specDir);
|
||||
fs.writeFileSync(path.join(specDir, 'overview.md'), '# Overview');
|
||||
const result = validateStepArtifacts('document', tmpDir);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.missing).toContain('specs/*/phase-*.md files');
|
||||
});
|
||||
|
||||
it('accepts phase files inside phases/ subdirectory', () => {
|
||||
const specDir = path.join(tmpDir, 'specs', 'my-feature');
|
||||
const phasesDir = path.join(specDir, 'phases');
|
||||
fs.ensureDirSync(phasesDir);
|
||||
fs.writeFileSync(path.join(specDir, 'overview.md'), '# Overview');
|
||||
fs.writeFileSync(path.join(phasesDir, 'phase-1.md'), '# Phase 1');
|
||||
const result = validateStepArtifacts('document', tmpDir);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('finalize', () => {
|
||||
it('valid when specs--completed/ has a subdirectory', () => {
|
||||
fs.ensureDirSync(path.join(tmpDir, 'specs--completed', 'my-feature'));
|
||||
const result = validateStepArtifacts('finalize', tmpDir);
|
||||
expect(result.valid).toBe(true);
|
||||
});
|
||||
|
||||
it('missing when specs--completed/ is absent', () => {
|
||||
const result = validateStepArtifacts('finalize', tmpDir);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.missing).toContain('specs--completed/ directory');
|
||||
});
|
||||
|
||||
it('missing when specs--completed/ is empty', () => {
|
||||
fs.ensureDirSync(path.join(tmpDir, 'specs--completed'));
|
||||
const result = validateStepArtifacts('finalize', tmpDir);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.missing).toContain('archived spec in specs--completed/');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ── installSkillsForBot ─────────────────────────────────────────────
|
||||
|
||||
describe('installSkillsForBot', () => {
|
||||
let fakeHome: string;
|
||||
let projectDir: string;
|
||||
let origHome: string | undefined;
|
||||
let origUserProfile: string | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
fakeHome = fs.mkdtempSync(path.join(os.tmpdir(), 'p2c-home-'));
|
||||
projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'p2c-proj-'));
|
||||
origHome = process.env.HOME;
|
||||
origUserProfile = process.env.USERPROFILE;
|
||||
process.env.HOME = fakeHome;
|
||||
process.env.USERPROFILE = fakeHome;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env.HOME = origHome;
|
||||
process.env.USERPROFILE = origUserProfile;
|
||||
fs.removeSync(fakeHome);
|
||||
fs.removeSync(projectDir);
|
||||
});
|
||||
|
||||
it('copies skills from user dir to project .claude/skills/', () => {
|
||||
const srcDir = path.join(fakeHome, '.claude', 'skills', 'plan2code-init');
|
||||
fs.ensureDirSync(srcDir);
|
||||
fs.writeFileSync(path.join(srcDir, 'SKILL.md'), '---\nname: init\n---\nSome content');
|
||||
|
||||
installSkillsForBot(projectDir);
|
||||
|
||||
const dest = path.join(projectDir, '.claude', 'skills', 'plan2code-init', 'SKILL.md');
|
||||
expect(fs.existsSync(dest)).toBe(true);
|
||||
expect(fs.readFileSync(dest, 'utf-8')).toContain('Some content');
|
||||
});
|
||||
|
||||
it('strips disable-model-invocation: true from copied SKILL.md', () => {
|
||||
const srcDir = path.join(fakeHome, '.claude', 'skills', 'plan2code-1-plan');
|
||||
fs.ensureDirSync(srcDir);
|
||||
fs.writeFileSync(
|
||||
path.join(srcDir, 'SKILL.md'),
|
||||
'disable-model-invocation: true\n---\nname: plan\n---\nPlan content',
|
||||
);
|
||||
|
||||
installSkillsForBot(projectDir);
|
||||
|
||||
const dest = path.join(projectDir, '.claude', 'skills', 'plan2code-1-plan', 'SKILL.md');
|
||||
const content = fs.readFileSync(dest, 'utf-8');
|
||||
expect(content).not.toContain('disable-model-invocation');
|
||||
expect(content).toContain('Plan content');
|
||||
});
|
||||
|
||||
it('preserves rest of skill content', () => {
|
||||
const srcDir = path.join(fakeHome, '.claude', 'skills', 'plan2code-2-document');
|
||||
fs.ensureDirSync(srcDir);
|
||||
const original = 'disable-model-invocation: true\n---\nname: doc\n---\nLine 1\nLine 2\nLine 3';
|
||||
fs.writeFileSync(path.join(srcDir, 'SKILL.md'), original);
|
||||
|
||||
installSkillsForBot(projectDir);
|
||||
|
||||
const dest = path.join(projectDir, '.claude', 'skills', 'plan2code-2-document', 'SKILL.md');
|
||||
const content = fs.readFileSync(dest, 'utf-8');
|
||||
expect(content).toContain('Line 1');
|
||||
expect(content).toContain('Line 2');
|
||||
expect(content).toContain('Line 3');
|
||||
expect(content).toContain('name: doc');
|
||||
});
|
||||
|
||||
it('skips gracefully when source skill does not exist', () => {
|
||||
// No skills in fakeHome — should not throw
|
||||
expect(() => installSkillsForBot(projectDir)).not.toThrow();
|
||||
// No .claude/skills/ created in project
|
||||
expect(fs.existsSync(path.join(projectDir, '.claude', 'skills'))).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,587 @@
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import chalk from 'chalk';
|
||||
import ora from 'ora';
|
||||
import { generateNewAppIdea, generateEnhancementIdea } from './idea-generator.js';
|
||||
import { runSession } from './session-runner.js';
|
||||
import { buildStepPrompt } from './prompts/step-instructions.js';
|
||||
import { checkAllPhasesComplete } from './step-detector.js';
|
||||
import { saveState, loadState, deleteState, findExistingState } from './bot-state.js';
|
||||
import { ObservationCollector } from './observation-collector.js';
|
||||
import { evaluateStep } from './evaluator.js';
|
||||
import type { BotConfig, BotMode, BotState, StepName, StepResult } from './types.js';
|
||||
|
||||
const BANNER = `
|
||||
╔══════════════════════════════════════╗
|
||||
║ plan2code-bot v1.1.0 ║
|
||||
║ Autonomous Workflow Test Runner ║
|
||||
╚══════════════════════════════════════╝
|
||||
`;
|
||||
|
||||
const MAX_IMPLEMENT_PASSES = 10;
|
||||
|
||||
/** Skill name mapping: bot step name → installed skill directory name */
|
||||
const SKILL_MAP: Record<StepName, string> = {
|
||||
init: 'plan2code-init',
|
||||
plan: 'plan2code-1-plan',
|
||||
document: 'plan2code-2-document',
|
||||
implement: 'plan2code-3-implement',
|
||||
finalize: 'plan2code-4-finalize',
|
||||
};
|
||||
|
||||
/**
|
||||
* Install bot-friendly copies of plan2code skills into the project's .claude/skills/.
|
||||
* The global skills have `disable-model-invocation: true` which prevents the autonomous
|
||||
* session from invoking them via the Skill tool. We copy them with that flag removed
|
||||
* so the session can invoke the real workflow prompts instead of guessing.
|
||||
*/
|
||||
export function installSkillsForBot(projectDir: string): void {
|
||||
const userSkillsDir = path.join(
|
||||
process.env.HOME || process.env.USERPROFILE || '',
|
||||
'.claude',
|
||||
'skills',
|
||||
);
|
||||
const projectSkillsDir = path.join(projectDir, '.claude', 'skills');
|
||||
|
||||
for (const skillName of Object.values(SKILL_MAP)) {
|
||||
const srcFile = path.join(userSkillsDir, skillName, 'SKILL.md');
|
||||
if (!fs.existsSync(srcFile)) continue;
|
||||
|
||||
const content = fs.readFileSync(srcFile, 'utf-8');
|
||||
// Remove the disable-model-invocation line so the bot session can invoke the skill
|
||||
const patched = content.replace(/^disable-model-invocation:\s*true\n?/m, '');
|
||||
|
||||
const destDir = path.join(projectSkillsDir, skillName);
|
||||
fs.ensureDirSync(destDir);
|
||||
fs.writeFileSync(path.join(destDir, 'SKILL.md'), patched);
|
||||
}
|
||||
}
|
||||
|
||||
export function validateStepArtifacts(step: StepName, projectDir: string): { valid: boolean; missing: string[] } {
|
||||
const missing: string[] = [];
|
||||
|
||||
switch (step) {
|
||||
case 'init': {
|
||||
const agentsPath = path.join(projectDir, 'AGENTS.md');
|
||||
if (!fs.existsSync(agentsPath)) missing.push('AGENTS.md');
|
||||
break;
|
||||
}
|
||||
case 'plan': {
|
||||
const specsDir = path.join(projectDir, 'specs');
|
||||
if (!fs.existsSync(specsDir)) {
|
||||
missing.push('specs/ directory');
|
||||
} else {
|
||||
const entries = fs.readdirSync(specsDir, { withFileTypes: true });
|
||||
const specDirs = entries.filter((e) => e.isDirectory());
|
||||
const hasPlanDraft = specDirs.some((d) => {
|
||||
const files = fs.readdirSync(path.join(specsDir, d.name));
|
||||
return files.some((f) => /plan[-_]?draft/i.test(f));
|
||||
});
|
||||
if (!hasPlanDraft) missing.push('specs/*/PLAN-DRAFT-*.md');
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'document': {
|
||||
const specsDir = path.join(projectDir, 'specs');
|
||||
if (!fs.existsSync(specsDir)) {
|
||||
missing.push('specs/ directory');
|
||||
} else {
|
||||
const entries = fs.readdirSync(specsDir, { withFileTypes: true });
|
||||
const specDirs = entries.filter((e) => e.isDirectory());
|
||||
let foundOverview = false;
|
||||
let foundPhaseFile = false;
|
||||
for (const d of specDirs) {
|
||||
const specPath = path.join(specsDir, d.name);
|
||||
const files = fs.readdirSync(specPath);
|
||||
if (files.some((f) => f === 'overview.md')) foundOverview = true;
|
||||
if (files.some((f) => /^phase[-_]?\d+.*\.md$/i.test(f))) foundPhaseFile = true;
|
||||
// Also check phases/ subdirectory
|
||||
const phasesSubdir = path.join(specPath, 'phases');
|
||||
if (fs.existsSync(phasesSubdir)) {
|
||||
const subFiles = fs.readdirSync(phasesSubdir);
|
||||
if (subFiles.some((f) => /phase/i.test(f))) foundPhaseFile = true;
|
||||
}
|
||||
}
|
||||
if (!foundOverview) missing.push('specs/*/overview.md');
|
||||
if (!foundPhaseFile) missing.push('specs/*/phase-*.md files');
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'finalize': {
|
||||
const completedDir = path.join(projectDir, 'specs--completed');
|
||||
if (!fs.existsSync(completedDir)) {
|
||||
missing.push('specs--completed/ directory');
|
||||
} else {
|
||||
const entries = fs.readdirSync(completedDir, { withFileTypes: true });
|
||||
const specDirs = entries.filter((e) => e.isDirectory());
|
||||
if (specDirs.length === 0) missing.push('archived spec in specs--completed/');
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return { valid: missing.length === 0, missing };
|
||||
}
|
||||
|
||||
function detectMode(workDir: string): BotMode {
|
||||
const agentsPath = path.join(workDir, 'AGENTS.md');
|
||||
return fs.existsSync(agentsPath) ? 'enhancement' : 'new-project';
|
||||
}
|
||||
|
||||
function formatDuration(ms: number): string {
|
||||
const seconds = Math.floor(ms / 1000);
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const remainingSeconds = seconds % 60;
|
||||
if (minutes > 0) {
|
||||
return `${minutes}m ${remainingSeconds}s`;
|
||||
}
|
||||
return `${seconds}s`;
|
||||
}
|
||||
|
||||
async function runStep(
|
||||
step: StepName,
|
||||
config: BotConfig,
|
||||
state: BotState,
|
||||
): Promise<StepResult> {
|
||||
const spinner = ora({
|
||||
text: chalk.cyan(`Running ${step} step...`),
|
||||
spinner: 'dots',
|
||||
}).start();
|
||||
|
||||
const prompt = buildStepPrompt(step, config);
|
||||
|
||||
try {
|
||||
// Create observation collector
|
||||
const collector = new ObservationCollector(step);
|
||||
|
||||
const result = await runSession({
|
||||
prompt,
|
||||
config,
|
||||
step,
|
||||
maxTurns: step === 'implement' ? 80 : 50,
|
||||
collector,
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
spinner.succeed(
|
||||
chalk.green(`${step} completed in ${formatDuration(result.duration)}`)
|
||||
);
|
||||
|
||||
// Run evaluation
|
||||
spinner.text = chalk.cyan('Evaluating step quality...');
|
||||
spinner.start();
|
||||
|
||||
const evaluation = await evaluateStep(step, result.observations, config.projectDir);
|
||||
|
||||
spinner.succeed(
|
||||
chalk.cyan(`Evaluation complete: ${formatScore(evaluation.score)}`)
|
||||
);
|
||||
|
||||
// Display evaluation summary
|
||||
console.log(chalk.dim(` Score: ${formatScore(evaluation.score)}`));
|
||||
if (evaluation.strengths.length > 0) {
|
||||
console.log(chalk.green(` ✓ ${evaluation.strengths[0]}`));
|
||||
}
|
||||
if (evaluation.weaknesses.length > 0) {
|
||||
console.log(chalk.yellow(` ⚠ ${evaluation.weaknesses[0]}`));
|
||||
}
|
||||
|
||||
const stepResult: StepResult = {
|
||||
step,
|
||||
success: result.success,
|
||||
sessionId: result.sessionId,
|
||||
duration: result.duration,
|
||||
error: null,
|
||||
evaluation,
|
||||
observations: result.observations,
|
||||
};
|
||||
|
||||
return stepResult;
|
||||
} else {
|
||||
spinner.fail(chalk.red(`${step} failed after ${formatDuration(result.duration)}`));
|
||||
|
||||
return {
|
||||
step,
|
||||
success: false,
|
||||
sessionId: result.sessionId,
|
||||
duration: result.duration,
|
||||
error: 'Session failed',
|
||||
observations: result.observations,
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMsg = error instanceof Error ? error.message : String(error);
|
||||
spinner.fail(chalk.red(`${step} error: ${errorMsg}`));
|
||||
return {
|
||||
step,
|
||||
success: false,
|
||||
sessionId: null,
|
||||
duration: 0,
|
||||
error: errorMsg,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function formatScore(score: number): string {
|
||||
if (score >= 85) return chalk.green(`${score}/100`);
|
||||
if (score >= 70) return chalk.yellow(`${score}/100`);
|
||||
return chalk.red(`${score}/100`);
|
||||
}
|
||||
|
||||
export interface CLIOptions {
|
||||
idea?: string;
|
||||
resume?: boolean;
|
||||
}
|
||||
|
||||
/** Check if a step completed successfully in a saved state */
|
||||
function stepSucceeded(state: BotState, step: StepName): boolean {
|
||||
return state.steps.some((s) => s.step === step && s.success);
|
||||
}
|
||||
|
||||
export async function runCLI(options: CLIOptions = {}): Promise<void> {
|
||||
console.log(chalk.cyan(BANNER));
|
||||
|
||||
const workDir = process.cwd();
|
||||
|
||||
// Check for resumable state
|
||||
let resuming = false;
|
||||
let savedState: BotState | null = null;
|
||||
|
||||
if (options.resume) {
|
||||
savedState = findExistingState(workDir);
|
||||
if (savedState) {
|
||||
resuming = true;
|
||||
console.log(chalk.yellow('Resuming previous incomplete run...'));
|
||||
} else {
|
||||
console.log(chalk.dim('No previous state found, starting fresh.'));
|
||||
}
|
||||
}
|
||||
|
||||
const mode = resuming ? savedState!.config.mode : detectMode(workDir);
|
||||
|
||||
console.log(chalk.dim(`Working directory: ${workDir}`));
|
||||
console.log(chalk.dim(`Mode: ${mode === 'new-project' ? 'New Project' : 'Enhancement'}`));
|
||||
if (options.idea) {
|
||||
console.log(chalk.dim(`Idea seed: ${options.idea}`));
|
||||
}
|
||||
console.log('');
|
||||
|
||||
let ideaName: string;
|
||||
let ideaDescription: string;
|
||||
let projectDir: string;
|
||||
|
||||
if (resuming) {
|
||||
// Restore from saved state
|
||||
ideaName = savedState!.config.ideaName;
|
||||
ideaDescription = savedState!.config.ideaDescription;
|
||||
projectDir = savedState!.config.projectDir;
|
||||
console.log(chalk.green(`Restored idea: ${ideaName}`));
|
||||
console.log(chalk.dim(` ${ideaDescription}`));
|
||||
console.log('');
|
||||
} else {
|
||||
// Step 1: Generate idea
|
||||
const ideaSpinner = ora({
|
||||
text: chalk.cyan('Generating idea...'),
|
||||
spinner: 'dots',
|
||||
}).start();
|
||||
|
||||
try {
|
||||
if (mode === 'new-project') {
|
||||
const idea = await generateNewAppIdea(options.idea);
|
||||
ideaName = idea.name;
|
||||
ideaDescription = idea.description;
|
||||
} else {
|
||||
const idea = await generateEnhancementIdea(workDir, options.idea);
|
||||
ideaName = idea.name;
|
||||
ideaDescription = idea.description;
|
||||
}
|
||||
ideaSpinner.succeed(chalk.green(`Idea generated: ${ideaName}`));
|
||||
} catch (error) {
|
||||
ideaSpinner.fail(chalk.red('Failed to generate idea'));
|
||||
throw error;
|
||||
}
|
||||
|
||||
console.log(chalk.dim(` ${ideaDescription}`));
|
||||
console.log('');
|
||||
|
||||
// Determine project directory
|
||||
projectDir = mode === 'new-project'
|
||||
? path.join(workDir, ideaName)
|
||||
: workDir;
|
||||
|
||||
// Create project directory for new projects
|
||||
if (mode === 'new-project') {
|
||||
fs.ensureDirSync(projectDir);
|
||||
}
|
||||
}
|
||||
|
||||
// Install bot-friendly skills (without disable-model-invocation)
|
||||
installSkillsForBot(projectDir);
|
||||
console.log(chalk.dim('Installed plan2code skills for bot sessions'));
|
||||
|
||||
// Write IDEA.md (only if not resuming past plan step, since it gets moved)
|
||||
if (!resuming || !stepSucceeded(savedState!, 'plan')) {
|
||||
const ideaContent = `# ${ideaName}\n\n${ideaDescription}\n`;
|
||||
fs.writeFileSync(path.join(projectDir, 'IDEA.md'), ideaContent);
|
||||
console.log(chalk.dim(`Wrote IDEA.md to ${projectDir}`));
|
||||
}
|
||||
console.log('');
|
||||
|
||||
// Build config
|
||||
const config: BotConfig = {
|
||||
workDir,
|
||||
projectDir,
|
||||
ideaDescription,
|
||||
ideaName,
|
||||
mode,
|
||||
};
|
||||
|
||||
// Initialize or restore state
|
||||
const state: BotState = resuming
|
||||
? { ...savedState!, config }
|
||||
: {
|
||||
config,
|
||||
steps: [],
|
||||
currentStep: null,
|
||||
implementPasses: 0,
|
||||
allPhasesComplete: false,
|
||||
};
|
||||
|
||||
// Step 2: Run init (new project only)
|
||||
if (mode === 'new-project') {
|
||||
if (resuming && stepSucceeded(savedState!, 'init')) {
|
||||
console.log(chalk.dim('--- Init (skipped — previously succeeded) ---'));
|
||||
} else {
|
||||
console.log(chalk.bold('--- Init ---'));
|
||||
state.currentStep = 'init';
|
||||
const initResult = await runStep('init', config, state);
|
||||
state.steps.push(initResult);
|
||||
saveState(state);
|
||||
|
||||
if (!initResult.success) {
|
||||
console.log(chalk.red('\nInit failed. Aborting.'));
|
||||
printSummary(state, workDir, false);
|
||||
return;
|
||||
}
|
||||
const initValidation = validateStepArtifacts('init', projectDir);
|
||||
if (!initValidation.valid) {
|
||||
console.log(chalk.yellow(` ⚠ Missing artifacts: ${initValidation.missing.join(', ')}`));
|
||||
initResult.success = false;
|
||||
initResult.error = `Missing artifacts: ${initValidation.missing.join(', ')}`;
|
||||
console.log(chalk.red('\nInit artifacts missing. Aborting.'));
|
||||
printSummary(state, workDir, false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
console.log('');
|
||||
}
|
||||
|
||||
// Step 3: Plan
|
||||
if (resuming && stepSucceeded(savedState!, 'plan')) {
|
||||
console.log(chalk.dim('--- Plan (skipped — previously succeeded) ---'));
|
||||
} else {
|
||||
console.log(chalk.bold('--- Plan ---'));
|
||||
state.currentStep = 'plan';
|
||||
const planResult = await runStep('plan', config, state);
|
||||
state.steps.push(planResult);
|
||||
saveState(state);
|
||||
|
||||
if (!planResult.success) {
|
||||
console.log(chalk.red('\nPlan step failed. Aborting.'));
|
||||
printSummary(state, workDir, false);
|
||||
return;
|
||||
}
|
||||
const planValidation = validateStepArtifacts('plan', projectDir);
|
||||
if (!planValidation.valid) {
|
||||
console.log(chalk.yellow(` ⚠ Missing artifacts: ${planValidation.missing.join(', ')}`));
|
||||
planResult.success = false;
|
||||
planResult.error = `Missing artifacts: ${planValidation.missing.join(', ')}`;
|
||||
console.log(chalk.red('\nPlan artifacts missing. Aborting.'));
|
||||
printSummary(state, workDir, false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Move IDEA.md into the spec directory so it stays with its feature
|
||||
const ideaPath = path.join(projectDir, 'IDEA.md');
|
||||
if (fs.existsSync(ideaPath)) {
|
||||
const specEntries = fs.readdirSync(path.join(projectDir, 'specs'), { withFileTypes: true });
|
||||
const firstSpecDir = specEntries.find((e) => e.isDirectory());
|
||||
if (firstSpecDir) {
|
||||
const dest = path.join(projectDir, 'specs', firstSpecDir.name, 'IDEA.md');
|
||||
fs.moveSync(ideaPath, dest, { overwrite: true });
|
||||
console.log(chalk.dim(`Moved IDEA.md → specs/${firstSpecDir.name}/IDEA.md`));
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log('');
|
||||
|
||||
// Step 4: Document
|
||||
if (resuming && stepSucceeded(savedState!, 'document')) {
|
||||
console.log(chalk.dim('--- Document (skipped — previously succeeded) ---'));
|
||||
} else {
|
||||
console.log(chalk.bold('--- Document ---'));
|
||||
state.currentStep = 'document';
|
||||
const docResult = await runStep('document', config, state);
|
||||
state.steps.push(docResult);
|
||||
saveState(state);
|
||||
|
||||
if (!docResult.success) {
|
||||
console.log(chalk.red('\nDocument step failed. Aborting.'));
|
||||
printSummary(state, workDir, false);
|
||||
return;
|
||||
}
|
||||
const docValidation = validateStepArtifacts('document', projectDir);
|
||||
if (!docValidation.valid) {
|
||||
console.log(chalk.yellow(` ⚠ Missing artifacts: ${docValidation.missing.join(', ')}`));
|
||||
docResult.success = false;
|
||||
docResult.error = `Missing artifacts: ${docValidation.missing.join(', ')}`;
|
||||
console.log(chalk.red('\nDocument artifacts missing. Aborting.'));
|
||||
printSummary(state, workDir, false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
console.log('');
|
||||
|
||||
// Step 5: Implement (loop until all phases complete)
|
||||
if (resuming && savedState!.allPhasesComplete) {
|
||||
console.log(chalk.dim('--- Implement (skipped — all phases already complete) ---'));
|
||||
} else {
|
||||
console.log(chalk.bold('--- Implement ---'));
|
||||
while (state.implementPasses < MAX_IMPLEMENT_PASSES) {
|
||||
state.implementPasses++;
|
||||
state.currentStep = 'implement';
|
||||
|
||||
console.log(chalk.dim(` Pass ${state.implementPasses}/${MAX_IMPLEMENT_PASSES}`));
|
||||
const implResult = await runStep('implement', config, state);
|
||||
state.steps.push(implResult);
|
||||
saveState(state);
|
||||
|
||||
if (!implResult.success) {
|
||||
console.log(chalk.yellow(`\nImplement pass ${state.implementPasses} failed. Continuing...`));
|
||||
}
|
||||
|
||||
// Check if all phases are complete
|
||||
if (checkAllPhasesComplete(projectDir)) {
|
||||
state.allPhasesComplete = true;
|
||||
saveState(state);
|
||||
console.log(chalk.green(' All phases complete!'));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!state.allPhasesComplete && state.implementPasses >= MAX_IMPLEMENT_PASSES) {
|
||||
console.log(chalk.yellow(`\nMax implement passes (${MAX_IMPLEMENT_PASSES}) reached.`));
|
||||
}
|
||||
}
|
||||
console.log('');
|
||||
|
||||
// Step 6: Finalize
|
||||
if (resuming && stepSucceeded(savedState!, 'finalize')) {
|
||||
console.log(chalk.dim('--- Finalize (skipped — previously succeeded) ---'));
|
||||
} else {
|
||||
console.log(chalk.bold('--- Finalize ---'));
|
||||
|
||||
// Check quality gate: average score must be >= 60
|
||||
const evaluatedSteps = state.steps.filter((s) => s.evaluation);
|
||||
if (evaluatedSteps.length > 0) {
|
||||
const avgScore =
|
||||
evaluatedSteps.reduce((sum, s) => sum + (s.evaluation?.score ?? 0), 0) /
|
||||
evaluatedSteps.length;
|
||||
|
||||
console.log(chalk.dim(` Average quality score: ${formatScore(Math.round(avgScore))}`));
|
||||
|
||||
if (avgScore < 60) {
|
||||
console.log(
|
||||
chalk.red(
|
||||
'\n⚠ Quality gate failed: Average score is below 60. Please review and fix issues before finalizing.'
|
||||
)
|
||||
);
|
||||
console.log(chalk.dim(' Check specs/<feature>/BOT-EVALUATION.md for detailed feedback.'));
|
||||
printSummary(state, workDir, false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
state.currentStep = 'finalize';
|
||||
const finalizeResult = await runStep('finalize', config, state);
|
||||
state.steps.push(finalizeResult);
|
||||
saveState(state);
|
||||
|
||||
if (finalizeResult.success) {
|
||||
const finalValidation = validateStepArtifacts('finalize', projectDir);
|
||||
if (!finalValidation.valid) {
|
||||
console.log(chalk.yellow(` ⚠ Missing artifacts: ${finalValidation.missing.join(', ')}`));
|
||||
finalizeResult.success = false;
|
||||
finalizeResult.error = `Missing artifacts: ${finalValidation.missing.join(', ')}`;
|
||||
saveState(state);
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log('');
|
||||
|
||||
// Determine overall success
|
||||
const allSucceeded = state.steps.length > 0 && state.steps.every((s) => s.success);
|
||||
printSummary(state, workDir, allSucceeded);
|
||||
|
||||
// Clean up state file on full success
|
||||
if (allSucceeded) {
|
||||
deleteState(projectDir);
|
||||
console.log(chalk.dim('Cleaned up state file (run succeeded)'));
|
||||
} else {
|
||||
console.log(chalk.dim('State file preserved for resume (run incomplete)'));
|
||||
}
|
||||
console.log('');
|
||||
}
|
||||
|
||||
function printSummary(state: BotState, workDir: string, allSucceeded: boolean): void {
|
||||
const { ideaName, mode, projectDir } = state.config;
|
||||
|
||||
console.log(chalk.cyan('═══════════════════════════════════════'));
|
||||
console.log(chalk.bold(' Bot Run Summary'));
|
||||
console.log(chalk.cyan('═══════════════════════════════════════'));
|
||||
console.log(chalk.dim(` Project: ${ideaName}`));
|
||||
console.log(chalk.dim(` Mode: ${mode}`));
|
||||
console.log(chalk.dim(` Directory: ${projectDir}`));
|
||||
console.log('');
|
||||
|
||||
const totalDuration = state.steps.reduce((sum, s) => sum + s.duration, 0);
|
||||
const successCount = state.steps.filter((s) => s.success).length;
|
||||
|
||||
for (const step of state.steps) {
|
||||
const icon = step.success ? chalk.green('✓') : chalk.red('✗');
|
||||
const scoreText = step.evaluation
|
||||
? ` [${formatScore(step.evaluation.score)}]`
|
||||
: '';
|
||||
console.log(
|
||||
` ${icon} ${step.step.padEnd(12)} ${formatDuration(step.duration)}${scoreText}`
|
||||
);
|
||||
}
|
||||
|
||||
console.log('');
|
||||
|
||||
// Display average quality score if available
|
||||
const evaluatedSteps = state.steps.filter((s) => s.evaluation);
|
||||
if (evaluatedSteps.length > 0) {
|
||||
const avgScore =
|
||||
evaluatedSteps.reduce((sum, s) => sum + (s.evaluation?.score ?? 0), 0) /
|
||||
evaluatedSteps.length;
|
||||
console.log(
|
||||
chalk.dim(` Average quality: ${formatScore(Math.round(avgScore))}`)
|
||||
);
|
||||
}
|
||||
|
||||
console.log(
|
||||
chalk.dim(
|
||||
` Total: ${successCount}/${state.steps.length} steps succeeded in ${formatDuration(totalDuration)}`
|
||||
)
|
||||
);
|
||||
console.log(chalk.dim(` Implement passes: ${state.implementPasses}`));
|
||||
if (!allSucceeded) {
|
||||
console.log(
|
||||
chalk.dim(
|
||||
` State saved to: ${path.relative(workDir, projectDir)}/.plan2code-bot-state.json`
|
||||
)
|
||||
);
|
||||
}
|
||||
console.log('');
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
import { query } from '@anthropic-ai/claude-agent-sdk';
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import type { EvaluationResult, ExecutionObservation, StepName } from './types.js';
|
||||
import { getCriteriaForStep } from './prompts/evaluation-criteria.js';
|
||||
|
||||
/**
|
||||
* Evaluates a completed step using LLM-as-judge.
|
||||
* Returns honest quality assessment based on observations and artifacts.
|
||||
*/
|
||||
export async function evaluateStep(
|
||||
step: StepName,
|
||||
observations: ExecutionObservation,
|
||||
projectDir: string
|
||||
): Promise<EvaluationResult> {
|
||||
const criteria = getCriteriaForStep(step);
|
||||
const prompt = buildEvaluationPrompt(step, observations, criteria, projectDir);
|
||||
|
||||
try {
|
||||
// Query LLM with ability to inspect artifacts
|
||||
const session = query({
|
||||
prompt,
|
||||
options: {
|
||||
maxTurns: 30,
|
||||
cwd: projectDir,
|
||||
permissionMode: 'bypassPermissions',
|
||||
allowDangerouslySkipPermissions: true,
|
||||
allowedTools: ['Read', 'Glob', 'Grep'],
|
||||
systemPrompt:
|
||||
'You are a QA engineer evaluating completed work. Be thorough, honest, and constructive.',
|
||||
},
|
||||
});
|
||||
|
||||
let output = '';
|
||||
for await (const message of session) {
|
||||
if (message.type === 'assistant') {
|
||||
for (const block of message.message.content) {
|
||||
if (block.type === 'text') output += block.text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const evaluation = parseEvaluationOutput(output, step);
|
||||
|
||||
// Write evaluation files
|
||||
await writeEvaluationFiles(evaluation, observations, projectDir);
|
||||
|
||||
return evaluation;
|
||||
} catch (error) {
|
||||
console.warn('Evaluation failed, using fallback:', error);
|
||||
return createFallbackEvaluation(step, observations);
|
||||
}
|
||||
}
|
||||
|
||||
function buildEvaluationPrompt(
|
||||
step: StepName,
|
||||
observations: ExecutionObservation,
|
||||
criteria: ReturnType<typeof getCriteriaForStep>,
|
||||
projectDir: string
|
||||
): string {
|
||||
const duration = observations.endTime - observations.startTime;
|
||||
const durationSec = Math.floor(duration / 1000);
|
||||
|
||||
const toolsSummary = observations.tools
|
||||
.map((t) => `- ${t.toolName} (${new Date(t.timestamp).toISOString()})`)
|
||||
.join('\n');
|
||||
|
||||
const filesSummary = [
|
||||
...observations.filesCreated.map((f) => `CREATED: ${f}`),
|
||||
...observations.filesModified.map((f) => `MODIFIED: ${f}`),
|
||||
].join('\n');
|
||||
|
||||
const questionsSummary = observations.questionsAsked
|
||||
.map(
|
||||
(q) =>
|
||||
`Q: ${q.question}\nA: ${q.selectedAnswer}\nReasoning: ${q.llmReasoning}`
|
||||
)
|
||||
.join('\n\n');
|
||||
|
||||
return `You are a QA engineer evaluating the quality of a completed workflow step.
|
||||
|
||||
## Step Information
|
||||
- Step: ${step}
|
||||
- Duration: ${durationSec}s
|
||||
- Tools used: ${observations.tools.length}
|
||||
- Files created: ${observations.filesCreated.length}
|
||||
- Files modified: ${observations.filesModified.length}
|
||||
- Errors: ${observations.errors.length}
|
||||
|
||||
## Execution Details
|
||||
|
||||
### Tools Used
|
||||
${toolsSummary || '(none)'}
|
||||
|
||||
### Files Changed
|
||||
${filesSummary || '(none)'}
|
||||
|
||||
${observations.questionsAsked.length > 0 ? `### Questions & Decisions\n${questionsSummary}` : ''}
|
||||
|
||||
${observations.errors.length > 0 ? `### Errors Encountered\n${observations.errors.join('\n')}` : ''}
|
||||
|
||||
## Evaluation Criteria
|
||||
|
||||
**Key Artifacts Expected:**
|
||||
${criteria.keyArtifacts.map((a) => `- ${a}`).join('\n')}
|
||||
|
||||
**Quality Checks:**
|
||||
${criteria.qualityChecks.map((c) => `- ${c}`).join('\n')}
|
||||
|
||||
**Common Pitfalls to Watch For:**
|
||||
${criteria.commonPitfalls.map((p) => `- ${p}`).join('\n')}
|
||||
|
||||
**Scoring Guidance:**
|
||||
${criteria.scoringGuidance}
|
||||
|
||||
## Your Task
|
||||
|
||||
Evaluate this step honestly and thoroughly:
|
||||
|
||||
1. **Inspect the artifacts** using Read, Glob, and Grep tools
|
||||
2. **Check against quality criteria** listed above
|
||||
3. **Identify strengths and weaknesses** based on actual evidence
|
||||
4. **Provide constructive suggestions** for improvement
|
||||
5. **Assign an honest score** (0-100) following the guidance
|
||||
|
||||
Be critical but fair. Most work scores 70-85. Don't inflate scores.
|
||||
|
||||
## Response Format
|
||||
|
||||
SCORE: <number 0-100>
|
||||
|
||||
STRENGTHS:
|
||||
- <strength 1>
|
||||
- <strength 2>
|
||||
- <strength 3>
|
||||
|
||||
WEAKNESSES:
|
||||
- <weakness 1>
|
||||
- <weakness 2>
|
||||
|
||||
SUGGESTIONS:
|
||||
- <suggestion 1>
|
||||
- <suggestion 2>
|
||||
|
||||
CRITICAL_ISSUES:
|
||||
- <critical issue 1 (or "None")>
|
||||
|
||||
REASONING:
|
||||
<1-2 paragraphs explaining your evaluation, referencing specific files/evidence>
|
||||
|
||||
Provide honest, evidence-based evaluation.`;
|
||||
}
|
||||
|
||||
function parseEvaluationOutput(
|
||||
output: string,
|
||||
step: StepName
|
||||
): EvaluationResult {
|
||||
// Extract score
|
||||
const scoreMatch = output.match(/SCORE:\s*(\d+)/i);
|
||||
if (!scoreMatch) {
|
||||
console.warn(`Evaluation parser: no SCORE found in output (${output.length} chars). Defaulting to 50.`);
|
||||
}
|
||||
const score = scoreMatch ? parseInt(scoreMatch[1], 10) : 50;
|
||||
|
||||
// Extract sections
|
||||
const strengthsMatch = output.match(
|
||||
/STRENGTHS:\s*((?:- .+\n?)+)/i
|
||||
);
|
||||
const weaknessesMatch = output.match(
|
||||
/WEAKNESSES:\s*((?:- .+\n?)+)/i
|
||||
);
|
||||
const suggestionsMatch = output.match(
|
||||
/SUGGESTIONS:\s*((?:- .+\n?)+)/i
|
||||
);
|
||||
const criticalMatch = output.match(
|
||||
/CRITICAL_ISSUES:\s*((?:- .+\n?)+)/i
|
||||
);
|
||||
const reasoningMatch = output.match(/REASONING:\s*(.+?)(?=\n\n|$)/is);
|
||||
|
||||
const parseList = (text: string | undefined): string[] => {
|
||||
if (!text) return [];
|
||||
return text
|
||||
.split('\n')
|
||||
.map((line) => line.replace(/^-\s*/, '').trim())
|
||||
.filter((line) => line.length > 0 && !line.toLowerCase().includes('none'));
|
||||
};
|
||||
|
||||
return {
|
||||
step,
|
||||
score: Math.max(0, Math.min(100, score)),
|
||||
strengths: parseList(strengthsMatch?.[1]),
|
||||
weaknesses: parseList(weaknessesMatch?.[1]),
|
||||
suggestions: parseList(suggestionsMatch?.[1]),
|
||||
criticalIssues: parseList(criticalMatch?.[1]),
|
||||
timestamp: Date.now(),
|
||||
evaluatorModel: 'claude-sonnet-4-5',
|
||||
reasoning: reasoningMatch?.[1]?.trim() || 'No reasoning provided',
|
||||
};
|
||||
}
|
||||
|
||||
function createFallbackEvaluation(
|
||||
step: StepName,
|
||||
observations: ExecutionObservation
|
||||
): EvaluationResult {
|
||||
return {
|
||||
step,
|
||||
score: 50,
|
||||
strengths: ['Step completed'],
|
||||
weaknesses: ['Evaluation failed - using fallback'],
|
||||
suggestions: ['Re-run with evaluation enabled'],
|
||||
criticalIssues: ['Evaluation system unavailable'],
|
||||
timestamp: Date.now(),
|
||||
evaluatorModel: 'fallback',
|
||||
reasoning: 'Evaluation failed, using fallback. Cannot provide detailed assessment.',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the active spec subdirectory (e.g. specs/<feature>/).
|
||||
* Falls back to projectDir if no spec folder exists yet (e.g. during init).
|
||||
*/
|
||||
function resolveOutputDir(projectDir: string): string {
|
||||
const specsDir = path.join(projectDir, 'specs');
|
||||
if (fs.existsSync(specsDir)) {
|
||||
const entries = fs.readdirSync(specsDir, { withFileTypes: true });
|
||||
const firstSpec = entries.find((e) => e.isDirectory());
|
||||
if (firstSpec) {
|
||||
return path.join(specsDir, firstSpec.name);
|
||||
}
|
||||
}
|
||||
return projectDir;
|
||||
}
|
||||
|
||||
async function writeEvaluationFiles(
|
||||
evaluation: EvaluationResult,
|
||||
observations: ExecutionObservation,
|
||||
projectDir: string
|
||||
): Promise<void> {
|
||||
const outputDir = resolveOutputDir(projectDir);
|
||||
|
||||
// Write BOT-EVALUATION.md
|
||||
const evalContent = formatEvaluationMarkdown(evaluation);
|
||||
const evalPath = path.join(outputDir, 'BOT-EVALUATION.md');
|
||||
|
||||
if (fs.existsSync(evalPath)) {
|
||||
// Append to existing file
|
||||
const existing = fs.readFileSync(evalPath, 'utf-8');
|
||||
fs.writeFileSync(evalPath, existing + '\n\n---\n\n' + evalContent);
|
||||
} else {
|
||||
fs.writeFileSync(evalPath, evalContent);
|
||||
}
|
||||
|
||||
// Write BOT-NOTES.md
|
||||
const notesContent = formatObservationsMarkdown(observations);
|
||||
const notesPath = path.join(outputDir, 'BOT-NOTES.md');
|
||||
|
||||
if (fs.existsSync(notesPath)) {
|
||||
const existing = fs.readFileSync(notesPath, 'utf-8');
|
||||
fs.writeFileSync(notesPath, existing + '\n\n---\n\n' + notesContent);
|
||||
} else {
|
||||
fs.writeFileSync(notesPath, notesContent);
|
||||
}
|
||||
}
|
||||
|
||||
function formatEvaluationMarkdown(evaluation: EvaluationResult): string {
|
||||
return `# Evaluation: ${evaluation.step} Step
|
||||
|
||||
**Score:** ${evaluation.score}/100
|
||||
**Timestamp:** ${new Date(evaluation.timestamp).toISOString()}
|
||||
**Evaluator:** ${evaluation.evaluatorModel}
|
||||
|
||||
## Strengths
|
||||
${evaluation.strengths.map((s) => `- ${s}`).join('\n') || '(none)'}
|
||||
|
||||
## Weaknesses
|
||||
${evaluation.weaknesses.map((w) => `- ${w}`).join('\n') || '(none)'}
|
||||
|
||||
## Suggestions for Improvement
|
||||
${evaluation.suggestions.map((s) => `- ${s}`).join('\n') || '(none)'}
|
||||
|
||||
${evaluation.criticalIssues.length > 0 ? `## Critical Issues\n${evaluation.criticalIssues.map((i) => `- ${i}`).join('\n')}\n` : ''}
|
||||
|
||||
## Reasoning
|
||||
${evaluation.reasoning}`;
|
||||
}
|
||||
|
||||
function formatObservationsMarkdown(observations: ExecutionObservation): string {
|
||||
const duration = observations.endTime - observations.startTime;
|
||||
const durationSec = Math.floor(duration / 1000);
|
||||
|
||||
return `# Observations: ${observations.step} Step
|
||||
|
||||
**Duration:** ${durationSec}s
|
||||
**Tools Used:** ${observations.tools.length}
|
||||
**Files Created:** ${observations.filesCreated.length}
|
||||
**Files Modified:** ${observations.filesModified.length}
|
||||
**Errors:** ${observations.errors.length}
|
||||
|
||||
${observations.questionsAsked.length > 0 ? `## Questions Asked & Answers\n\n${observations.questionsAsked.map((q) => `### Question: "${q.question}"\n**Selected:** "${q.selectedAnswer}"\n**Reasoning:** ${q.llmReasoning}`).join('\n\n')}\n` : ''}
|
||||
|
||||
## Tool Usage
|
||||
${observations.tools.map((t) => `- ${t.toolName}`).join('\n')}
|
||||
|
||||
## Files Created
|
||||
${observations.filesCreated.map((f) => `- ${f}`).join('\n') || '(none)'}
|
||||
|
||||
## Files Modified
|
||||
${observations.filesModified.map((f) => `- ${f}`).join('\n') || '(none)'}
|
||||
|
||||
${observations.assistantMessages.length > 0 ? `## Assistant Output Summary\n${observations.assistantMessages.slice(0, 3).map((m) => `> ${m.substring(0, 100)}...`).join('\n')}\n` : ''}`;
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import { query } from '@anthropic-ai/claude-agent-sdk';
|
||||
|
||||
interface IdeaResult {
|
||||
name: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
function parseIdea(text: string): IdeaResult {
|
||||
const nameMatch = text.match(/NAME:\s*(.+)/i);
|
||||
const descMatch = text.match(/DESCRIPTION:\s*([\s\S]+?)(?:\n\n|$)/i);
|
||||
|
||||
const name = nameMatch?.[1]?.trim() ?? 'auto-project';
|
||||
const description = descMatch?.[1]?.trim() ?? text.trim();
|
||||
|
||||
// Ensure kebab-case
|
||||
const kebabName = name
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-|-$/g, '');
|
||||
|
||||
return { name: kebabName, description };
|
||||
}
|
||||
|
||||
export async function generateNewAppIdea(seed?: string): Promise<IdeaResult> {
|
||||
const categories = [
|
||||
'CLI tool',
|
||||
'single-page web app',
|
||||
'REST API service',
|
||||
'browser extension',
|
||||
'interactive data visualization dashboard',
|
||||
'terminal-based game',
|
||||
'real-time web app (using WebSockets)',
|
||||
'static site generator or theme',
|
||||
'browser-based game',
|
||||
'desktop utility (using Electron or Tauri)',
|
||||
'chat bot or conversational tool',
|
||||
'automation script or workflow tool',
|
||||
];
|
||||
const category = categories[Math.floor(Math.random() * categories.length)];
|
||||
|
||||
const seedClause = seed
|
||||
? `\n\nThe user provided this seed for inspiration. Stay closely aligned with the theme and intent of the seed — build on it, don't ignore it:\n"${seed}"`
|
||||
: '';
|
||||
|
||||
const prompt = `Generate a random, creative idea for a ${category}. The project should be achievable in a single coding session (1-2 hours) and should be interesting but not overly complex.
|
||||
|
||||
IMPORTANT: Be creative and diverse with your ideas. Avoid defaulting to developer-centric tools (git analyzers, code formatters, repo scanners, etc.) unless the category specifically calls for it. Think about ideas that would appeal to a broad audience — productivity, entertainment, education, health, finance, art, music, social, cooking, travel, fitness, etc.${seedClause}
|
||||
|
||||
Respond in EXACTLY this format (no other text):
|
||||
NAME: <kebab-case-project-name>
|
||||
DESCRIPTION: <2-3 sentence description of what the app does, its key features, and the tech stack to use>`;
|
||||
|
||||
const session = query({
|
||||
prompt,
|
||||
options: {
|
||||
maxTurns: 1,
|
||||
systemPrompt: 'You are a wildly creative project idea generator. You come up with surprising, fun, and diverse software project ideas spanning many domains — not just developer tools. Respond only in the exact format requested.',
|
||||
},
|
||||
});
|
||||
|
||||
let output = '';
|
||||
for await (const message of session) {
|
||||
if (message.type === 'assistant') {
|
||||
for (const block of message.message.content) {
|
||||
if (block.type === 'text') {
|
||||
output += block.text;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return parseIdea(output);
|
||||
}
|
||||
|
||||
export async function generateEnhancementIdea(projectDir: string, seed?: string): Promise<IdeaResult> {
|
||||
const seedClause = seed
|
||||
? `\n\nUse this as inspiration for the enhancement: "${seed}"`
|
||||
: '';
|
||||
|
||||
const prompt = `You are in a project directory. Scan the existing codebase to understand what it does, then propose a realistic enhancement (new feature, refactor, improvement, or extension).
|
||||
|
||||
Use the Read, Glob, and Grep tools to explore the project. Look at:
|
||||
- Package.json or similar config files for project info
|
||||
- Source files for current functionality
|
||||
- README or docs for context${seedClause}
|
||||
|
||||
Then respond in EXACTLY this format (no other text):
|
||||
NAME: <kebab-case-enhancement-name>
|
||||
DESCRIPTION: <2-3 sentence description of the enhancement, what it adds/changes, and why it would be valuable>`;
|
||||
|
||||
const session = query({
|
||||
prompt,
|
||||
options: {
|
||||
maxTurns: 8,
|
||||
cwd: projectDir,
|
||||
tools: { type: 'preset', preset: 'claude_code' },
|
||||
allowedTools: ['Read', 'Glob', 'Grep'],
|
||||
systemPrompt: { type: 'preset', preset: 'claude_code' },
|
||||
},
|
||||
});
|
||||
|
||||
let output = '';
|
||||
for await (const message of session) {
|
||||
if (message.type === 'assistant') {
|
||||
for (const block of message.message.content) {
|
||||
if (block.type === 'text') {
|
||||
output += block.text;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return parseIdea(output);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { runCLI } from './cli.js';
|
||||
export type { BotConfig, BotMode, BotState, StepName, StepResult } from './types.js';
|
||||
@@ -0,0 +1,243 @@
|
||||
import { query } from '@anthropic-ai/claude-agent-sdk';
|
||||
import type { BotConfig, ExecutionObservation, StepName } from './types.js';
|
||||
import type { ObservationCollector } from './observation-collector.js';
|
||||
|
||||
interface AskUserQuestionInput {
|
||||
questions: Array<{
|
||||
question: string;
|
||||
options: Array<{ label: string; description: string }>;
|
||||
multiSelect?: boolean;
|
||||
}>;
|
||||
}
|
||||
|
||||
interface IntelligentAnswers {
|
||||
answers: Record<string, string>;
|
||||
reasoning: Record<string, string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an intelligent responder that uses LLM-as-judge for ALL decisions.
|
||||
* Replaces the old hardcoded auto-responder logic.
|
||||
*/
|
||||
export function createIntelligentResponder(
|
||||
config: BotConfig,
|
||||
step: StepName,
|
||||
collector: ObservationCollector
|
||||
) {
|
||||
return async (
|
||||
toolName: string,
|
||||
input: Record<string, unknown>
|
||||
): Promise<{ behavior: 'allow'; updatedInput?: Record<string, unknown> } | { behavior: 'deny'; message: string }> => {
|
||||
// Record every tool invocation for observations
|
||||
collector.recordToolUse(toolName, input, undefined, true);
|
||||
|
||||
// Handle AskUserQuestion with LLM-generated answers
|
||||
if (toolName === 'AskUserQuestion') {
|
||||
const askInput = input as unknown as AskUserQuestionInput;
|
||||
|
||||
// Get current observations to provide context to LLM
|
||||
const observations = collector.getSnapshot();
|
||||
|
||||
// Generate answers using LLM-as-judge
|
||||
const answers = await generateIntelligentAnswers(
|
||||
askInput,
|
||||
observations,
|
||||
config,
|
||||
step
|
||||
);
|
||||
|
||||
// Record each question/answer pair for metrics
|
||||
for (const q of askInput.questions) {
|
||||
const answer = answers.answers[q.question];
|
||||
const reasoning = answers.reasoning[q.question] || 'No reasoning provided';
|
||||
collector.recordQuestion(q.question, q.options, answer, reasoning);
|
||||
}
|
||||
|
||||
return {
|
||||
behavior: 'allow',
|
||||
updatedInput: { ...input, answers: answers.answers },
|
||||
};
|
||||
}
|
||||
|
||||
// Allow all other tools
|
||||
return { behavior: 'allow' };
|
||||
};
|
||||
}
|
||||
|
||||
async function generateIntelligentAnswers(
|
||||
askInput: AskUserQuestionInput,
|
||||
observations: ExecutionObservation,
|
||||
config: BotConfig,
|
||||
step: StepName
|
||||
): Promise<IntelligentAnswers> {
|
||||
const prompt = buildDecisionPrompt(askInput, observations, config, step);
|
||||
|
||||
try {
|
||||
// Query LLM for decision (single turn, read-only tools)
|
||||
const session = query({
|
||||
prompt,
|
||||
options: {
|
||||
maxTurns: 1,
|
||||
cwd: config.projectDir,
|
||||
permissionMode: 'bypassPermissions',
|
||||
allowDangerouslySkipPermissions: true,
|
||||
allowedTools: ['Read', 'Glob', 'Grep'],
|
||||
systemPrompt: 'You are a QA engineer reviewing work-in-progress. Be thoughtful and honest.',
|
||||
},
|
||||
});
|
||||
|
||||
let output = '';
|
||||
for await (const message of session) {
|
||||
if (message.type === 'assistant') {
|
||||
for (const block of message.message.content) {
|
||||
if (block.type === 'text') output += block.text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return parseDecisionOutput(output, askInput);
|
||||
} catch (error) {
|
||||
console.warn('LLM decision failed, using fallback logic:', error);
|
||||
// Fallback to reasonable defaults if LLM fails
|
||||
return generateFallbackAnswers(askInput, step);
|
||||
}
|
||||
}
|
||||
|
||||
function buildDecisionPrompt(
|
||||
askInput: AskUserQuestionInput,
|
||||
observations: ExecutionObservation,
|
||||
config: BotConfig,
|
||||
step: StepName
|
||||
): string {
|
||||
const duration = observations.endTime - observations.startTime;
|
||||
const toolSummary = observations.tools
|
||||
.map((t) => `- ${t.toolName}`)
|
||||
.join('\n');
|
||||
const filesSummary = [
|
||||
...observations.filesCreated.map((f) => `CREATED: ${f}`),
|
||||
...observations.filesModified.map((f) => `MODIFIED: ${f}`),
|
||||
].join('\n');
|
||||
|
||||
const questionsText = askInput.questions
|
||||
.map((q, i) => {
|
||||
const optionsText = q.options
|
||||
.map((o, j) => ` ${j + 1}. ${o.label} - ${o.description}`)
|
||||
.join('\n');
|
||||
return `QUESTION ${i + 1}: ${q.question}\nOptions:\n${optionsText}`;
|
||||
})
|
||||
.join('\n\n');
|
||||
|
||||
return `You are a QA engineer reviewing a workflow step in progress.
|
||||
|
||||
## Context
|
||||
- Step: ${step}
|
||||
- Mode: ${config.mode}
|
||||
- Duration so far: ${Math.floor(duration / 1000)}s
|
||||
- Tools used: ${observations.tools.length}
|
||||
- Errors encountered: ${observations.errors.length}
|
||||
|
||||
## What's Happened So Far
|
||||
|
||||
### Tools Used
|
||||
${toolSummary || '(none yet)'}
|
||||
|
||||
### Files Changed
|
||||
${filesSummary || '(none yet)'}
|
||||
|
||||
${observations.errors.length > 0 ? `### Errors\n${observations.errors.join('\n')}` : ''}
|
||||
|
||||
## Questions to Answer
|
||||
|
||||
${questionsText}
|
||||
|
||||
## Your Task
|
||||
|
||||
You need to answer these questions as a thoughtful QA engineer would:
|
||||
1. Use Read, Glob, and Grep tools to inspect the current state of artifacts if needed
|
||||
2. Consider what you've observed (tools used, files created, errors)
|
||||
3. For each question, select the most appropriate answer
|
||||
4. Provide brief reasoning for your choice
|
||||
|
||||
Respond in this format:
|
||||
|
||||
QUESTION 1:
|
||||
ANSWER: <option label>
|
||||
REASONING: <1-2 sentences explaining your choice>
|
||||
|
||||
QUESTION 2:
|
||||
ANSWER: <option label>
|
||||
REASONING: <1-2 sentences>
|
||||
|
||||
Be honest. If work looks incomplete or problematic, don't approve it.
|
||||
If tests should be run but haven't been, don't skip them without good reason.
|
||||
Act like a real developer who cares about quality.`;
|
||||
}
|
||||
|
||||
function parseDecisionOutput(
|
||||
output: string,
|
||||
askInput: AskUserQuestionInput
|
||||
): IntelligentAnswers {
|
||||
const answers: Record<string, string> = {};
|
||||
const reasoning: Record<string, string> = {};
|
||||
|
||||
// Parse structured output
|
||||
const questionBlocks = output.split(/QUESTION \d+:/i).slice(1);
|
||||
|
||||
askInput.questions.forEach((q, i) => {
|
||||
const block = questionBlocks[i] || '';
|
||||
|
||||
const answerMatch = block.match(/ANSWER:\s*(.+?)(?=\n|$)/i);
|
||||
const reasoningMatch = block.match(/REASONING:\s*(.+?)(?=\n\n|$)/is);
|
||||
|
||||
const selectedLabel = answerMatch?.[1]?.trim() || '';
|
||||
|
||||
// Find matching option by label (case-insensitive partial match)
|
||||
const matchedOption = q.options.find(
|
||||
(opt) =>
|
||||
opt.label.toLowerCase().includes(selectedLabel.toLowerCase()) ||
|
||||
selectedLabel.toLowerCase().includes(opt.label.toLowerCase())
|
||||
);
|
||||
|
||||
answers[q.question] = matchedOption?.label || q.options[0].label;
|
||||
reasoning[q.question] = reasoningMatch?.[1]?.trim() || 'No reasoning provided';
|
||||
});
|
||||
|
||||
return { answers, reasoning };
|
||||
}
|
||||
|
||||
function generateFallbackAnswers(
|
||||
askInput: AskUserQuestionInput,
|
||||
step: StepName
|
||||
): IntelligentAnswers {
|
||||
// Simple fallback: pick first option for most questions
|
||||
// For approval questions, approve; for testing, skip
|
||||
const answers: Record<string, string> = {};
|
||||
const reasoning: Record<string, string> = {};
|
||||
|
||||
for (const q of askInput.questions) {
|
||||
const questionLower = q.question.toLowerCase();
|
||||
|
||||
if (questionLower.includes('approve') || questionLower.includes('proceed')) {
|
||||
const approveOption = q.options.find(
|
||||
(o) =>
|
||||
o.label.toLowerCase().includes('approve') ||
|
||||
o.label.toLowerCase().includes('yes')
|
||||
);
|
||||
answers[q.question] = approveOption?.label || q.options[0].label;
|
||||
reasoning[q.question] = 'Fallback approval (LLM unavailable)';
|
||||
} else if (questionLower.includes('test')) {
|
||||
const skipOption = q.options.find(
|
||||
(o) =>
|
||||
o.label.toLowerCase().includes('skip') ||
|
||||
o.label.toLowerCase().includes('none')
|
||||
);
|
||||
answers[q.question] = skipOption?.label || q.options[0].label;
|
||||
reasoning[q.question] = 'Fallback skip (LLM unavailable)';
|
||||
} else {
|
||||
answers[q.question] = q.options[0].label;
|
||||
reasoning[q.question] = 'Fallback first option (LLM unavailable)';
|
||||
}
|
||||
}
|
||||
|
||||
return { answers, reasoning };
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import type { ExecutionObservation, StepName } from './types.js';
|
||||
|
||||
/**
|
||||
* Collects detailed observations during step execution.
|
||||
* Tracks tool usage, messages, errors, file changes, and questions asked.
|
||||
*/
|
||||
export class ObservationCollector {
|
||||
private observations: ExecutionObservation;
|
||||
private recentToolKeys: Set<string> = new Set();
|
||||
|
||||
constructor(step: StepName) {
|
||||
this.observations = {
|
||||
step,
|
||||
startTime: Date.now(),
|
||||
endTime: 0,
|
||||
tools: [],
|
||||
assistantMessages: [],
|
||||
errors: [],
|
||||
filesCreated: [],
|
||||
filesModified: [],
|
||||
questionsAsked: [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Records a tool invocation with its input and output.
|
||||
* Deduplicates if the same tool+input is recorded from both canUseTool and the message stream.
|
||||
*/
|
||||
recordToolUse(
|
||||
toolName: string,
|
||||
input: Record<string, unknown>,
|
||||
output: unknown,
|
||||
allowed: boolean
|
||||
): void {
|
||||
// Deduplicate based on tool name + serialized input (within a short time window)
|
||||
const key = `${toolName}:${JSON.stringify(input)}`;
|
||||
if (this.recentToolKeys.has(key)) {
|
||||
return;
|
||||
}
|
||||
this.recentToolKeys.add(key);
|
||||
// Clean up old keys periodically to avoid unbounded growth
|
||||
if (this.recentToolKeys.size > 500) {
|
||||
const entries = [...this.recentToolKeys];
|
||||
this.recentToolKeys = new Set(entries.slice(entries.length - 250));
|
||||
}
|
||||
|
||||
this.observations.tools.push({
|
||||
toolName,
|
||||
input,
|
||||
output,
|
||||
timestamp: Date.now(),
|
||||
allowed,
|
||||
autoAnswered: toolName === 'AskUserQuestion',
|
||||
});
|
||||
|
||||
// Extract file paths from common tools
|
||||
if (toolName === 'Write' && input.file_path) {
|
||||
this.observations.filesCreated.push(input.file_path as string);
|
||||
}
|
||||
if (toolName === 'Edit' && input.file_path) {
|
||||
this.observations.filesModified.push(input.file_path as string);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Records messages from the session (assistant text, errors).
|
||||
*/
|
||||
recordMessage(message: any): void {
|
||||
if (message.type === 'assistant') {
|
||||
for (const block of message.message.content) {
|
||||
if (block.type === 'text') {
|
||||
this.observations.assistantMessages.push(block.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (message.type === 'error') {
|
||||
this.observations.errors.push(message.error?.message ?? 'Unknown error');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Records a question that was asked and the LLM-generated answer.
|
||||
*/
|
||||
recordQuestion(
|
||||
question: string,
|
||||
options: Array<{ label: string; description: string }>,
|
||||
selectedAnswer: string,
|
||||
llmReasoning: string
|
||||
): void {
|
||||
this.observations.questionsAsked.push({
|
||||
question,
|
||||
options,
|
||||
llmReasoning,
|
||||
selectedAnswer,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a snapshot of current observations (for real-time decision making).
|
||||
*/
|
||||
getSnapshot(): ExecutionObservation {
|
||||
return { ...this.observations, endTime: Date.now() };
|
||||
}
|
||||
|
||||
/**
|
||||
* Finalizes observations and returns the complete record.
|
||||
*/
|
||||
finalize(): ExecutionObservation {
|
||||
this.observations.endTime = Date.now();
|
||||
return { ...this.observations };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
import type { StepName } from '../types.js';
|
||||
|
||||
export interface StepEvaluationCriteria {
|
||||
step: StepName;
|
||||
keyArtifacts: string[];
|
||||
qualityChecks: string[];
|
||||
commonPitfalls: string[];
|
||||
scoringGuidance: string;
|
||||
}
|
||||
|
||||
export const EVALUATION_CRITERIA: Record<StepName, StepEvaluationCriteria> = {
|
||||
init: {
|
||||
step: 'init',
|
||||
keyArtifacts: ['AGENTS.md', 'IDEA.md'],
|
||||
qualityChecks: [
|
||||
'AGENTS.md exists with project name and description',
|
||||
'AGENTS.md includes a brief intro line and a Status section indicating the project is in planning phase',
|
||||
'AGENTS.md does NOT contain hallucinated architecture, commands, file structures, or tech stack details',
|
||||
'No .agents-docs/ directory was created (too early for detail files)',
|
||||
'No project scaffolding (package.json, dependencies, src/) was created',
|
||||
'IDEA.md exists with the project idea',
|
||||
],
|
||||
commonPitfalls: [
|
||||
'Hallucinating architecture or tech stack details before the plan step',
|
||||
'Creating .agents-docs/ detail files with invented content',
|
||||
'Scaffolding project files or installing dependencies prematurely',
|
||||
],
|
||||
scoringGuidance: `
|
||||
100 = Perfect: AGENTS.md stub with intro line, project name/description, and status section. IDEA.md present. Nothing else created.
|
||||
85-95 = Good but minor extra content beyond the expected stub format (e.g., an extra placeholder heading)
|
||||
70-84 = AGENTS.md exists but includes some hallucinated details (e.g., assumed tech stack or commands)
|
||||
50-69 = Significant hallucination (e.g., .agents-docs/ created with invented content, project scaffolded)
|
||||
<50 = Major problems (e.g., AGENTS.md missing, full project structure hallucinated)
|
||||
|
||||
The expected AGENTS.md format is: intro line, Project Overview (name + description), and a Status section. This is the target for a 100 score.
|
||||
`,
|
||||
},
|
||||
|
||||
plan: {
|
||||
step: 'plan',
|
||||
keyArtifacts: ['specs/*/PLAN-DRAFT-*.md', 'IDEA.md'],
|
||||
qualityChecks: [
|
||||
'Plan breaks work into clear, achievable phases',
|
||||
'Each phase has specific goals and deliverables',
|
||||
'Technical approach is appropriate',
|
||||
'Scope is realistic for the idea',
|
||||
'Dependencies between phases are identified',
|
||||
],
|
||||
commonPitfalls: [
|
||||
'Phases too vague ("polish the app")',
|
||||
'Missing specific tasks within phases',
|
||||
'No testing strategy mentioned',
|
||||
'Overly ambitious scope',
|
||||
'Missing file paths or specific actions',
|
||||
],
|
||||
scoringGuidance: `
|
||||
100 = Exceptional plan: detailed phases, realistic scope, clear tasks, testing included
|
||||
85-95 = Good plan with minor improvements possible (e.g., one phase could be more specific)
|
||||
70-84 = Acceptable but has vague sections or missing testing strategy
|
||||
50-69 = Significant issues (e.g., multiple vague phases, unrealistic scope)
|
||||
<50 = Major problems (e.g., no clear phases, plan doesn't match idea)
|
||||
|
||||
Most plans should score 70-85. Be critical of vague language.
|
||||
`,
|
||||
},
|
||||
|
||||
document: {
|
||||
step: 'document',
|
||||
keyArtifacts: ['specs/*/overview.md', 'specs/*/phase-*.md files'],
|
||||
qualityChecks: [
|
||||
'overview.md provides clear project summary',
|
||||
'Each phase file has specific tasks with checkboxes',
|
||||
'File paths are explicit (not generic)',
|
||||
'Dependencies between tasks are identified',
|
||||
'Technical details are specific',
|
||||
'Acceptance criteria are clear',
|
||||
],
|
||||
commonPitfalls: [
|
||||
'Tasks too generic ("implement feature X")',
|
||||
'Missing file paths',
|
||||
'No checkboxes or unclear task structure',
|
||||
'Missing dependencies',
|
||||
'Overly verbose or lacking specifics',
|
||||
],
|
||||
scoringGuidance: `
|
||||
100 = Exceptional documentation: specific tasks, explicit file paths, clear dependencies
|
||||
85-95 = Good documentation with minor vagueness in one or two tasks
|
||||
70-84 = Acceptable but multiple tasks lack specifics or file paths
|
||||
50-69 = Significant issues (e.g., many generic tasks, missing file paths)
|
||||
<50 = Major problems (e.g., tasks don't match plan, fundamentally vague)
|
||||
|
||||
Most documentation should score 70-85. Penalize generic language heavily.
|
||||
`,
|
||||
},
|
||||
|
||||
implement: {
|
||||
step: 'implement',
|
||||
keyArtifacts: ['actual code files', 'checked-off tasks in phase files'],
|
||||
qualityChecks: [
|
||||
'Phase tasks are being completed',
|
||||
'Code files are actually created/modified',
|
||||
'Implementation follows the documented plan',
|
||||
'No major errors blocking progress',
|
||||
'Tests are written (if applicable)',
|
||||
],
|
||||
commonPitfalls: [
|
||||
'Tasks marked complete but files not actually changed',
|
||||
'Implementation deviates significantly from plan',
|
||||
'Errors not addressed',
|
||||
'Skipping tests without justification',
|
||||
'Working on wrong phase',
|
||||
],
|
||||
scoringGuidance: `
|
||||
100 = Exceptional implementation: all tasks complete, code works, tests pass
|
||||
85-95 = Good implementation with minor issues or incomplete tests
|
||||
70-84 = Acceptable but some tasks incomplete or code has issues
|
||||
50-69 = Significant issues (e.g., many tasks incomplete, code doesn't work)
|
||||
<50 = Major problems (e.g., wrong phase, no actual work done)
|
||||
|
||||
Implementation scoring depends heavily on actual progress. Be realistic.
|
||||
`,
|
||||
},
|
||||
|
||||
finalize: {
|
||||
step: 'finalize',
|
||||
keyArtifacts: ['specs--completed/', 'README or docs', 'final code state'],
|
||||
qualityChecks: [
|
||||
'All phases are marked complete',
|
||||
'Spec moved to specs--completed/',
|
||||
'Documentation is updated',
|
||||
'Code is in working state',
|
||||
'No obvious loose ends',
|
||||
],
|
||||
commonPitfalls: [
|
||||
'Incomplete phases',
|
||||
'Missing specs--completed/ move',
|
||||
'Documentation not updated',
|
||||
'Code broken or incomplete',
|
||||
'Unrealistic self-assessment',
|
||||
],
|
||||
scoringGuidance: `
|
||||
100 = Exceptional finalization: everything complete, polished, documented
|
||||
85-95 = Good finalization with minor issues
|
||||
70-84 = Acceptable but some loose ends or documentation gaps
|
||||
50-69 = Significant issues (e.g., incomplete phases, broken code)
|
||||
<50 = Major problems (e.g., work not actually done, fundamentally incomplete)
|
||||
|
||||
Finalize scores should reflect overall project quality. Be honest.
|
||||
`,
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets evaluation criteria for a specific step.
|
||||
*/
|
||||
export function getCriteriaForStep(step: StepName): StepEvaluationCriteria {
|
||||
return EVALUATION_CRITERIA[step];
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { buildStepPrompt } from './step-instructions.js';
|
||||
import type { BotConfig } from '../types.js';
|
||||
|
||||
const config: BotConfig = {
|
||||
workDir: '/tmp/work',
|
||||
projectDir: '/tmp/work/my-app',
|
||||
ideaDescription: 'A todo app with drag-and-drop',
|
||||
ideaName: 'drag-todo',
|
||||
mode: 'new-project',
|
||||
};
|
||||
|
||||
describe('buildStepPrompt', () => {
|
||||
it('each step includes the autonomous preamble', () => {
|
||||
const steps = ['init', 'plan', 'document', 'implement', 'finalize'] as const;
|
||||
for (const step of steps) {
|
||||
const prompt = buildStepPrompt(step, config);
|
||||
expect(prompt).toContain('running autonomously');
|
||||
}
|
||||
});
|
||||
|
||||
it('init prompt includes /plan2code-init skill invocation', () => {
|
||||
const prompt = buildStepPrompt('init', config);
|
||||
expect(prompt).toContain('/plan2code-init');
|
||||
});
|
||||
|
||||
it('plan prompt includes /plan2code-1-plan skill invocation', () => {
|
||||
const prompt = buildStepPrompt('plan', config);
|
||||
expect(prompt).toContain('/plan2code-1-plan');
|
||||
});
|
||||
|
||||
it('document prompt includes /plan2code-2-document skill invocation', () => {
|
||||
const prompt = buildStepPrompt('document', config);
|
||||
expect(prompt).toContain('/plan2code-2-document');
|
||||
});
|
||||
|
||||
it('implement prompt instructs direct tool usage without skill invocation', () => {
|
||||
const prompt = buildStepPrompt('implement', config);
|
||||
expect(prompt).toContain('Do NOT use the Skill tool');
|
||||
});
|
||||
|
||||
it('finalize prompt includes /plan2code-4-finalize skill invocation', () => {
|
||||
const prompt = buildStepPrompt('finalize', config);
|
||||
expect(prompt).toContain('/plan2code-4-finalize');
|
||||
});
|
||||
|
||||
it('plan prompt includes project name and description', () => {
|
||||
const prompt = buildStepPrompt('plan', config);
|
||||
expect(prompt).toContain('drag-todo');
|
||||
expect(prompt).toContain('A todo app with drag-and-drop');
|
||||
});
|
||||
|
||||
it('init prompt includes project name and description', () => {
|
||||
const prompt = buildStepPrompt('init', config);
|
||||
expect(prompt).toContain('drag-todo');
|
||||
expect(prompt).toContain('A todo app with drag-and-drop');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,131 @@
|
||||
import type { BotConfig, StepName } from '../types.js';
|
||||
|
||||
const AUTONOMOUS_PREAMBLE = `You are running autonomously as part of an automated test pipeline. Do NOT pause for human input. When you encounter questions or approval gates, make reasonable decisions and proceed. If asked for confirmation, approve. If asked to choose, pick the most reasonable option. Complete the entire step without stopping.`;
|
||||
|
||||
export function buildStepPrompt(step: StepName, config: BotConfig): string {
|
||||
switch (step) {
|
||||
case 'init':
|
||||
return buildInitPrompt(config);
|
||||
case 'plan':
|
||||
return buildPlanPrompt(config);
|
||||
case 'document':
|
||||
return buildDocumentPrompt(config);
|
||||
case 'implement':
|
||||
return buildImplementPrompt(config);
|
||||
case 'finalize':
|
||||
return buildFinalizePrompt(config);
|
||||
}
|
||||
}
|
||||
|
||||
function buildInitPrompt(config: BotConfig): string {
|
||||
return `${AUTONOMOUS_PREAMBLE}
|
||||
|
||||
This is a brand new project with no existing code. Create a minimal stub AGENTS.md file and an IDEA.md file. Do NOT run the /plan2code-init skill — there is no codebase to analyze yet.
|
||||
|
||||
The project idea is: ${config.ideaDescription}
|
||||
The project name is: ${config.ideaName}
|
||||
|
||||
## What to create
|
||||
|
||||
### AGENTS.md
|
||||
Create a minimal stub with ONLY the following — do NOT invent architecture, tech stack details, commands, or file structures:
|
||||
|
||||
\`\`\`markdown
|
||||
# AGENTS.md
|
||||
|
||||
This file provides guidance to AI coding agents when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
**Name:** ${config.ideaName}
|
||||
**Description:** ${config.ideaDescription}
|
||||
|
||||
## Status
|
||||
This project is in the planning phase. Architecture, commands, and detailed documentation will be added after the plan and document steps are complete.
|
||||
\`\`\`
|
||||
|
||||
### IDEA.md
|
||||
If IDEA.md does not already exist, create it with the project name and description.
|
||||
|
||||
## Rules
|
||||
- Do NOT create .agents-docs/ or any detail files — there is nothing to document yet
|
||||
- Do NOT hallucinate architecture, dependencies, file structures, or tech stack choices
|
||||
- Do NOT install dependencies or scaffold project files
|
||||
- ONLY create the two files above`;
|
||||
}
|
||||
|
||||
function buildPlanPrompt(config: BotConfig): string {
|
||||
return `${AUTONOMOUS_PREAMBLE}
|
||||
|
||||
Run the /plan2code-1-plan skill to create a plan for this project.
|
||||
|
||||
Read the IDEA.md file first to understand the project. The project is: ${config.ideaDescription}
|
||||
|
||||
When making decisions during planning:
|
||||
- Set confidence levels reasonably high (85-95%)
|
||||
- Accept the generated tech stack without revision
|
||||
- Do not request additional reference files
|
||||
- Approve the plan when asked for sign-off
|
||||
- Keep scope small and achievable (3-4 phases max)
|
||||
- Use the project name: ${config.ideaName}`;
|
||||
}
|
||||
|
||||
function buildDocumentPrompt(config: BotConfig): string {
|
||||
return `${AUTONOMOUS_PREAMBLE}
|
||||
|
||||
Run the /plan2code-2-document skill to transform the plan into implementation specs.
|
||||
|
||||
Read the existing plan output in specs/ first to understand what was planned.
|
||||
|
||||
When making decisions:
|
||||
- Accept all generated documentation
|
||||
- Approve phase breakdowns and task lists
|
||||
- Do not request changes to the generated docs`;
|
||||
}
|
||||
|
||||
function buildImplementPrompt(config: BotConfig): string {
|
||||
return `${AUTONOMOUS_PREAMBLE}
|
||||
|
||||
You are a senior software engineer implementing a project phase. Do NOT use the Skill tool — implement directly using Read, Write, Edit, Glob, and Grep tools.
|
||||
|
||||
## Process
|
||||
|
||||
1. **Find the spec**: Read \`specs/*/overview.md\` to find the phase checklist
|
||||
2. **Pick the next phase**: Find the first phase marked \`[ ]\` (pending) or \`[/]\` (in-progress)
|
||||
3. **Read the phase file**: Read the corresponding \`phase-X.md\` from the same directory
|
||||
4. **Mark phase in-progress**: Update \`[ ]\` to \`[/]\` in overview.md
|
||||
5. **Implement each task sequentially**:
|
||||
- Read the task specification completely
|
||||
- Write the code using Write or Edit tools — create real files, not code blocks
|
||||
- Mark the task \`[x]\` in the phase file immediately after completing it
|
||||
6. **Complete the phase**: After all tasks, fill in the "Phase Completion Summary" in the phase file
|
||||
7. **Mark phase complete**: Update \`[/]\` to \`[x]\` in overview.md
|
||||
|
||||
## Rules
|
||||
|
||||
- Follow AGENTS.md if it exists
|
||||
- Implement specs EXACTLY — no creative additions or unsolicited improvements
|
||||
- Write task completion status (\`[x]\`) to disk immediately after each task — never batch
|
||||
- Only create files mentioned in the spec tasks
|
||||
- Use the specified file paths, function names, and structures from the spec
|
||||
- No placeholder code — fully implement every function
|
||||
- Match existing codebase conventions
|
||||
- Do NOT run git commands
|
||||
- Skip running tests unless explicitly listed as a phase task
|
||||
|
||||
## Project info
|
||||
- Project: ${config.ideaName}
|
||||
- Description: ${config.ideaDescription}
|
||||
- Project directory: ${config.projectDir}`;
|
||||
}
|
||||
|
||||
function buildFinalizePrompt(config: BotConfig): string {
|
||||
return `${AUTONOMOUS_PREAMBLE}
|
||||
|
||||
Run the /plan2code-4-finalize skill to validate and archive the completed project.
|
||||
|
||||
When making decisions:
|
||||
- Approve all documentation updates
|
||||
- Accept the completion summary
|
||||
- If asked for a rating or feedback, give 8/10 and positive feedback
|
||||
- Complete the archival process fully`;
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
|
||||
// Mock the SDK before importing session-runner
|
||||
vi.mock('@anthropic-ai/claude-agent-sdk', () => ({
|
||||
query: vi.fn(),
|
||||
}));
|
||||
|
||||
import { query } from '@anthropic-ai/claude-agent-sdk';
|
||||
import { runSession } from './session-runner.js';
|
||||
import { ObservationCollector } from './observation-collector.js';
|
||||
import type { BotConfig } from './types.js';
|
||||
|
||||
const config: BotConfig = {
|
||||
workDir: '/tmp/work',
|
||||
projectDir: '/tmp/work/my-app',
|
||||
ideaDescription: 'test app',
|
||||
ideaName: 'test-app',
|
||||
mode: 'new-project',
|
||||
};
|
||||
|
||||
describe('runSession', () => {
|
||||
it('returns success: false when output is empty', async () => {
|
||||
// Simulate a session that yields an assistant message with empty text
|
||||
const mockQuery = vi.mocked(query);
|
||||
mockQuery.mockReturnValue(
|
||||
(async function* () {
|
||||
yield {
|
||||
type: 'assistant' as const,
|
||||
session_id: 'sess-1',
|
||||
message: { content: [{ type: 'text' as const, text: '' }] },
|
||||
};
|
||||
yield {
|
||||
type: 'result' as const,
|
||||
session_id: 'sess-1',
|
||||
};
|
||||
})() as any,
|
||||
);
|
||||
|
||||
const collector = new ObservationCollector('init');
|
||||
const result = await runSession({
|
||||
prompt: 'do something',
|
||||
config,
|
||||
step: 'init',
|
||||
collector,
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.output.trim()).toBe('');
|
||||
expect(result.observations).toBeDefined();
|
||||
});
|
||||
|
||||
it('returns success: true when output has content', async () => {
|
||||
const mockQuery = vi.mocked(query);
|
||||
mockQuery.mockReturnValue(
|
||||
(async function* () {
|
||||
yield {
|
||||
type: 'assistant' as const,
|
||||
session_id: 'sess-2',
|
||||
message: { content: [{ type: 'text' as const, text: 'AGENTS.md has been created successfully' }] },
|
||||
};
|
||||
yield {
|
||||
type: 'result' as const,
|
||||
session_id: 'sess-2',
|
||||
};
|
||||
})() as any,
|
||||
);
|
||||
|
||||
const collector = new ObservationCollector('init');
|
||||
const result = await runSession({
|
||||
prompt: 'do something',
|
||||
config,
|
||||
step: 'init',
|
||||
collector,
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.output).toContain('AGENTS.md');
|
||||
expect(result.sessionId).toBe('sess-2');
|
||||
expect(result.observations).toBeDefined();
|
||||
expect(result.observations.step).toBe('init');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,77 @@
|
||||
import { query } from '@anthropic-ai/claude-agent-sdk';
|
||||
import { createIntelligentResponder } from './intelligent-responder.js';
|
||||
import type { BotConfig, ExecutionObservation, StepName } from './types.js';
|
||||
import type { ObservationCollector } from './observation-collector.js';
|
||||
|
||||
export interface SessionOptions {
|
||||
prompt: string;
|
||||
config: BotConfig;
|
||||
step: StepName;
|
||||
maxTurns?: number;
|
||||
collector: ObservationCollector;
|
||||
}
|
||||
|
||||
export interface SessionResult {
|
||||
sessionId: string | null;
|
||||
output: string;
|
||||
success: boolean;
|
||||
duration: number;
|
||||
observations: ExecutionObservation;
|
||||
}
|
||||
|
||||
export async function runSession(options: SessionOptions): Promise<SessionResult> {
|
||||
const { prompt, config, step, maxTurns = 50, collector } = options;
|
||||
const startTime = Date.now();
|
||||
let output = '';
|
||||
let sessionId: string | null = null;
|
||||
|
||||
try {
|
||||
const session = query({
|
||||
prompt,
|
||||
options: {
|
||||
cwd: config.projectDir,
|
||||
maxTurns,
|
||||
permissionMode: 'bypassPermissions',
|
||||
allowDangerouslySkipPermissions: true,
|
||||
canUseTool: createIntelligentResponder(config, step, collector),
|
||||
systemPrompt: { type: 'preset', preset: 'claude_code' },
|
||||
settingSources: ['project'],
|
||||
},
|
||||
});
|
||||
|
||||
for await (const message of session) {
|
||||
// Record all messages for observations
|
||||
collector.recordMessage(message);
|
||||
|
||||
if (message.type === 'assistant') {
|
||||
sessionId = message.session_id ?? sessionId;
|
||||
for (const block of message.message.content) {
|
||||
if (block.type === 'text') {
|
||||
output += block.text + '\n';
|
||||
} else if (block.type === 'tool_use') {
|
||||
// Capture tool invocations from the message stream as a fallback
|
||||
// in case canUseTool doesn't fire (e.g., Skill sub-sessions)
|
||||
collector.recordToolUse(
|
||||
block.name,
|
||||
block.input as Record<string, unknown>,
|
||||
undefined,
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if (message.type === 'result') {
|
||||
sessionId = message.session_id ?? sessionId;
|
||||
}
|
||||
}
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
const hasOutput = output.trim().length > 0;
|
||||
const observations = collector.finalize();
|
||||
return { sessionId, output, success: hasOutput, duration, observations };
|
||||
} catch (error) {
|
||||
const duration = Date.now() - startTime;
|
||||
const errorMsg = error instanceof Error ? error.message : String(error);
|
||||
const observations = collector.finalize();
|
||||
return { sessionId, output, success: false, duration, observations };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import fs from 'fs-extra';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import { checkAllPhasesComplete, detectStepCompletion } from './step-detector.js';
|
||||
|
||||
let tmpDir: string;
|
||||
|
||||
beforeEach(() => {
|
||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'p2c-test-'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.removeSync(tmpDir);
|
||||
});
|
||||
|
||||
// ── checkAllPhasesComplete ──────────────────────────────────────────
|
||||
|
||||
describe('checkAllPhasesComplete', () => {
|
||||
it('returns false when no specs/ directory exists', () => {
|
||||
expect(checkAllPhasesComplete(tmpDir)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when specs/ has no subdirectories', () => {
|
||||
fs.ensureDirSync(path.join(tmpDir, 'specs'));
|
||||
expect(checkAllPhasesComplete(tmpDir)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when spec dir exists but has no overview.md and no phase files', () => {
|
||||
// This is the false-positive bug we fixed — an empty spec dir should NOT be "complete"
|
||||
fs.ensureDirSync(path.join(tmpDir, 'specs', 'my-feature'));
|
||||
expect(checkAllPhasesComplete(tmpDir)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true when overview.md has all phases checked [x]', () => {
|
||||
const specDir = path.join(tmpDir, 'specs', 'my-feature');
|
||||
fs.ensureDirSync(specDir);
|
||||
fs.writeFileSync(
|
||||
path.join(specDir, 'overview.md'),
|
||||
`# Overview\n- [x] Phase 1: Setup\n- [x] Phase 2: Core\n- [x] Phase 3: Polish\n`,
|
||||
);
|
||||
expect(checkAllPhasesComplete(tmpDir)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when overview.md has unchecked [ ] phases', () => {
|
||||
const specDir = path.join(tmpDir, 'specs', 'my-feature');
|
||||
fs.ensureDirSync(specDir);
|
||||
fs.writeFileSync(
|
||||
path.join(specDir, 'overview.md'),
|
||||
`# Overview\n- [x] Phase 1: Setup\n- [ ] Phase 2: Core\n- [ ] Phase 3: Polish\n`,
|
||||
);
|
||||
expect(checkAllPhasesComplete(tmpDir)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true via fallback: phase-*.md files with all checked, no overview.md', () => {
|
||||
const specDir = path.join(tmpDir, 'specs', 'my-feature');
|
||||
fs.ensureDirSync(specDir);
|
||||
fs.writeFileSync(
|
||||
path.join(specDir, 'phase-1.md'),
|
||||
`# Phase 1\n- [x] Task A\n- [x] Task B\n`,
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(specDir, 'phase-2.md'),
|
||||
`# Phase 2\n- [x] Task C\n`,
|
||||
);
|
||||
expect(checkAllPhasesComplete(tmpDir)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false via fallback: phase-*.md with unchecked items', () => {
|
||||
const specDir = path.join(tmpDir, 'specs', 'my-feature');
|
||||
fs.ensureDirSync(specDir);
|
||||
fs.writeFileSync(
|
||||
path.join(specDir, 'phase-1.md'),
|
||||
`# Phase 1\n- [x] Task A\n- [ ] Task B\n`,
|
||||
);
|
||||
expect(checkAllPhasesComplete(tmpDir)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true via nested phases/ subdirectory fallback with all checked', () => {
|
||||
const specDir = path.join(tmpDir, 'specs', 'my-feature');
|
||||
const phasesDir = path.join(specDir, 'phases');
|
||||
fs.ensureDirSync(phasesDir);
|
||||
fs.writeFileSync(
|
||||
path.join(phasesDir, 'phase-1.md'),
|
||||
`# Phase 1\n- [x] Task A\n- [x] Task B\n`,
|
||||
);
|
||||
expect(checkAllPhasesComplete(tmpDir)).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false via nested phases/ with unchecked items', () => {
|
||||
const specDir = path.join(tmpDir, 'specs', 'my-feature');
|
||||
const phasesDir = path.join(specDir, 'phases');
|
||||
fs.ensureDirSync(phasesDir);
|
||||
fs.writeFileSync(
|
||||
path.join(phasesDir, 'phase-1.md'),
|
||||
`# Phase 1\n- [x] Task A\n- [ ] Task B\n`,
|
||||
);
|
||||
expect(checkAllPhasesComplete(tmpDir)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ── detectStepCompletion ────────────────────────────────────────────
|
||||
|
||||
describe('detectStepCompletion', () => {
|
||||
it('init: completed when output mentions agents.md created', () => {
|
||||
const result = detectStepCompletion('AGENTS.md has been created successfully', 'init');
|
||||
expect(result.completed).toBe(true);
|
||||
expect(result.nextStep).toBe('plan');
|
||||
});
|
||||
|
||||
it('init: not completed for unrelated output', () => {
|
||||
const result = detectStepCompletion('Hello world, nothing happened', 'init');
|
||||
expect(result.completed).toBe(false);
|
||||
expect(result.nextStep).toBeNull();
|
||||
});
|
||||
|
||||
it('plan: completed when plan is saved', () => {
|
||||
const result = detectStepCompletion('The plan has been saved and finalized', 'plan');
|
||||
expect(result.completed).toBe(true);
|
||||
expect(result.nextStep).toBe('document');
|
||||
});
|
||||
|
||||
it('plan: not completed for unrelated output', () => {
|
||||
const result = detectStepCompletion('Reading the codebase...', 'plan');
|
||||
expect(result.completed).toBe(false);
|
||||
expect(result.nextStep).toBeNull();
|
||||
});
|
||||
|
||||
it('document: completed when overview.md is mentioned', () => {
|
||||
const result = detectStepCompletion('Created overview.md with all phases', 'document');
|
||||
expect(result.completed).toBe(true);
|
||||
expect(result.nextStep).toBe('implement');
|
||||
});
|
||||
|
||||
it('document: not completed for unrelated output', () => {
|
||||
const result = detectStepCompletion('Thinking about the design...', 'document');
|
||||
expect(result.completed).toBe(false);
|
||||
expect(result.nextStep).toBeNull();
|
||||
});
|
||||
|
||||
it('implement: completed when phase is done', () => {
|
||||
const result = detectStepCompletion('Phase 1 is now complete!', 'implement');
|
||||
expect(result.completed).toBe(true);
|
||||
expect(result.nextStep).toBe('finalize');
|
||||
});
|
||||
|
||||
it('implement: not completed for unrelated output', () => {
|
||||
const result = detectStepCompletion('Working on some files', 'implement');
|
||||
expect(result.completed).toBe(false);
|
||||
expect(result.nextStep).toBeNull();
|
||||
});
|
||||
|
||||
it('finalize: completed when finalize is done', () => {
|
||||
const result = detectStepCompletion('Finalize step is complete and archived', 'finalize');
|
||||
expect(result.completed).toBe(true);
|
||||
expect(result.nextStep).toBeNull(); // last step
|
||||
});
|
||||
|
||||
it('finalize: not completed for unrelated output', () => {
|
||||
const result = detectStepCompletion('Just starting...', 'finalize');
|
||||
expect(result.completed).toBe(false);
|
||||
expect(result.nextStep).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,151 @@
|
||||
import fs from 'fs-extra';
|
||||
import path from 'path';
|
||||
import type { StepName } from './types.js';
|
||||
|
||||
export interface DetectionResult {
|
||||
completed: boolean;
|
||||
nextStep: StepName | null;
|
||||
needsAnotherImplementPass: boolean;
|
||||
}
|
||||
|
||||
const STEP_ORDER: StepName[] = ['init', 'plan', 'document', 'implement', 'finalize'];
|
||||
|
||||
export function detectStepCompletion(output: string, step: StepName): DetectionResult {
|
||||
const lowerOutput = output.toLowerCase();
|
||||
|
||||
let completed = false;
|
||||
|
||||
switch (step) {
|
||||
case 'init':
|
||||
completed = lowerOutput.includes('agents.md') && (
|
||||
lowerOutput.includes('created') ||
|
||||
lowerOutput.includes('generated') ||
|
||||
lowerOutput.includes('written')
|
||||
);
|
||||
break;
|
||||
|
||||
case 'plan':
|
||||
completed = lowerOutput.includes('plan') && (
|
||||
lowerOutput.includes('complete') ||
|
||||
lowerOutput.includes('approved') ||
|
||||
lowerOutput.includes('finalized') ||
|
||||
lowerOutput.includes('saved')
|
||||
);
|
||||
break;
|
||||
|
||||
case 'document':
|
||||
completed = lowerOutput.includes('overview.md') || (
|
||||
lowerOutput.includes('document') && lowerOutput.includes('complete')
|
||||
);
|
||||
break;
|
||||
|
||||
case 'implement':
|
||||
completed = lowerOutput.includes('phase') && (
|
||||
lowerOutput.includes('complete') ||
|
||||
lowerOutput.includes('done') ||
|
||||
lowerOutput.includes('finished')
|
||||
);
|
||||
break;
|
||||
|
||||
case 'finalize':
|
||||
completed = lowerOutput.includes('finalize') && (
|
||||
lowerOutput.includes('complete') ||
|
||||
lowerOutput.includes('archived') ||
|
||||
lowerOutput.includes('done')
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
// Determine next step
|
||||
const currentIdx = STEP_ORDER.indexOf(step);
|
||||
const nextStep = currentIdx < STEP_ORDER.length - 1 ? STEP_ORDER[currentIdx + 1] : null;
|
||||
|
||||
return {
|
||||
completed,
|
||||
nextStep: completed ? nextStep : null,
|
||||
needsAnotherImplementPass: false,
|
||||
};
|
||||
}
|
||||
|
||||
export function checkAllPhasesComplete(projectDir: string): boolean {
|
||||
const specsDir = path.join(projectDir, 'specs');
|
||||
|
||||
if (!fs.existsSync(specsDir)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const entries = fs.readdirSync(specsDir, { withFileTypes: true });
|
||||
const specDirs = entries.filter((e) => e.isDirectory());
|
||||
|
||||
if (specDirs.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let foundPhaseTracking = false;
|
||||
|
||||
for (const dir of specDirs) {
|
||||
const specPath = path.join(specsDir, dir.name);
|
||||
|
||||
// Try overview.md first
|
||||
const overviewPath = path.join(specPath, 'overview.md');
|
||||
if (fs.existsSync(overviewPath)) {
|
||||
foundPhaseTracking = true;
|
||||
if (hasUncheckedPhases(fs.readFileSync(overviewPath, 'utf-8'))) {
|
||||
return false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Fallback: look for phase-*.md or PHASE-*.md in the spec dir
|
||||
const phaseFiles = findPhaseFiles(specPath);
|
||||
if (phaseFiles.length > 0) {
|
||||
foundPhaseTracking = true;
|
||||
for (const pf of phaseFiles) {
|
||||
if (hasUncheckedPhases(fs.readFileSync(pf, 'utf-8'))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Fallback: look inside a phases/ subdirectory
|
||||
const phasesSubdir = path.join(specPath, 'phases');
|
||||
if (fs.existsSync(phasesSubdir)) {
|
||||
const subPhaseFiles = findPhaseFiles(phasesSubdir);
|
||||
if (subPhaseFiles.length > 0) {
|
||||
foundPhaseTracking = true;
|
||||
for (const pf of subPhaseFiles) {
|
||||
if (hasUncheckedPhases(fs.readFileSync(pf, 'utf-8'))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Only return true if we positively confirmed all phases are checked off
|
||||
return foundPhaseTracking;
|
||||
}
|
||||
|
||||
function hasUncheckedPhases(content: string): boolean {
|
||||
const lines = content.split('\n');
|
||||
|
||||
const phaseLines = lines.filter((line) =>
|
||||
line.match(/^[-*]\s*\[[ x]\]/i) && line.toLowerCase().includes('phase')
|
||||
);
|
||||
|
||||
if (phaseLines.length > 0) {
|
||||
return phaseLines.some((line) => line.includes('[ ]'));
|
||||
}
|
||||
|
||||
// No phase-specific checkboxes — check for any unchecked boxes
|
||||
return lines.some((line) => /^[-*]\s*\[ \]/.test(line));
|
||||
}
|
||||
|
||||
function findPhaseFiles(dir: string): string[] {
|
||||
if (!fs.existsSync(dir)) return [];
|
||||
const entries = fs.readdirSync(dir);
|
||||
return entries
|
||||
.filter((name) => /^phase[-_]?\d+.*\.md$/i.test(name))
|
||||
.map((name) => path.join(dir, name));
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
export type BotMode = 'new-project' | 'enhancement';
|
||||
|
||||
export interface BotConfig {
|
||||
workDir: string;
|
||||
projectDir: string;
|
||||
ideaDescription: string;
|
||||
ideaName: string;
|
||||
mode: BotMode;
|
||||
}
|
||||
|
||||
export type StepName = 'init' | 'plan' | 'document' | 'implement' | 'finalize';
|
||||
|
||||
export interface ToolObservation {
|
||||
toolName: string;
|
||||
input: Record<string, unknown>;
|
||||
output?: unknown;
|
||||
timestamp: number;
|
||||
allowed: boolean;
|
||||
autoAnswered?: boolean;
|
||||
}
|
||||
|
||||
export interface QuestionContext {
|
||||
question: string;
|
||||
options: Array<{ label: string; description: string }>;
|
||||
llmReasoning: string;
|
||||
selectedAnswer: string;
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
export interface ExecutionObservation {
|
||||
step: StepName;
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
tools: ToolObservation[];
|
||||
assistantMessages: string[];
|
||||
errors: string[];
|
||||
filesCreated: string[];
|
||||
filesModified: string[];
|
||||
questionsAsked: QuestionContext[];
|
||||
}
|
||||
|
||||
export interface EvaluationResult {
|
||||
step: StepName;
|
||||
score: number;
|
||||
strengths: string[];
|
||||
weaknesses: string[];
|
||||
suggestions: string[];
|
||||
criticalIssues: string[];
|
||||
timestamp: number;
|
||||
evaluatorModel: string;
|
||||
reasoning: string;
|
||||
}
|
||||
|
||||
export interface StepResult {
|
||||
step: StepName;
|
||||
success: boolean;
|
||||
sessionId: string | null;
|
||||
duration: number;
|
||||
error: string | null;
|
||||
evaluation?: EvaluationResult;
|
||||
observations?: ExecutionObservation;
|
||||
}
|
||||
|
||||
export interface BotState {
|
||||
config: BotConfig;
|
||||
steps: StepResult[];
|
||||
currentStep: StepName | null;
|
||||
implementPasses: number;
|
||||
allPhasesComplete: boolean;
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { defineConfig } from 'tsup';
|
||||
|
||||
export default defineConfig({
|
||||
entry: {
|
||||
'bin/plan2code-bot': 'src/bin/plan2code-bot.ts',
|
||||
index: 'src/index.ts',
|
||||
},
|
||||
format: ['esm'],
|
||||
dts: false,
|
||||
clean: true,
|
||||
sourcemap: true,
|
||||
banner: {
|
||||
js: '#!/usr/bin/env node',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
include: ['src/**/*.test.ts'],
|
||||
},
|
||||
});
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
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.
|
||||
> **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
|
||||
|
||||
@@ -34,7 +34,7 @@ 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
|
||||
4. Ask for JIRA ticket ID, agent selection, loop mode, and max iterations
|
||||
|
||||
## How It Works
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "plan2code-loop",
|
||||
"version": "1.0.0",
|
||||
"version": "1.6.2",
|
||||
"description": "Plan2Code Loop - Autonomous spec-driven implementation CLI",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
@@ -43,3 +43,4 @@
|
||||
"typescript": "^5.9.3"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,11 +23,11 @@ async function selectSpec(cwd: string = process.cwd()): Promise<string | null> {
|
||||
logger.info('');
|
||||
logger.info('To get started:');
|
||||
logger.info('');
|
||||
logger.info('1. Create a spec using `smarsh2code-1--plan`');
|
||||
logger.info('1. Create a spec using `plan2code-1-plan`');
|
||||
logger.info(' command in our AI Agent');
|
||||
logger.info('');
|
||||
logger.info('2. Come back here and run `smarsh2code-loop`');
|
||||
logger.info(' as an alternative to `smarsh2code-3--implement`');
|
||||
logger.info('2. Come back here and run `plan2code-loop`');
|
||||
logger.info(' as an alternative to `plan2code-3-implement`');
|
||||
logger.info('');
|
||||
return null;
|
||||
}
|
||||
@@ -67,8 +67,6 @@ async function selectSpec(cwd: string = process.cwd()): Promise<string | null> {
|
||||
async function selectAgent(): Promise<string> {
|
||||
const allAgents = agentRegistry.getAll();
|
||||
|
||||
|
||||
|
||||
const agentName = await select({
|
||||
message: 'Select AI agent:',
|
||||
choices: allAgents.map((agent) => ({
|
||||
@@ -108,6 +106,7 @@ async function selectMaxIterations(): Promise<number> {
|
||||
|
||||
return max;
|
||||
}
|
||||
|
||||
/**
|
||||
* Select loop mode: one task per loop or one phase per loop
|
||||
*/
|
||||
@@ -126,6 +125,7 @@ async function selectLoopMode(): Promise<LoopMode> {
|
||||
],
|
||||
default: 'task',
|
||||
});
|
||||
|
||||
return mode;
|
||||
}
|
||||
|
||||
@@ -176,7 +176,7 @@ async function handleExistingSession(
|
||||
export async function setupSession(
|
||||
stateManager: StateManager
|
||||
): Promise<SessionSetupResult | null> {
|
||||
// Show welcome
|
||||
// Show Planny welcome
|
||||
logger.welcome();
|
||||
|
||||
// Select spec
|
||||
@@ -224,7 +224,8 @@ export async function setupSession(
|
||||
model: 'default',
|
||||
maxIterations,
|
||||
specPath,
|
||||
timeout: 30,
|
||||
timeout: 3,
|
||||
maxRetries: 5,
|
||||
verbose: false,
|
||||
startedAt: new Date().toISOString(),
|
||||
currentIteration: 0,
|
||||
|
||||
@@ -64,9 +64,82 @@ export class Controller {
|
||||
});
|
||||
}
|
||||
|
||||
private async executeIteration(prompt: string): Promise<AgentExecutionResult> {
|
||||
const timeoutMs = this.config.timeout * 60 * 1000;
|
||||
private computeTimeoutMs(attempt: number): number {
|
||||
const baseMs = this.config.timeout * 60 * 1000;
|
||||
return baseMs + (attempt * 30 * 1000);
|
||||
}
|
||||
|
||||
private formatDuration(ms: number): string {
|
||||
const totalSec = Math.round(ms / 1000);
|
||||
const min = Math.floor(totalSec / 60);
|
||||
const sec = totalSec % 60;
|
||||
if (min === 0) return `${sec}s`;
|
||||
if (sec === 0) return `${min}m`;
|
||||
return `${min}m ${sec}s`;
|
||||
}
|
||||
|
||||
private async executeWithRetry(prompt: string, iterNum: number): Promise<AgentExecutionResult | 'fatal_timeout'> {
|
||||
const maxAttempts = (this.config.maxRetries ?? 5) + 1;
|
||||
|
||||
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
||||
const timeoutMs = this.computeTimeoutMs(attempt);
|
||||
|
||||
if (attempt > 0) {
|
||||
const prevTimeoutMs = this.computeTimeoutMs(attempt - 1);
|
||||
logger.warning(
|
||||
`Timed out after ${this.formatDuration(prevTimeoutMs)}, retrying (${attempt}/${maxAttempts - 1})...`
|
||||
);
|
||||
}
|
||||
|
||||
const spinnerBase = attempt > 0
|
||||
? `Waiting for AI Agent response (retry ${attempt}/${maxAttempts - 1})`
|
||||
: 'Waiting for AI Agent response (please be patient)';
|
||||
const spinner = logger.spinner(spinnerBase);
|
||||
const startTime = Date.now();
|
||||
|
||||
const elapsedInterval = setInterval(() => {
|
||||
const elapsed = Math.round((Date.now() - startTime) / 1000);
|
||||
spinner.text = `${spinnerBase} ... (${elapsed}s)`;
|
||||
}, 1000);
|
||||
|
||||
const result = await this.executeIteration(prompt, timeoutMs);
|
||||
clearInterval(elapsedInterval);
|
||||
spinner.stop();
|
||||
|
||||
// Cancelled or interrupted — return immediately, don't retry
|
||||
if (result.cancelled || this.interrupted) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Completed (success or error exit code) — return to caller
|
||||
if (!result.timedOut) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// Timed out — retry if attempts remain, otherwise fatal
|
||||
if (attempt < maxAttempts - 1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// All attempts exhausted
|
||||
logger.error(
|
||||
`Iteration ${iterNum} timed out on all ${maxAttempts} attempt${maxAttempts === 1 ? '' : 's'}. Stopping loop.`
|
||||
);
|
||||
const entry: IterationLogEntry = {
|
||||
iteration: iterNum,
|
||||
timestamp: new Date().toISOString(),
|
||||
duration: result.duration,
|
||||
exitCode: -1,
|
||||
status: 'timeout',
|
||||
};
|
||||
await this.stateManager.appendIterationLog(entry);
|
||||
return 'fatal_timeout';
|
||||
}
|
||||
|
||||
return 'fatal_timeout'; // unreachable, satisfies TS
|
||||
}
|
||||
|
||||
private async executeIteration(prompt: string, timeoutMs: number): Promise<AgentExecutionResult> {
|
||||
// Create new AbortController for this iteration
|
||||
this.abortController = new AbortController();
|
||||
|
||||
@@ -142,12 +215,14 @@ export class Controller {
|
||||
logger.info(`Please ensure the "${this.agent.config.command}" command is installed and available in your PATH.`);
|
||||
throw new Error(`Agent "${this.agent.config.displayName}" is not available. Please install it and try again.`);
|
||||
}
|
||||
|
||||
// Ensure git repo and .gitignore are set up before any iterations
|
||||
const gitReady = await ensureGitRepo(process.cwd());
|
||||
if (!gitReady) {
|
||||
throw new Error('Failed to initialize a git repository in the current working directory. Cannot start Plan2Code Loop.');
|
||||
}
|
||||
ensureGitignore(process.cwd());
|
||||
|
||||
const loopModeLabel = (this.config.loopMode || 'task') === 'phase' ? 'One phase per loop' : 'One task per loop';
|
||||
logger.header('Starting Plan2Code Loop');
|
||||
logger.info(`Agent: ${this.agent.config.displayName}`);
|
||||
@@ -177,19 +252,21 @@ export class Controller {
|
||||
// Build prompt - simple, just spec path and iteration info
|
||||
const prompt = await this.buildPrompt();
|
||||
|
||||
const spinner = logger.spinner('Waiting for AI Agent response (please be patient)');
|
||||
const startTime = Date.now();
|
||||
|
||||
// Update spinner with elapsed time every second
|
||||
const elapsedInterval = setInterval(() => {
|
||||
const elapsed = Math.round((Date.now() - startTime) / 1000);
|
||||
spinner.text = `Waiting for AI Agent response (please be patient) ... (${elapsed}s)`;
|
||||
}, 1000);
|
||||
|
||||
try {
|
||||
const result = await this.executeIteration(prompt);
|
||||
clearInterval(elapsedInterval);
|
||||
spinner.stop();
|
||||
const retryResult = await this.executeWithRetry(prompt, iterNum);
|
||||
|
||||
if (retryResult === 'fatal_timeout') {
|
||||
return {
|
||||
completed: false,
|
||||
iterations: this.config.currentIteration,
|
||||
exitReason: 'error',
|
||||
tasksCompleted: this.tasksCompleted,
|
||||
prereqsCompleted: this.prereqsCompleted,
|
||||
error: new Error(`Iteration ${iterNum} failed after all retry attempts`),
|
||||
};
|
||||
}
|
||||
|
||||
const result = retryResult;
|
||||
|
||||
// Check if cancelled
|
||||
if (result.cancelled || this.interrupted) {
|
||||
@@ -215,9 +292,11 @@ export class Controller {
|
||||
|
||||
// Branch completion handling based on loop mode
|
||||
const isPhaseMode = (this.config.loopMode || 'task') === 'phase';
|
||||
|
||||
if (isPhaseMode) {
|
||||
// Phase mode: parse ALL completion markers from output
|
||||
const allCompletions = checkForAllCompletions(result.stdout + result.stderr);
|
||||
|
||||
// Determine status
|
||||
let status: IterationLogEntry['status'] = 'running';
|
||||
if (allCompletions.tasks.length > 0 || allCompletions.loopComplete || allCompletions.phaseComplete) {
|
||||
@@ -229,10 +308,12 @@ export class Controller {
|
||||
} else if (result.exitCode !== 0) {
|
||||
status = 'error';
|
||||
}
|
||||
|
||||
// Log iteration with count of tasks
|
||||
const markerSummary = allCompletions.tasks.map(t => `${t.marker}: ${t.taskId}`).join(', ');
|
||||
const logEntry = this.createLogEntry(result, status, markerSummary || undefined);
|
||||
await this.stateManager.appendIterationLog(logEntry);
|
||||
|
||||
// Display each completed task
|
||||
const duration = Math.round(result.duration / 1000);
|
||||
for (const task of allCompletions.tasks) {
|
||||
@@ -259,6 +340,7 @@ export class Controller {
|
||||
logger.warning(blockInfo);
|
||||
}
|
||||
}
|
||||
|
||||
// Show phase-level summary
|
||||
if (allCompletions.tasks.length > 0) {
|
||||
const completedCount = allCompletions.tasks.filter(t => t.marker === 'TASK_COMPLETE').length;
|
||||
@@ -273,6 +355,7 @@ export class Controller {
|
||||
} else {
|
||||
logger.iteration(iterNum, this.config.maxIterations, `completed in ${duration}s`);
|
||||
}
|
||||
|
||||
// Verbose output
|
||||
if (this.config.verbose) {
|
||||
console.log();
|
||||
@@ -287,6 +370,7 @@ export class Controller {
|
||||
} else if (result.exitCode !== 0 && result.stderr.trim()) {
|
||||
logger.error(` ${result.stderr.trim().split('\n')[0]}`);
|
||||
}
|
||||
|
||||
// Handle LOOP_COMPLETE
|
||||
if (allCompletions.loopComplete) {
|
||||
this.onLoopComplete?.();
|
||||
@@ -302,42 +386,41 @@ export class Controller {
|
||||
}
|
||||
} else {
|
||||
// Task mode (default): existing single-marker logic
|
||||
const completion = checkForCompletion(result.stdout + result.stderr);
|
||||
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';
|
||||
// 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';
|
||||
}
|
||||
} 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);
|
||||
// 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);
|
||||
// Display iteration result with task info from completion marker
|
||||
this.displayIterationResult(result, iterNum, completion);
|
||||
|
||||
// Handle completion markers
|
||||
if (completion.completed) {
|
||||
const taskDisplay = this.formatTaskDisplay(completion);
|
||||
|
||||
// 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!');
|
||||
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 === 'PREREQ_COMPLETE' || completion.marker === 'PREREQ_ASSUMED') {
|
||||
this.prereqsCompleted++;
|
||||
await this.onTaskComplete?.({
|
||||
@@ -350,29 +433,29 @@ export class Controller {
|
||||
: completion.taskId ? `Prereq ${completion.taskId}` : 'Prerequisite';
|
||||
const verb = completion.marker === 'PREREQ_COMPLETE' ? 'Verified' : 'Assumed';
|
||||
logger.success(`${verb}: ${prereqDisplay}`);
|
||||
} else if (completion.marker === 'TASK_BLOCKED') {
|
||||
const blockInfo = completion.taskId
|
||||
? `Task ${completion.taskId} blocked: ${completion.reason || 'Unknown reason'}`
|
||||
: `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,
|
||||
} 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,
|
||||
prereqsCompleted: this.prereqsCompleted,
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -382,19 +465,14 @@ export class Controller {
|
||||
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) {
|
||||
if (result.exitCode !== 0) {
|
||||
// In phase mode, check if any tasks completed despite error exit code
|
||||
const hasCompletions = isPhaseMode
|
||||
? checkForAllCompletions(result.stdout + result.stderr).tasks.length > 0
|
||||
: checkForCompletion(result.stdout + result.stderr).completed;
|
||||
if (!hasCompletions) {
|
||||
logger.warning(`Iteration ${iterNum} exited with code ${result.exitCode}, continuing...`);
|
||||
logger.warning(`Iteration ${iterNum} exited with code ${result.exitCode}, continuing...`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const LOOP_PROMPT_TEMPLATE = `# PLAN-LOOP: Autonomous Task Implementation
|
||||
export const LOOP_PROMPT_TEMPLATE = `# PLAN2CODE-LOOP: Autonomous Task Implementation
|
||||
|
||||
## CRITICAL CONSTRAINT
|
||||
**IMPLEMENT EXACTLY ONE TASK PER ITERATION.**
|
||||
@@ -23,18 +23,19 @@ Find the FIRST unchecked task, implement ONLY that task, then STOP and report.
|
||||
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
|
||||
5. Find the FIRST unverified prerequisite (no "VERIFIED" or "ASSUMED" annotation)
|
||||
- If found, verify/complete it, then annotate "VERIFIED" or "ASSUMED: [reason]" inline
|
||||
6. Only if ALL prerequisites are verified or assumed, find the FIRST unchecked task (\`- [ ]\`)
|
||||
7. That is your ONE task - implement ONLY that task
|
||||
|
||||
## Checkbox States
|
||||
## Checkbox States (Task items only)
|
||||
- \`[ ]\` = incomplete/pending (do the FIRST one you find)
|
||||
- \`[x]\` = complete (skip)
|
||||
- \`[?]\` = assumed complete, couldn't verify (skip)
|
||||
- \`[!]\` = blocked (skip)
|
||||
|
||||
Prerequisites use plain bullets with inline annotations, not checkboxes.
|
||||
|
||||
## Implementation Steps
|
||||
1. Read and understand the single task
|
||||
2. Implement it completely
|
||||
@@ -66,7 +67,10 @@ Use only when ALL phases in overview.md are marked complete.
|
||||
|
||||
## Scratchpad Management
|
||||
|
||||
After completing each task, append to \`{{specPath}}/.plan2code-loop/scratchpad.md\`:
|
||||
After completing each task, add a new entry at the **bottom** of \`{{specPath}}/.plan2code-loop/scratchpad.md\`.
|
||||
Never edit, reorganize, or insert into existing content — only append new entries to the end of the file.
|
||||
|
||||
Each entry should include:
|
||||
- Task completed and Phase item reference
|
||||
- Key decisions made and reasoning
|
||||
- Files changed
|
||||
@@ -83,11 +87,14 @@ If key patterns or learnings were discovered, update \`./AGENTS.md\` if it exist
|
||||
|
||||
Remember: ONE TASK ONLY. Find it, implement it, mark it done, output TASK_COMPLETE with the task ID and description, then stop.
|
||||
`;
|
||||
|
||||
export const LOOP_PROMPT_TEMPLATE_PHASE = `# PLAN2CODE-LOOP: Autonomous Phase Implementation
|
||||
|
||||
## CRITICAL CONSTRAINT
|
||||
**IMPLEMENT ALL REMAINING TASKS IN THE CURRENT PHASE.**
|
||||
Find the first incomplete phase, then implement every remaining task in that phase before stopping.
|
||||
Complete each task fully before moving to the next task within the phase.
|
||||
|
||||
## Project Information
|
||||
- **Project Root:** \`{{projectRoot}}\`
|
||||
- **Spec Location:** \`{{specPath}}\`
|
||||
@@ -106,17 +113,19 @@ Complete each task fully before moving to the next task within the phase.
|
||||
2. Find the FIRST phase with an unchecked checkbox (\`- [ ]\` or \`- [/]\`)
|
||||
3. Read that phase's file (e.g., \`phase-1.md\`)
|
||||
4. Check the \`## Prerequisites\` section FIRST
|
||||
5. Complete ALL unchecked prerequisites (\`- [ ]\`) first, in order
|
||||
- Verify/complete each, then mark \`[x]\` or \`[?]\`
|
||||
6. Once ALL prerequisites are complete, implement ALL unchecked tasks in order
|
||||
5. Verify ALL unverified prerequisites first, in order
|
||||
- Annotate each "VERIFIED" or "ASSUMED: [reason]" inline
|
||||
6. Once ALL prerequisites are verified, implement ALL unchecked tasks in order
|
||||
7. Continue until every task in the phase is marked \`[x]\`
|
||||
|
||||
## Checkbox States
|
||||
## Checkbox States (Task items only)
|
||||
- \`[ ]\` = incomplete/pending
|
||||
- \`[x]\` = complete (skip)
|
||||
- \`[?]\` = assumed complete, couldn't verify (skip)
|
||||
- \`[!]\` = blocked (skip, note in scratchpad)
|
||||
|
||||
Prerequisites use plain bullets with inline annotations, not checkboxes.
|
||||
|
||||
## Implementation Steps (repeat for EACH task in the phase)
|
||||
1. Read and understand the task
|
||||
2. Implement it completely
|
||||
@@ -137,9 +146,13 @@ git commit -m "<commit message>"
|
||||
\`\`\`
|
||||
|
||||
**Commit message format:**
|
||||
- With JIRA ticket: Use \`-m "Task X.Y: description" -m "{{jiraTicketId}}" -m "AI Assisted"\` (three \`-m\` flags)
|
||||
- Without JIRA ticket: \`-m "Task X.Y: description" -m "AI Assisted"\` (two \`-m\` flags)
|
||||
- ALWAYS include the "AI Assisted" footer as the final \`-m\` flag
|
||||
\`\`\`
|
||||
git add -A
|
||||
git commit -m "Task X.Y: description" -m "{{jiraTicketId}}" -m "AI Assisted"
|
||||
\`\`\`
|
||||
- With JIRA ticket: three \`-m\` flags (description, ticket ID, AI Assisted)
|
||||
- Without JIRA ticket: two \`-m\` flags (description, AI Assisted)
|
||||
- ALWAYS include "AI Assisted" as the final \`-m\` flag
|
||||
|
||||
Replace X.Y with the actual task ID and description with a concise summary of what was implemented.
|
||||
|
||||
@@ -165,7 +178,10 @@ After ALL tasks in the phase are complete (or blocked), output:
|
||||
|
||||
## Scratchpad Management
|
||||
|
||||
After completing each task, append to \`{{specPath}}/.plan2code-loop/scratchpad.md\`:
|
||||
After completing each task, add a new entry at the **bottom** of \`{{specPath}}/.plan2code-loop/scratchpad.md\`.
|
||||
Never edit, reorganize, or insert into existing content — only append new entries to the end of the file.
|
||||
|
||||
Each entry should include:
|
||||
- Task completed and Phase item reference
|
||||
- Key decisions made and reasoning
|
||||
- Files changed
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
export type LoopMode = 'task' | 'phase';
|
||||
|
||||
export interface SessionConfig {
|
||||
agent: string; // "claude-code" | "copilot-cli"
|
||||
model: string; // Selected model
|
||||
maxIterations: number; // 5-50
|
||||
timeout: number; // Minutes per iteration
|
||||
timeout: number; // Base timeout in minutes per iteration attempt
|
||||
maxRetries: number; // Max retry attempts per iteration (timeout increments by 30s each retry)
|
||||
verbose: boolean;
|
||||
specPath: string; // Path to the spec directory
|
||||
startedAt: string; // ISO timestamp
|
||||
@@ -25,7 +27,8 @@ export type SessionState = 'new' | 'continue' | 'changed';
|
||||
|
||||
export const DEFAULT_CONFIG: Partial<SessionConfig> = {
|
||||
maxIterations: 100,
|
||||
timeout: 30,
|
||||
timeout: 3,
|
||||
maxRetries: 5,
|
||||
verbose: false,
|
||||
currentIteration: 0,
|
||||
loopMode: 'task',
|
||||
|
||||
@@ -54,6 +54,7 @@ export function checkForCompletion(output: string): CompletionCheckResult {
|
||||
taskName: prereqMatch[2].trim(),
|
||||
};
|
||||
}
|
||||
|
||||
// Check for PREREQ_ASSUMED with prereq info
|
||||
// Format: PREREQ_ASSUMED: P2.1 - Design approval (cannot verify)
|
||||
const assumedMatch = output.match(/PREREQ_ASSUMED[:\[\s]+([^\]:\-\s]+)[\]:\-\s]+(.+?)(?:\n|$)/i);
|
||||
@@ -65,6 +66,7 @@ export function checkForCompletion(output: string): CompletionCheckResult {
|
||||
taskName: assumedMatch[2].trim(),
|
||||
};
|
||||
}
|
||||
|
||||
// Check for TASK_BLOCKED with task info and reason
|
||||
// Format: TASK_BLOCKED: 1.1 - Reason why blocked
|
||||
const blockedWithTaskMatch = output.match(/TASK_BLOCKED[:\[\s]+(\d+\.\d+)[\]:\-\s]+(.+?)(?:\n|$)/i);
|
||||
@@ -94,6 +96,7 @@ export function checkForCompletion(output: string): CompletionCheckResult {
|
||||
|
||||
return { completed: false };
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract ALL completion markers from output (for phase mode).
|
||||
* Returns an array of all TASK_COMPLETE/TASK_BLOCKED markers found,
|
||||
@@ -107,12 +110,15 @@ export function checkForAllCompletions(output: string): {
|
||||
const tasks: CompletionCheckResult[] = [];
|
||||
let loopComplete = false;
|
||||
let phaseComplete = false;
|
||||
|
||||
if (output.includes('LOOP_COMPLETE')) {
|
||||
loopComplete = true;
|
||||
}
|
||||
|
||||
if (output.includes('PHASE_COMPLETE')) {
|
||||
phaseComplete = true;
|
||||
}
|
||||
|
||||
// Find all TASK_COMPLETE markers with task info
|
||||
// Format: TASK_COMPLETE: 1.1 - Task description
|
||||
const completeRegex = /TASK_COMPLETE[:\[\s]+(\d+\.\d+)[\]:\-\s]+(.+?)(?:\n|$)/gi;
|
||||
@@ -125,6 +131,7 @@ export function checkForAllCompletions(output: string): {
|
||||
taskName: match[2].trim(),
|
||||
});
|
||||
}
|
||||
|
||||
// Find all TASK_BLOCKED markers with task info
|
||||
const blockedRegex = /TASK_BLOCKED[:\[\s]+(\d+\.\d+)[\]:\-\s]+(.+?)(?:\n|$)/gi;
|
||||
while ((match = blockedRegex.exec(output)) !== null) {
|
||||
@@ -135,6 +142,7 @@ export function checkForAllCompletions(output: string): {
|
||||
reason: match[2].trim(),
|
||||
});
|
||||
}
|
||||
|
||||
// Find PREREQ_COMPLETE markers
|
||||
const prereqRegex = /PREREQ_COMPLETE[:\[\s]+([^\]:\-\s]+)[\]:\-\s]+(.+?)(?:\n|$)/gi;
|
||||
while ((match = prereqRegex.exec(output)) !== null) {
|
||||
@@ -145,6 +153,7 @@ export function checkForAllCompletions(output: string): {
|
||||
taskName: match[2].trim(),
|
||||
});
|
||||
}
|
||||
|
||||
// Find PREREQ_ASSUMED markers
|
||||
const assumedRegex = /PREREQ_ASSUMED[:\[\s]+([^\]:\-\s]+)[\]:\-\s]+(.+?)(?:\n|$)/gi;
|
||||
while ((match = assumedRegex.exec(output)) !== null) {
|
||||
@@ -155,5 +164,6 @@ export function checkForAllCompletions(output: string): {
|
||||
taskName: match[2].trim(),
|
||||
});
|
||||
}
|
||||
|
||||
return { tasks, loopComplete, phaseComplete };
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ export async function ensureGitRepo(cwd: string): Promise<boolean> {
|
||||
/**
|
||||
* Required entries for the .gitignore file
|
||||
*/
|
||||
const REQUIRED_GITIGNORE_ENTRIES = ['specs/', 'specs--completed/', 'nul', 'node_modules/'];
|
||||
const REQUIRED_GITIGNORE_ENTRIES = ['specs/', 'specs--completed/', '.plan2code-loop', '.plan2code-metrics', 'nul', 'node_modules/'];
|
||||
|
||||
/**
|
||||
* Ensure .gitignore exists with required entries
|
||||
@@ -109,7 +109,9 @@ export async function createTaskCommit(options: GitCommitOptions): Promise<boole
|
||||
// Build commit message
|
||||
let commitMessage = taskName;
|
||||
if (jiraTicketId) {
|
||||
commitMessage = `${taskName}\n\n${jiraTicketId}`;
|
||||
commitMessage = `${taskName}\n\n${jiraTicketId}\nAI Assisted`;
|
||||
} else {
|
||||
commitMessage = `${taskName}\n\nAI Assisted`;
|
||||
}
|
||||
|
||||
// Create the commit
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export { logger, MASCOT, type Logger } from './logger.js';
|
||||
export {
|
||||
checkForCompletion,
|
||||
checkForAllCompletions,
|
||||
type CompletionMarker,
|
||||
type CompletionCheckResult
|
||||
} from './completion.js';
|
||||
|
||||
@@ -117,7 +117,7 @@ export const logger = {
|
||||
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.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();
|
||||
},
|
||||
|
||||
@@ -6,7 +6,7 @@ export default defineConfig({
|
||||
index: 'src/index.ts',
|
||||
},
|
||||
format: ['esm'],
|
||||
dts: true,
|
||||
dts: false,
|
||||
clean: true,
|
||||
sourcemap: true,
|
||||
banner: {
|
||||
|
||||
@@ -0,0 +1,289 @@
|
||||
# plan2code-metrics
|
||||
|
||||
Recursive self-improvement toolchain for plan2code contributors. Collects metrics from completed project specs, aggregates them across runs and prompt generations, then uses AI to diagnose weak steps and propose targeted edits to the workflow prompts.
|
||||
|
||||
## Quick Reference
|
||||
|
||||
```bash
|
||||
# Install (one-time, from the plan2code root)
|
||||
node install.js # → "A" (Install All + dev tools) includes metrics
|
||||
# → or "M" (Metrics only) under the CUSTOM sub-menu
|
||||
|
||||
# After finishing any project spec (steps 1-4):
|
||||
cd your-project
|
||||
plan2code-metrics # → "Collect metrics" → pick your spec dir → done (5 sec)
|
||||
|
||||
# When you're curious or have 3+ runs:
|
||||
plan2code-metrics # → "View metrics status" to see the dashboard
|
||||
# → "Run analysis" for AI diagnosis of weak spots
|
||||
# → "Generate improvement proposal" for prompt edits
|
||||
# → "Review and apply" to patch src/plan2code-*.md
|
||||
```
|
||||
|
||||
**One habit:** collect after every finished spec. Everything else is on-demand.
|
||||
|
||||
### How Many Runs Do I Need?
|
||||
|
||||
| Runs | What You Get |
|
||||
|------|-------------|
|
||||
| **1** | Raw data and basic dashboard. Start here. |
|
||||
| **3+** | AI analysis unlocks (warns below 3 that results may be unreliable). Pattern detection starts working. |
|
||||
| **5-10+** | Averages stabilize. Generation-over-generation comparisons become meaningful. |
|
||||
|
||||
More is always better — each run adds a data point. You're looking for trends, not individual scores.
|
||||
|
||||
### The Feedback Loop
|
||||
|
||||
```
|
||||
Use plan2code on a project (steps 1-4)
|
||||
|
|
||||
v
|
||||
Collect metrics from the finished spec <-- do this every time
|
||||
|
|
||||
v
|
||||
Aggregate across runs (automatic)
|
||||
|
|
||||
v
|
||||
Analyze weak spots (AI-powered, 3+ runs)
|
||||
|
|
||||
v
|
||||
Generate prompt improvements
|
||||
|
|
||||
v
|
||||
Apply edits to src/plan2code-*.md
|
||||
|
|
||||
v
|
||||
Prompts have new SHA hashes = new "generation"
|
||||
|
|
||||
v
|
||||
Use improved prompts on next project
|
||||
|
|
||||
v
|
||||
Collect again, compare generations <-- the loop closes
|
||||
```
|
||||
|
||||
After applying edits, the prompt file SHA hashes change. The next collected run falls into a **new cohort** (generation). The dashboard then shows generation-over-generation deltas — "Did confidence go up? Did blockers decrease?" — so you can measure whether your edits actually helped.
|
||||
|
||||
---
|
||||
|
||||
## Installation
|
||||
|
||||
Requires Node.js >= 18.
|
||||
|
||||
### Via the plan2code installer (recommended)
|
||||
|
||||
From the plan2code repo root:
|
||||
|
||||
```bash
|
||||
node install.js
|
||||
```
|
||||
|
||||
Choose **A** (Install All + dev tools) to install prompts, loop CLI, bot, metrics, and status line together. Or choose **C** (Custom) then **M** (Metrics) to install just the metrics CLI.
|
||||
|
||||
The installer handles `npm install`, `npm run build`, and global linking automatically. After install, `plan2code-metrics` is available from any directory.
|
||||
|
||||
### Manual install
|
||||
|
||||
If you prefer to install manually or are developing on the metrics package:
|
||||
|
||||
```bash
|
||||
cd plan2code-metrics
|
||||
npm install
|
||||
npm run build
|
||||
npm link
|
||||
```
|
||||
|
||||
To run without linking:
|
||||
|
||||
```bash
|
||||
npm start
|
||||
# or
|
||||
node dist/bin/plan2code-metrics.js
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
Run `plan2code-metrics` from inside (or near) a plan2code repository. The CLI is fully interactive — no flags, all inputs collected via prompts.
|
||||
|
||||
```
|
||||
plan2code-metrics
|
||||
```
|
||||
|
||||
### Main Menu
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| **Collect metrics** | Parse a completed project spec and extract step-by-step metrics into a run JSON |
|
||||
| **Import run data** | Copy a run JSON from another project into the local metrics store |
|
||||
| **View metrics status** | Show aggregated metrics with health indicators and generation deltas |
|
||||
| **Run analysis** | AI-powered diagnosis of weak steps (requires Claude Code or Copilot CLI) |
|
||||
| **Generate improvement proposal** | AI generates surgical prompt edits based on a diagnosis |
|
||||
| **Review and apply** | Interactive diff review to accept/reject individual edits |
|
||||
|
||||
### Each Time You Finish a Spec
|
||||
|
||||
After completing Step 4 (finalize) on any project:
|
||||
|
||||
```bash
|
||||
cd your-project
|
||||
plan2code-metrics
|
||||
```
|
||||
|
||||
Pick **"Collect metrics"** and point it at your spec directory (`specs/<feature>/` or `specs--completed/<feature>/`). It reads your artifacts, extracts ~30 metrics, and writes a run JSON. Takes about 5 seconds.
|
||||
|
||||
The data stays local to that project in `.plan2code-metrics/runs/`. To aggregate across multiple projects, use **"Import"** to copy run files into one central location.
|
||||
|
||||
### Cross-Project Aggregation
|
||||
|
||||
If you use plan2code across several repos, you can consolidate all run data in one place:
|
||||
|
||||
```bash
|
||||
# From your central repo (e.g., the plan2code repo itself):
|
||||
plan2code-metrics
|
||||
# → "Import run data"
|
||||
# → Paste path to: /path/to/other-project/.plan2code-metrics/runs/run-xxx.json
|
||||
```
|
||||
|
||||
Imported runs are copied locally and included in all future aggregations, analyses, and comparisons.
|
||||
|
||||
## What It Measures
|
||||
|
||||
Each collection scrapes your spec files and extracts:
|
||||
|
||||
### Step 1 (Plan) — from `PLAN-DRAFT-*.md` and `PLAN-CONVERSATION-*.md`
|
||||
|
||||
- **Confidence score** — overall planning confidence percentage
|
||||
- **Confidence breakdown** — requirements, feasibility, integration, risk sub-scores
|
||||
- **Clarification rounds** — how many rounds of Q&A occurred
|
||||
- **Verification gaps found** — missing requirements caught during verification
|
||||
- **Functional/non-functional requirement counts** — FR- and NFR- headings
|
||||
- **Risk count** — risk table entries
|
||||
- **Phase count** — number of implementation phases planned
|
||||
|
||||
### Step 2 (Document) — from `overview.md` and `phase-*.md`
|
||||
|
||||
- **Total tasks** — checkbox count across all phases
|
||||
- **Tasks per phase** — distribution of work
|
||||
- **Phase count** — number of phase files
|
||||
- **Parallel groups** — parallel execution groups identified
|
||||
- **Requirement coverage** — coverage percentage if present
|
||||
- **Verification items** — verification checklist items added
|
||||
|
||||
### Step 3 (Implement) — from `.plan2code-loop/` data (loop mode only)
|
||||
|
||||
- **Task completion rate** — completed / total tasks
|
||||
- **Blocker count** — tasks marked TASK_BLOCKED
|
||||
- **Blocker categories** — normalized reasons for blocks
|
||||
- **Total iterations** — loop iteration count
|
||||
- **Avg iteration duration** — mean time per iteration
|
||||
- **Exit code distribution** — process exit codes
|
||||
- **Completion marker success rate** — valid markers / total iterations
|
||||
|
||||
### Step 4 (Finalize) — from `overview.md` and directory structure
|
||||
|
||||
- **Completion rate at audit** — tasks done at finalization time
|
||||
- **Verification failures** — tasks marked with `[!]`
|
||||
- **Documentation updates needed** — TODO/FIXME/update markers
|
||||
- **Archival success** — whether spec was moved to `specs--completed/`
|
||||
|
||||
### Health Targets
|
||||
|
||||
The dashboard compares key metrics against targets:
|
||||
|
||||
| Metric | Target | Direction |
|
||||
|--------|--------|-----------|
|
||||
| avg_confidence | 90 | >= |
|
||||
| avg_clarification_rounds | 2.0 | <= |
|
||||
| avg_verification_gaps_found | 2.0 | <= |
|
||||
| avg_parallel_groups | 0.5 | >= |
|
||||
| avg_task_completion_rate | 0.95 | >= |
|
||||
| avg_blocker_count | 1.5 | <= |
|
||||
| avg_completion_marker_success_rate | 0.95 | >= |
|
||||
| avg_verification_failures_found | 1.0 | <= |
|
||||
| archival_success_rate | 0.99 | >= |
|
||||
|
||||
Green checkmark = meeting target. Red X = below target (with the target shown).
|
||||
|
||||
## How It Works
|
||||
|
||||
### 1. Collect
|
||||
|
||||
Reads completed project artifacts from `specs/` or `specs--completed/` and extracts metrics for each workflow step. Output: `.plan2code-metrics/runs/run-<timestamp>.json`
|
||||
|
||||
### 2. Aggregate
|
||||
|
||||
Merges all run files into `aggregated.json`, grouped by **prompt generation** — a SHA fingerprint of the 8 workflow prompt files. Two runs that used identical prompt files belong to the same cohort.
|
||||
|
||||
### 3. Analyze
|
||||
|
||||
Sends aggregated metrics to an AI model with an analyst prompt. The AI produces a diagnosis identifying the weakest workflow steps and root causes. No edits are proposed at this stage.
|
||||
|
||||
Output: `.plan2code-metrics/proposals/<timestamp>-diagnosis.md`
|
||||
|
||||
### 4. Improve
|
||||
|
||||
Sends the diagnosis plus current prompt file contents to an AI model. The AI proposes surgical `old_text -> new_text` edits, each validated against:
|
||||
|
||||
- The `old_text` actually exists in the target file
|
||||
- The edited file stays under the 11,000 character limit
|
||||
|
||||
Output: `.plan2code-metrics/proposals/prop-<timestamp>.json`
|
||||
|
||||
### 5. Apply
|
||||
|
||||
Interactive diff review for each proposed edit. Accept, reject, or skip individual changes. Applied edits are patched directly into `src/plan2code-*.md`.
|
||||
|
||||
## Data Directory
|
||||
|
||||
All metrics data lives in `.plan2code-metrics/` (add to `.gitignore` if desired):
|
||||
|
||||
```
|
||||
.plan2code-metrics/
|
||||
├── runs/ # Individual run JSONs
|
||||
│ ├── run-20260215-120000-a1b2.json
|
||||
│ └── run-20260220-090000-c3d4.json
|
||||
├── aggregated.json # Merged cohort data
|
||||
└── proposals/ # Diagnoses and improvement proposals
|
||||
├── 20260220-diagnosis.md
|
||||
└── prop-20260220.json
|
||||
```
|
||||
|
||||
## AI Backends
|
||||
|
||||
The analysis and improvement steps require an AI agent. Two backends are supported:
|
||||
|
||||
| Backend | Command | Notes |
|
||||
|---------|---------|-------|
|
||||
| **Claude Code** | `claude` | Uses `--print` mode. Recommended. |
|
||||
| **GitHub Copilot CLI** | `copilot` | Uses stdin piping with `--allow-all-tools -s`. |
|
||||
|
||||
Model selection is interactive — choose from available models when prompted.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
src/
|
||||
├── bin/plan2code-metrics.ts # Entry point
|
||||
├── cli.ts # Interactive menu (main loop)
|
||||
├── types.ts # RunMetrics, PromptProposal, CohortMetrics, etc.
|
||||
├── collector.ts # Reads project artifacts → run JSON
|
||||
├── aggregator.ts # Merges runs → aggregated.json (cohort grouping)
|
||||
├── analyzer.ts # AI diagnosis via LLM invocation
|
||||
├── improver.ts # AI improvement proposal + validation
|
||||
├── applier.ts # Interactive diff review + file patching
|
||||
├── invoke-llm.ts # Unified LLM invocation (Claude Code / Copilot CLI)
|
||||
├── index.ts # Public API exports
|
||||
└── prompts/
|
||||
├── analyze.md # AI prompt template for diagnosis
|
||||
└── improve.md # AI prompt template for improvement proposals
|
||||
```
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
npm run dev # Watch mode (rebuilds on change)
|
||||
npm run build # Production build
|
||||
npm test # Run unit tests
|
||||
npm run test:watch # Watch mode tests
|
||||
npm start # Run the CLI
|
||||
```
|
||||
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"name": "plan2code-metrics",
|
||||
"version": "1.0.0",
|
||||
"description": "Plan2Code Metrics - Recursive self-improvement toolchain for plan2code contributors",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"bin": {
|
||||
"plan2code-metrics": "./dist/bin/plan2code-metrics.js"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"keywords": [
|
||||
"cli",
|
||||
"ai",
|
||||
"metrics",
|
||||
"plan2code",
|
||||
"contributor-tooling"
|
||||
],
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"dev": "tsup --watch",
|
||||
"start": "node dist/bin/plan2code-metrics.js",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"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",
|
||||
"vitest": "^3.1.1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { avg, rate, buildCohortKey, backfillPromptVersions } from './aggregator.js';
|
||||
import type { PromptVersions } from './types.js';
|
||||
|
||||
// ── avg() ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe('avg', () => {
|
||||
it('returns null for empty array', () => {
|
||||
expect(avg([])).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null for all-null/undefined values', () => {
|
||||
expect(avg([null, undefined, null])).toBeNull();
|
||||
});
|
||||
|
||||
it('computes correct average for valid numbers', () => {
|
||||
expect(avg([10, 20, 30])).toBe(20);
|
||||
});
|
||||
|
||||
it('filters out null/undefined/NaN from mixed arrays', () => {
|
||||
expect(avg([10, null, 20, undefined, NaN, 30])).toBe(20);
|
||||
});
|
||||
});
|
||||
|
||||
// ── rate() ────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe('rate', () => {
|
||||
it('returns null for empty array', () => {
|
||||
expect(rate([])).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null for all-null values', () => {
|
||||
expect(rate([null, null])).toBeNull();
|
||||
});
|
||||
|
||||
it('returns 1.0 for all-true', () => {
|
||||
expect(rate([true, true, true])).toBe(1.0);
|
||||
});
|
||||
|
||||
it('returns 0.0 for all-false', () => {
|
||||
expect(rate([false, false, false])).toBe(0.0);
|
||||
});
|
||||
|
||||
it('computes correct rate for mixed true/false', () => {
|
||||
expect(rate([true, false, true, false])).toBe(0.5);
|
||||
});
|
||||
|
||||
it('filters out null/undefined from mixed arrays', () => {
|
||||
expect(rate([true, null, false, undefined])).toBe(0.5);
|
||||
});
|
||||
});
|
||||
|
||||
// ── backfillPromptVersions() ──────────────────────────────────────────────────
|
||||
|
||||
const FULL_VERSIONS: PromptVersions = {
|
||||
plan: 'sha256:aaa',
|
||||
revise_plan: 'sha256:bbb',
|
||||
document: 'sha256:ccc',
|
||||
implement: 'sha256:ddd',
|
||||
finalize: 'sha256:eee',
|
||||
init: 'sha256:fff',
|
||||
init_update: 'sha256:ggg',
|
||||
quick_task: 'sha256:hhh',
|
||||
};
|
||||
|
||||
describe('backfillPromptVersions', () => {
|
||||
it('returns all 8 fields with sentinels when given empty-ish object', () => {
|
||||
const result = backfillPromptVersions({} as PromptVersions);
|
||||
expect(Object.keys(result)).toHaveLength(8);
|
||||
for (const val of Object.values(result)) {
|
||||
expect(val).toBe('sha256:missing');
|
||||
}
|
||||
});
|
||||
|
||||
it('preserves existing values, fills missing with sha256:missing', () => {
|
||||
const partial = { plan: 'sha256:aaa', implement: 'sha256:ddd' } as PromptVersions;
|
||||
const result = backfillPromptVersions(partial);
|
||||
expect(result.plan).toBe('sha256:aaa');
|
||||
expect(result.implement).toBe('sha256:ddd');
|
||||
expect(result.revise_plan).toBe('sha256:missing');
|
||||
expect(result.document).toBe('sha256:missing');
|
||||
expect(result.finalize).toBe('sha256:missing');
|
||||
expect(result.init).toBe('sha256:missing');
|
||||
expect(result.init_update).toBe('sha256:missing');
|
||||
expect(result.quick_task).toBe('sha256:missing');
|
||||
});
|
||||
|
||||
it('returns unchanged object when all 8 fields present', () => {
|
||||
const result = backfillPromptVersions(FULL_VERSIONS);
|
||||
expect(result).toEqual(FULL_VERSIONS);
|
||||
});
|
||||
});
|
||||
|
||||
// ── buildCohortKey() ──────────────────────────────────────────────────────────
|
||||
|
||||
describe('buildCohortKey', () => {
|
||||
it('returns 12-char hex string', () => {
|
||||
const key = buildCohortKey(FULL_VERSIONS);
|
||||
expect(key).toMatch(/^[0-9a-f]{12}$/);
|
||||
});
|
||||
|
||||
it('is deterministic (same input → same output)', () => {
|
||||
const key1 = buildCohortKey(FULL_VERSIONS);
|
||||
const key2 = buildCohortKey(FULL_VERSIONS);
|
||||
expect(key1).toBe(key2);
|
||||
});
|
||||
|
||||
it('old 4-field run with backfill sentinels produces same key as raw 4-field object', () => {
|
||||
// Simulate an old run that only had 4 fields
|
||||
const oldRun = {
|
||||
plan: 'sha256:aaa',
|
||||
implement: 'sha256:ddd',
|
||||
document: 'sha256:ccc',
|
||||
finalize: 'sha256:eee',
|
||||
} as PromptVersions;
|
||||
|
||||
// After backfill, the missing fields get 'sha256:missing'
|
||||
const backfilled = backfillPromptVersions(oldRun);
|
||||
|
||||
// buildCohortKey filters out 'sha256:missing', so both should match
|
||||
const keyDirect = buildCohortKey(oldRun);
|
||||
const keyBackfilled = buildCohortKey(backfilled);
|
||||
expect(keyDirect).toBe(keyBackfilled);
|
||||
});
|
||||
|
||||
it('different prompt versions → different keys', () => {
|
||||
const altered = { ...FULL_VERSIONS, plan: 'sha256:zzz' };
|
||||
expect(buildCohortKey(FULL_VERSIONS)).not.toBe(buildCohortKey(altered));
|
||||
});
|
||||
|
||||
it('key is independent of field insertion order', () => {
|
||||
const ordered: PromptVersions = {
|
||||
plan: 'sha256:aaa',
|
||||
revise_plan: 'sha256:bbb',
|
||||
document: 'sha256:ccc',
|
||||
implement: 'sha256:ddd',
|
||||
finalize: 'sha256:eee',
|
||||
init: 'sha256:fff',
|
||||
init_update: 'sha256:ggg',
|
||||
quick_task: 'sha256:hhh',
|
||||
};
|
||||
const reversed: PromptVersions = {
|
||||
quick_task: 'sha256:hhh',
|
||||
init_update: 'sha256:ggg',
|
||||
init: 'sha256:fff',
|
||||
finalize: 'sha256:eee',
|
||||
implement: 'sha256:ddd',
|
||||
document: 'sha256:ccc',
|
||||
revise_plan: 'sha256:bbb',
|
||||
plan: 'sha256:aaa',
|
||||
};
|
||||
expect(buildCohortKey(ordered)).toBe(buildCohortKey(reversed));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,219 @@
|
||||
/**
|
||||
* aggregator.ts
|
||||
* Merges all run JSON files into aggregated.json, grouped by prompt generation (SHA fingerprint).
|
||||
*/
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import crypto from 'crypto';
|
||||
import type { RunMetrics, AggregatedMetrics, CohortMetrics, PromptVersions } from './types.js';
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
export function avg(values: (number | null | undefined)[]): number | null {
|
||||
const valid = values.filter((v): v is number => v != null && !isNaN(v));
|
||||
if (valid.length === 0) return null;
|
||||
return valid.reduce((a, b) => a + b, 0) / valid.length;
|
||||
}
|
||||
|
||||
export function rate(values: (boolean | null | undefined)[]): number | null {
|
||||
const valid = values.filter((v): v is boolean => v != null);
|
||||
if (valid.length === 0) return null;
|
||||
return valid.filter(v => v).length / valid.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a stable cohort key from the sorted prompt_versions object.
|
||||
* Two runs with identical prompt files get the same cohort key.
|
||||
*/
|
||||
export function buildCohortKey(promptVersions: PromptVersions): string {
|
||||
// Filter out missing sentinels so old runs (4 fields) keep their original key
|
||||
const entries = Object.entries(promptVersions)
|
||||
.filter(([, v]) => v !== 'sha256:missing')
|
||||
.sort(([a], [b]) => a.localeCompare(b));
|
||||
const str = JSON.stringify(Object.fromEntries(entries));
|
||||
return crypto.createHash('sha256').update(str).digest('hex').slice(0, 12);
|
||||
}
|
||||
|
||||
/**
|
||||
* Backfill missing PromptVersions fields for old run files (pre-v1.1).
|
||||
*/
|
||||
export function backfillPromptVersions(pv: PromptVersions): PromptVersions {
|
||||
return {
|
||||
plan: pv.plan ?? 'sha256:missing',
|
||||
revise_plan: pv.revise_plan ?? 'sha256:missing',
|
||||
document: pv.document ?? 'sha256:missing',
|
||||
implement: pv.implement ?? 'sha256:missing',
|
||||
finalize: pv.finalize ?? 'sha256:missing',
|
||||
init: pv.init ?? 'sha256:missing',
|
||||
init_update: pv.init_update ?? 'sha256:missing',
|
||||
quick_task: pv.quick_task ?? 'sha256:missing',
|
||||
};
|
||||
}
|
||||
|
||||
// ── Load run files ────────────────────────────────────────────────────────────
|
||||
|
||||
export function loadRunFiles(runsDir: string): RunMetrics[] {
|
||||
if (!fs.existsSync(runsDir)) return [];
|
||||
|
||||
const files = fs.readdirSync(runsDir)
|
||||
.filter(f => f.endsWith('.json') && f.startsWith('run-'))
|
||||
.sort();
|
||||
|
||||
const runs: RunMetrics[] = [];
|
||||
for (const file of files) {
|
||||
try {
|
||||
const content = fs.readFileSync(path.join(runsDir, file), 'utf8');
|
||||
const data = JSON.parse(content) as RunMetrics;
|
||||
data.prompt_versions = backfillPromptVersions(data.prompt_versions);
|
||||
runs.push(data);
|
||||
} catch (err) {
|
||||
console.warn(`Warning: could not parse ${file}: ${err}`);
|
||||
}
|
||||
}
|
||||
return runs;
|
||||
}
|
||||
|
||||
// ── Build cohort metrics ──────────────────────────────────────────────────────
|
||||
|
||||
function buildCohort(runs: RunMetrics[], cohortKey: string): CohortMetrics {
|
||||
const runIds = runs.map(r => r.run_id);
|
||||
const timestamps = runs
|
||||
.flatMap(r => [r.project.started_at, r.project.completed_at])
|
||||
.filter((t): t is string => t != null)
|
||||
.sort();
|
||||
|
||||
// Step 1 averages
|
||||
const avgConfidence = avg(runs.map(r => r.step1_plan.final_confidence));
|
||||
const avgClarification = avg(runs.map(r => r.step1_plan.clarification_rounds));
|
||||
const avgVerifGaps = avg(runs.map(r => r.step1_plan.verification_gaps_found));
|
||||
const avgFRCount = avg(runs.map(r => r.step1_plan.functional_requirements_count));
|
||||
const avgNFRCount = avg(runs.map(r => r.step1_plan.non_functional_requirements_count));
|
||||
const avgRiskCount = avg(runs.map(r => r.step1_plan.risk_count));
|
||||
const avgPhaseStep1 = avg(runs.map(r => r.step1_plan.phase_count));
|
||||
|
||||
// Step 2 averages
|
||||
const avgTotalTasks = avg(runs.map(r => r.step2_document.total_tasks));
|
||||
const avgPhaseStep2 = avg(runs.map(r => r.step2_document.phase_count));
|
||||
const avgParallelGroups = avg(runs.map(r => r.step2_document.parallel_groups_identified));
|
||||
const avgReqCoverage = avg(runs.map(r => r.step2_document.requirement_coverage_percent));
|
||||
const avgVerifItems = avg(runs.map(r => r.step2_document.verification_items_added));
|
||||
|
||||
// Step 3 averages
|
||||
const avgCompletionRate = avg(runs.map(r => r.step3_implement.task_completion_rate));
|
||||
const avgBlockerCount = avg(runs.map(r => r.step3_implement.blocker_count));
|
||||
|
||||
// Step 4 averages
|
||||
const avgCompletionAtAudit = avg(runs.map(r => r.step4_finalize.completion_rate_at_audit));
|
||||
const avgVerifFailures = avg(runs.map(r => r.step4_finalize.verification_failures_found));
|
||||
const avgDocUpdates = avg(runs.map(r => r.step4_finalize.documentation_updates_needed));
|
||||
const archivalRate = rate(runs.map(r => r.step4_finalize.archival_succeeded));
|
||||
|
||||
// User feedback
|
||||
const feedbackRuns = runs.filter(r => r.user_feedback != null);
|
||||
const avgUserRating = avg(feedbackRuns.map(r => r.user_feedback!.overall_rating));
|
||||
const feedbackCount = feedbackRuns.length;
|
||||
|
||||
return {
|
||||
cohort_key: cohortKey,
|
||||
prompt_versions: runs[0].prompt_versions,
|
||||
run_count: runs.length,
|
||||
run_ids: runIds,
|
||||
first_seen: timestamps[0] ?? runs[0].run_id,
|
||||
last_seen: timestamps[timestamps.length - 1] ?? runs[runs.length - 1].run_id,
|
||||
|
||||
avg_confidence: avgConfidence,
|
||||
avg_clarification_rounds: avgClarification,
|
||||
avg_verification_gaps_found: avgVerifGaps,
|
||||
avg_functional_requirements_count: avgFRCount,
|
||||
avg_non_functional_requirements_count: avgNFRCount,
|
||||
avg_risk_count: avgRiskCount,
|
||||
avg_phase_count_step1: avgPhaseStep1,
|
||||
|
||||
avg_total_tasks: avgTotalTasks,
|
||||
avg_phase_count_step2: avgPhaseStep2,
|
||||
avg_parallel_groups: avgParallelGroups,
|
||||
avg_requirement_coverage_percent: avgReqCoverage,
|
||||
avg_verification_items_added: avgVerifItems,
|
||||
|
||||
avg_task_completion_rate: avgCompletionRate,
|
||||
avg_blocker_count: avgBlockerCount,
|
||||
|
||||
avg_completion_rate_at_audit: avgCompletionAtAudit,
|
||||
avg_verification_failures_found: avgVerifFailures,
|
||||
avg_documentation_updates_needed: avgDocUpdates,
|
||||
archival_success_rate: archivalRate,
|
||||
|
||||
avg_user_rating: avgUserRating,
|
||||
feedback_count: feedbackCount,
|
||||
};
|
||||
}
|
||||
|
||||
// ── Main aggregator ───────────────────────────────────────────────────────────
|
||||
|
||||
export function aggregate(runsDir: string, outputPath: string): AggregatedMetrics {
|
||||
const runs = loadRunFiles(runsDir);
|
||||
|
||||
// Group by cohort key
|
||||
const cohortMap = new Map<string, RunMetrics[]>();
|
||||
for (const run of runs) {
|
||||
const key = buildCohortKey(run.prompt_versions);
|
||||
if (!cohortMap.has(key)) cohortMap.set(key, []);
|
||||
cohortMap.get(key)!.push(run);
|
||||
}
|
||||
|
||||
// Sort cohorts by first seen
|
||||
const cohorts: CohortMetrics[] = [];
|
||||
for (const [key, cohortRuns] of cohortMap) {
|
||||
cohorts.push(buildCohort(cohortRuns, key));
|
||||
}
|
||||
cohorts.sort((a, b) => a.first_seen.localeCompare(b.first_seen));
|
||||
|
||||
// Determine current cohort (most recent)
|
||||
const currentCohortKey = cohorts.length > 0
|
||||
? cohorts[cohorts.length - 1].cohort_key
|
||||
: null;
|
||||
|
||||
const aggregated: AggregatedMetrics = {
|
||||
schema_version: '1.0',
|
||||
last_updated: new Date().toISOString(),
|
||||
total_runs: runs.length,
|
||||
cohorts,
|
||||
current_cohort_key: currentCohortKey,
|
||||
};
|
||||
|
||||
// Write to output path
|
||||
const dir = path.dirname(outputPath);
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
fs.writeFileSync(outputPath, JSON.stringify(aggregated, null, 2), 'utf8');
|
||||
|
||||
return aggregated;
|
||||
}
|
||||
|
||||
export function loadAggregated(outputPath: string): AggregatedMetrics | null {
|
||||
try {
|
||||
const content = fs.readFileSync(outputPath, 'utf8');
|
||||
return JSON.parse(content) as AggregatedMetrics;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Import a single run JSON from another project into the local runs dir.
|
||||
* Returns true if imported, false if already present.
|
||||
*/
|
||||
export function importRun(runJsonPath: string, runsDir: string): boolean {
|
||||
const content = fs.readFileSync(runJsonPath, 'utf8');
|
||||
const run = JSON.parse(content) as RunMetrics;
|
||||
run.prompt_versions = backfillPromptVersions(run.prompt_versions);
|
||||
const destPath = path.join(runsDir, `${run.run_id}.json`);
|
||||
|
||||
if (fs.existsSync(destPath)) {
|
||||
return false; // Already imported
|
||||
}
|
||||
|
||||
fs.mkdirSync(runsDir, { recursive: true });
|
||||
fs.writeFileSync(destPath, JSON.stringify(run, null, 2), 'utf8');
|
||||
return true;
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* analyzer.ts
|
||||
* Reads aggregated.json + prompt file contents, builds analysis prompt, invokes AI.
|
||||
*/
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import type { AggregatedMetrics } from './types.js';
|
||||
import { invokeLLM, type AgentType } from './invoke-llm.js';
|
||||
|
||||
const ANALYZE_PROMPT_PATH = new URL('../src/prompts/analyze.md', import.meta.url).pathname
|
||||
.replace(/^\/([A-Za-z]:)/, '$1'); // Fix Windows path
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
function readPromptFiles(plan2codeRoot: string): Record<string, string> {
|
||||
const srcDir = path.join(plan2codeRoot, 'src');
|
||||
const promptFiles = [
|
||||
'plan2code-1-plan.md',
|
||||
'plan2code-1b-revise-plan.md',
|
||||
'plan2code-2-document.md',
|
||||
'plan2code-3-implement.md',
|
||||
'plan2code-4-finalize.md',
|
||||
'plan2code-init.md',
|
||||
'plan2code-init-update.md',
|
||||
'plan2code-quick-task.md',
|
||||
];
|
||||
|
||||
const contents: Record<string, string> = {};
|
||||
for (const file of promptFiles) {
|
||||
try {
|
||||
contents[file] = fs.readFileSync(path.join(srcDir, file), 'utf8');
|
||||
} catch {
|
||||
contents[file] = '(file not found)';
|
||||
}
|
||||
}
|
||||
return contents;
|
||||
}
|
||||
|
||||
function interpolate(template: string, vars: Record<string, string>): string {
|
||||
let result = template;
|
||||
for (const [key, value] of Object.entries(vars)) {
|
||||
result = result.replaceAll(`{{${key}}}`, value);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function generateDiagnosisId(): string {
|
||||
const now = new Date();
|
||||
const ts = now.toISOString().replace(/[-:T.Z]/g, '').slice(0, 14);
|
||||
return `diag-${ts}`;
|
||||
}
|
||||
|
||||
// ── Main analyzer ─────────────────────────────────────────────────────────────
|
||||
|
||||
export interface AnalyzerOptions {
|
||||
aggregatedPath: string; // Path to aggregated.json
|
||||
plan2codeRoot: string; // Path to plan2code repo root
|
||||
proposalsDir: string; // Where to save diagnosis output
|
||||
model?: string; // AI model to use (default: agent's default)
|
||||
agent?: AgentType; // Agent to use (default: claude-code)
|
||||
}
|
||||
|
||||
export async function runAnalysis(opts: AnalyzerOptions): Promise<string> {
|
||||
const { aggregatedPath, plan2codeRoot, proposalsDir, model = 'default', agent = 'claude-code' } = opts;
|
||||
|
||||
// Load aggregated metrics
|
||||
let aggregated: AggregatedMetrics | null = null;
|
||||
try {
|
||||
aggregated = JSON.parse(fs.readFileSync(aggregatedPath, 'utf8')) as AggregatedMetrics;
|
||||
} catch {
|
||||
throw new Error(`Could not read aggregated metrics at ${aggregatedPath}. Run "Collect metrics" and "Import run data" first.`);
|
||||
}
|
||||
|
||||
if (aggregated.total_runs === 0) {
|
||||
throw new Error('No runs in aggregated metrics. Collect and import some runs first.');
|
||||
}
|
||||
|
||||
// Read prompt files
|
||||
const promptContents = readPromptFiles(plan2codeRoot);
|
||||
|
||||
// Format for interpolation
|
||||
const promptContentsStr = Object.entries(promptContents)
|
||||
.map(([file, content]) => `## ${file}\n\n${content}`)
|
||||
.join('\n\n---\n\n');
|
||||
|
||||
const aggregatedStr = JSON.stringify(aggregated, null, 2);
|
||||
|
||||
// Load analyze prompt template
|
||||
let analyzeTemplate: string;
|
||||
try {
|
||||
analyzeTemplate = fs.readFileSync(ANALYZE_PROMPT_PATH, 'utf8');
|
||||
} catch {
|
||||
// Fallback: look relative to cwd
|
||||
const altPath = path.join(process.cwd(), 'src', 'prompts', 'analyze.md');
|
||||
analyzeTemplate = fs.readFileSync(altPath, 'utf8');
|
||||
}
|
||||
|
||||
const fullPrompt = interpolate(analyzeTemplate, {
|
||||
aggregatedMetrics: aggregatedStr,
|
||||
promptContents: promptContentsStr,
|
||||
});
|
||||
|
||||
// Invoke Claude
|
||||
console.log(`\nInvoking AI analysis (agent: ${agent}, model: ${model === 'default' ? 'user default' : model})...`);
|
||||
console.log('This may take a minute...\n');
|
||||
|
||||
let diagnosisContent: string;
|
||||
try {
|
||||
diagnosisContent = await invokeLLM({
|
||||
prompt: fullPrompt,
|
||||
model,
|
||||
agent,
|
||||
timeout: 300_000,
|
||||
});
|
||||
} catch (err) {
|
||||
throw new Error(`AI invocation failed: ${err instanceof Error ? err.message : String(err)}\n\nMake sure the ${agent} CLI is installed and authenticated.`);
|
||||
}
|
||||
|
||||
// Save diagnosis
|
||||
const diagId = generateDiagnosisId();
|
||||
fs.mkdirSync(proposalsDir, { recursive: true });
|
||||
const diagPath = path.join(proposalsDir, `${diagId}-diagnosis.md`);
|
||||
fs.writeFileSync(diagPath, diagnosisContent, 'utf8');
|
||||
|
||||
console.log(`Diagnosis saved to: ${diagPath}`);
|
||||
return diagPath;
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
/**
|
||||
* applier.ts
|
||||
* Interactive review of a PromptProposal: display colored diff per edit,
|
||||
* user approves/rejects each edit, apply approved edits to src files.
|
||||
*/
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { confirm } from '@inquirer/prompts';
|
||||
import chalk from 'chalk';
|
||||
import type { PromptEdit, PromptProposal } from './types.js';
|
||||
import { validateEdit } from './improver.js';
|
||||
|
||||
const CHAR_LIMIT = 11_000;
|
||||
|
||||
// ── Diff display ──────────────────────────────────────────────────────────────
|
||||
|
||||
function displayEditDiff(edit: PromptEdit, index: number, total: number): void {
|
||||
console.log();
|
||||
console.log(chalk.bold(`─── Edit ${index + 1} of ${total} ───────────────────────────────────`));
|
||||
console.log(chalk.cyan(`File: ${edit.file}`));
|
||||
console.log(chalk.gray(`Rationale: ${edit.rationale}`));
|
||||
console.log(chalk.gray(`Expected impact: ${edit.expected_metric_impact}`));
|
||||
console.log(chalk.gray(`Char impact: ${edit.char_count_before} → ${edit.char_count_after} (${edit.char_count_delta >= 0 ? '+' : ''}${edit.char_count_delta})`));
|
||||
|
||||
// Show char count status
|
||||
if (edit.char_count_after > CHAR_LIMIT) {
|
||||
console.log(chalk.red(`⚠ WARNING: Would exceed ${CHAR_LIMIT} char limit!`));
|
||||
} else {
|
||||
const headroom = CHAR_LIMIT - edit.char_count_after;
|
||||
console.log(chalk.green(`✓ Char count OK (${headroom} chars headroom after edit)`));
|
||||
}
|
||||
|
||||
console.log();
|
||||
console.log(chalk.bold('── REMOVED (old_text) ──'));
|
||||
|
||||
// Show old text with line-level context
|
||||
const oldLines = edit.old_text.split('\n');
|
||||
for (const line of oldLines) {
|
||||
console.log(chalk.red('- ') + chalk.red(line));
|
||||
}
|
||||
|
||||
console.log();
|
||||
console.log(chalk.bold('── ADDED (new_text) ──'));
|
||||
|
||||
const newLines = edit.new_text.split('\n');
|
||||
for (const line of newLines) {
|
||||
console.log(chalk.green('+ ') + chalk.green(line));
|
||||
}
|
||||
|
||||
console.log();
|
||||
}
|
||||
|
||||
// ── Apply edit to file ────────────────────────────────────────────────────────
|
||||
|
||||
function applyEdit(edit: PromptEdit, plan2codeRoot: string): boolean {
|
||||
// Reject path traversal attempts
|
||||
if (edit.file.includes('..') || path.isAbsolute(edit.file)) {
|
||||
console.error(chalk.red(`✗ Rejected: "${edit.file}" contains path traversal or absolute path`));
|
||||
return false;
|
||||
}
|
||||
|
||||
const filePath = path.join(plan2codeRoot, 'src', edit.file);
|
||||
try {
|
||||
let content = fs.readFileSync(filePath, 'utf8');
|
||||
if (!content.includes(edit.old_text)) {
|
||||
console.error(chalk.red(`✗ Cannot apply: old_text not found in ${edit.file} (may have been modified by a previous edit)`));
|
||||
return false;
|
||||
}
|
||||
content = content.replace(edit.old_text, edit.new_text);
|
||||
|
||||
// Post-apply char count check
|
||||
if (content.length > CHAR_LIMIT) {
|
||||
console.error(chalk.red(`✗ Cannot apply: would exceed ${CHAR_LIMIT} char limit (${content.length} chars)`));
|
||||
return false;
|
||||
}
|
||||
|
||||
fs.writeFileSync(filePath, content, 'utf8');
|
||||
console.log(chalk.green(`✓ Applied edit to ${edit.file} (now ${content.length} chars)`));
|
||||
return true;
|
||||
} catch (err) {
|
||||
console.error(chalk.red(`✗ Failed to apply edit: ${err instanceof Error ? err.message : String(err)}`));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Main applier ──────────────────────────────────────────────────────────────
|
||||
|
||||
export interface ApplierOptions {
|
||||
proposalPath: string;
|
||||
plan2codeRoot: string;
|
||||
proposalsDir: string;
|
||||
}
|
||||
|
||||
export interface ApplierResult {
|
||||
approved: number;
|
||||
rejected: number;
|
||||
applied: number;
|
||||
failed: number;
|
||||
}
|
||||
|
||||
export async function reviewAndApply(opts: ApplierOptions): Promise<ApplierResult> {
|
||||
const { proposalPath, plan2codeRoot, proposalsDir } = opts;
|
||||
|
||||
// Load proposal
|
||||
let proposal: PromptProposal;
|
||||
try {
|
||||
proposal = JSON.parse(fs.readFileSync(proposalPath, 'utf8')) as PromptProposal;
|
||||
} catch {
|
||||
throw new Error(`Could not read proposal at ${proposalPath}`);
|
||||
}
|
||||
|
||||
if (proposal.proposals.length === 0) {
|
||||
console.log(chalk.yellow('No edits in this proposal.'));
|
||||
return { approved: 0, rejected: 0, applied: 0, failed: 0 };
|
||||
}
|
||||
|
||||
console.log();
|
||||
console.log(chalk.bold.cyan('=== Plan2Code Prompt Improvement Review ==='));
|
||||
console.log(chalk.gray(`Proposal: ${proposal.proposal_id}`));
|
||||
console.log(chalk.gray(`Created: ${proposal.created_at}`));
|
||||
console.log(chalk.gray(`Based on: ${proposal.based_on_runs.length} run(s)`));
|
||||
console.log(chalk.gray(`Edits: ${proposal.proposals.length}`));
|
||||
|
||||
// Re-validate all edits against current file state
|
||||
const promptContents: Record<string, string> = {};
|
||||
const srcDir = path.join(plan2codeRoot, 'src');
|
||||
for (const edit of proposal.proposals) {
|
||||
if (!promptContents[edit.file]) {
|
||||
try {
|
||||
promptContents[edit.file] = fs.readFileSync(path.join(srcDir, edit.file), 'utf8');
|
||||
} catch {
|
||||
promptContents[edit.file] = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let approved = 0;
|
||||
let rejected = 0;
|
||||
let applied = 0;
|
||||
let failed = 0;
|
||||
|
||||
for (let i = 0; i < proposal.proposals.length; i++) {
|
||||
const edit = proposal.proposals[i];
|
||||
|
||||
// Re-validate
|
||||
const validation = validateEdit(edit, promptContents);
|
||||
if (!validation.valid) {
|
||||
console.log();
|
||||
console.log(chalk.red(`✗ Edit ${i + 1} is no longer valid (files may have changed):`));
|
||||
for (const err of validation.errors) {
|
||||
console.log(chalk.red(` - ${err}`));
|
||||
}
|
||||
rejected++;
|
||||
continue;
|
||||
}
|
||||
|
||||
displayEditDiff(edit, i, proposal.proposals.length);
|
||||
|
||||
const approve = await confirm({
|
||||
message: `Apply this edit to ${edit.file}?`,
|
||||
default: true,
|
||||
});
|
||||
|
||||
if (!approve) {
|
||||
console.log(chalk.gray('Skipped.'));
|
||||
rejected++;
|
||||
continue;
|
||||
}
|
||||
|
||||
approved++;
|
||||
const success = applyEdit(edit, plan2codeRoot);
|
||||
if (success) {
|
||||
applied++;
|
||||
// Update in-memory content to reflect the edit
|
||||
promptContents[edit.file] = promptContents[edit.file].replace(edit.old_text, edit.new_text);
|
||||
} else {
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
// Update proposal status
|
||||
proposal.status = applied > 0 ? 'applied' : 'rejected';
|
||||
fs.writeFileSync(proposalPath, JSON.stringify(proposal, null, 2), 'utf8');
|
||||
|
||||
// Summary
|
||||
console.log();
|
||||
console.log(chalk.bold('─── Review Complete ──────────────────────────────────'));
|
||||
console.log(`Approved: ${chalk.green(String(approved))} Rejected: ${chalk.red(String(rejected))} Applied: ${chalk.green(String(applied))} Failed: ${chalk.red(String(failed))}`);
|
||||
|
||||
if (applied > 0) {
|
||||
console.log();
|
||||
console.log(chalk.bold.cyan('Next steps — create a PR with your changes:'));
|
||||
console.log(chalk.gray(' git add src/'));
|
||||
console.log(chalk.gray(` git commit -m "metrics: apply prompt improvements (${proposal.proposal_id})"`));
|
||||
console.log(chalk.gray(' git push -u origin HEAD'));
|
||||
console.log(chalk.gray(' gh pr create --title "metrics: apply gen N improvements"'));
|
||||
}
|
||||
|
||||
return { approved, rejected, applied, failed };
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { runCLI } from '../cli.js';
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
await runCLI();
|
||||
process.exit(0);
|
||||
} catch (err) {
|
||||
if (err instanceof Error && err.message.includes('User force closed')) {
|
||||
// Ctrl+C — exit cleanly
|
||||
process.exit(0);
|
||||
}
|
||||
console.error(err instanceof Error ? err.message : String(err));
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,841 @@
|
||||
/**
|
||||
* cli.ts
|
||||
* 100% interactive menu-driven CLI for plan2code-metrics.
|
||||
* No flags — all inputs collected via @inquirer/prompts.
|
||||
*/
|
||||
|
||||
import fs from 'fs';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import { select, input, confirm } from '@inquirer/prompts';
|
||||
import chalk from 'chalk';
|
||||
import ora from 'ora';
|
||||
import { collectRun } from './collector.js';
|
||||
import { aggregate, loadAggregated, importRun, loadRunFiles } from './aggregator.js';
|
||||
import { runAnalysis } from './analyzer.js';
|
||||
import { generateImprovement } from './improver.js';
|
||||
import { reviewAndApply } from './applier.js';
|
||||
import type { AggregatedMetrics, CohortMetrics } from './types.js';
|
||||
import { METRIC_TARGETS } from './types.js';
|
||||
import { AGENTS, type AgentType } from './invoke-llm.js';
|
||||
|
||||
// ── Session state (set at startup via interactive prompts) ───────────────────
|
||||
|
||||
let resolvedPlan2CodeRoot = '';
|
||||
let resolvedProjectRoot = '';
|
||||
|
||||
// ── Persisted user config (~/.plan2code-metrics.json) ──────────────────────
|
||||
|
||||
const USER_CONFIG_PATH = path.join(os.homedir(), '.plan2code-metrics.json');
|
||||
|
||||
interface UserConfig {
|
||||
plan2codeRepoPath?: string;
|
||||
}
|
||||
|
||||
function loadUserConfig(): UserConfig {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(USER_CONFIG_PATH, 'utf8'));
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function saveUserConfig(config: UserConfig): void {
|
||||
try {
|
||||
fs.writeFileSync(USER_CONFIG_PATH, JSON.stringify(config, null, 2), 'utf8');
|
||||
} catch { /* best-effort */ }
|
||||
}
|
||||
|
||||
// ── Defaults ──────────────────────────────────────────────────────────────────
|
||||
|
||||
const DEFAULT_METRICS_DIR = '.plan2code-metrics';
|
||||
const DEFAULT_RUNS_SUBDIR = 'runs';
|
||||
const DEFAULT_AGGREGATED_FILE = 'aggregated.json';
|
||||
const DEFAULT_PROPOSALS_SUBDIR = 'proposals';
|
||||
|
||||
function getMetricsDirs(baseDir = resolvedProjectRoot) {
|
||||
const metricsDir = path.join(baseDir, DEFAULT_METRICS_DIR);
|
||||
return {
|
||||
metricsDir,
|
||||
runsDir: path.join(metricsDir, DEFAULT_RUNS_SUBDIR),
|
||||
aggregatedPath: path.join(metricsDir, DEFAULT_AGGREGATED_FILE),
|
||||
proposalsDir: path.join(metricsDir, DEFAULT_PROPOSALS_SUBDIR),
|
||||
};
|
||||
}
|
||||
|
||||
// Detect plan2code root (either this directory or parent directories)
|
||||
function detectPlan2CodeRoot(): string | null {
|
||||
let dir = process.cwd();
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const srcDir = path.join(dir, 'src');
|
||||
if (
|
||||
fs.existsSync(path.join(srcDir, 'plan2code-1-plan.md')) &&
|
||||
fs.existsSync(path.join(srcDir, 'plan2code-2-document.md'))
|
||||
) {
|
||||
return dir;
|
||||
}
|
||||
const parent = path.dirname(dir);
|
||||
if (parent === dir) break;
|
||||
dir = parent;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Detect plan2code version
|
||||
function detectPlan2CodeVersion(plan2codeRoot: string): string {
|
||||
try {
|
||||
const versionPath = path.join(plan2codeRoot, 'version.json');
|
||||
const content = JSON.parse(fs.readFileSync(versionPath, 'utf8'));
|
||||
return content.version ?? '0.0.0';
|
||||
} catch {
|
||||
try {
|
||||
const pkg = JSON.parse(fs.readFileSync(path.join(plan2codeRoot, 'package.json'), 'utf8'));
|
||||
return pkg.version ?? '0.0.0';
|
||||
} catch {
|
||||
return '0.0.0';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Metric health helpers ─────────────────────────────────────────────────────
|
||||
|
||||
function metricStatus(key: string, value: number | null): string {
|
||||
if (value == null) return chalk.gray('N/A');
|
||||
const target = METRIC_TARGETS[key as keyof typeof METRIC_TARGETS];
|
||||
if (!target) return chalk.white(String(Math.round(value * 100) / 100));
|
||||
|
||||
const ok = target.direction === 'gte' ? value >= target.target : value <= target.target;
|
||||
const formatted = Number.isInteger(value) ? String(value) : value.toFixed(2);
|
||||
return ok ? chalk.green(`✓ ${formatted}`) : chalk.red(`✗ ${formatted} (target: ${target.direction === 'gte' ? '≥' : '≤'}${target.target})`);
|
||||
}
|
||||
|
||||
// ── Agent + model selection ───────────────────────────────────────────────────
|
||||
|
||||
async function selectAgentAndModel(): Promise<{ agent: AgentType; model: string }> {
|
||||
const agentChoice = await select({
|
||||
message: 'Which AI agent?',
|
||||
choices: Object.values(AGENTS).map(a => ({ name: a.displayName, value: a.name })),
|
||||
});
|
||||
|
||||
return { agent: agentChoice, model: AGENTS[agentChoice].defaultModel };
|
||||
}
|
||||
|
||||
// ── Flow: Collect metrics ─────────────────────────────────────────────────────
|
||||
|
||||
async function flowCollect(): Promise<void> {
|
||||
console.log();
|
||||
console.log(chalk.bold.cyan('── Collect Metrics ──'));
|
||||
|
||||
const sourceChoice = await select({
|
||||
message: 'Where is the completed project spec?',
|
||||
choices: [
|
||||
{ name: 'Archived spec (specs--completed/<feature-name>/)', value: 'archived' },
|
||||
{ name: 'Active spec directory (specs/<feature-name>/)', value: 'active' },
|
||||
{ name: 'Custom path', value: 'custom' },
|
||||
],
|
||||
});
|
||||
|
||||
let specDir: string;
|
||||
let projectName: string;
|
||||
|
||||
if (sourceChoice === 'active' || sourceChoice === 'archived') {
|
||||
const baseSubdir = sourceChoice === 'active' ? 'specs' : 'specs--completed';
|
||||
const baseDir = path.join(resolvedProjectRoot, baseSubdir);
|
||||
|
||||
if (!fs.existsSync(baseDir)) {
|
||||
console.log(chalk.red(`No ${baseSubdir}/ directory found in ${resolvedProjectRoot}`));
|
||||
return;
|
||||
}
|
||||
|
||||
const entries = fs.readdirSync(baseDir)
|
||||
.filter(f => fs.statSync(path.join(baseDir, f)).isDirectory());
|
||||
|
||||
if (entries.length === 0) {
|
||||
console.log(chalk.red(`No spec directories found in ${baseSubdir}/`));
|
||||
return;
|
||||
}
|
||||
|
||||
const chosen = await select({
|
||||
message: 'Select spec directory:',
|
||||
choices: entries.map(e => ({ name: e, value: e })),
|
||||
});
|
||||
|
||||
specDir = path.join(baseDir, chosen);
|
||||
projectName = chosen;
|
||||
} else {
|
||||
specDir = await input({
|
||||
message: 'Enter full path to spec directory:',
|
||||
validate: (v) => fs.existsSync(v) ? true : 'Directory not found',
|
||||
});
|
||||
projectName = await input({
|
||||
message: 'Project name (for metrics label):',
|
||||
default: path.basename(specDir),
|
||||
});
|
||||
}
|
||||
|
||||
// Detect plan2code root
|
||||
const plan2codeRoot = resolvedPlan2CodeRoot;
|
||||
const plan2codeVersion = detectPlan2CodeVersion(plan2codeRoot);
|
||||
|
||||
const { runsDir, aggregatedPath } = getMetricsDirs();
|
||||
|
||||
const spinner = ora('Collecting metrics from project artifacts...').start();
|
||||
try {
|
||||
const metrics = await collectRun({
|
||||
specDir,
|
||||
projectName,
|
||||
plan2codeRoot,
|
||||
plan2codeVersion,
|
||||
outputDir: runsDir,
|
||||
});
|
||||
|
||||
spinner.succeed(`Metrics collected: ${metrics.run_id}`);
|
||||
console.log(chalk.gray(` Saved to: ${runsDir}/${metrics.run_id}.json`));
|
||||
|
||||
// Re-aggregate
|
||||
aggregate(runsDir, aggregatedPath);
|
||||
console.log(chalk.gray(' Aggregated metrics updated.'));
|
||||
|
||||
// Show summary
|
||||
console.log();
|
||||
console.log(chalk.bold('Collected:'));
|
||||
console.log(` Step 1 (Plan): ${metrics.step1_plan.present ? chalk.green('✓') : chalk.gray('—')}`);
|
||||
console.log(` Step 2 (Document): ${metrics.step2_document.present ? chalk.green('✓') : chalk.gray('—')}`);
|
||||
console.log(` Step 3 (Implement): ${metrics.step3_implement.present ? chalk.green('✓') : chalk.gray('—')}`);
|
||||
console.log(` Step 4 (Finalize): ${metrics.step4_finalize.present ? chalk.green('✓') : chalk.gray('—')}`);
|
||||
console.log(` User Feedback: ${metrics.user_feedback ? chalk.green(`✓ (${metrics.user_feedback.overall_rating}/10)`) : chalk.gray('—')}`);
|
||||
|
||||
// Offer to collect feedback interactively if not present
|
||||
if (!metrics.user_feedback) {
|
||||
console.log();
|
||||
const wantFeedback = await confirm({
|
||||
message: 'No user feedback found in overview.md. Would you like to provide feedback now?',
|
||||
default: false,
|
||||
});
|
||||
|
||||
if (wantFeedback) {
|
||||
const rating = await input({
|
||||
message: 'Overall rating (1-10):',
|
||||
validate: (v) => {
|
||||
const n = parseInt(v, 10);
|
||||
return (n >= 1 && n <= 10) ? true : 'Must be a number between 1 and 10';
|
||||
},
|
||||
});
|
||||
const reason = await input({ message: 'Rating reason:' });
|
||||
const wentWell = await input({ message: 'What went well?' });
|
||||
const wentPoorly = await input({ message: 'What went poorly?' });
|
||||
|
||||
// Write the feedback table into overview.md
|
||||
const overviewPath = path.join(specDir, 'overview.md');
|
||||
let overviewContent = '';
|
||||
try {
|
||||
overviewContent = fs.readFileSync(overviewPath, 'utf8');
|
||||
} catch { /* empty */ }
|
||||
|
||||
// Escape pipe characters in user text to prevent table parsing issues
|
||||
const esc = (s: string) => s.replace(/\|/g, '\\|');
|
||||
|
||||
const feedbackTable = [
|
||||
'',
|
||||
'## User Feedback',
|
||||
'| Field | Value |',
|
||||
'|-------|-------|',
|
||||
`| Rating | ${rating} |`,
|
||||
`| Reason | ${esc(reason)} |`,
|
||||
`| Went Well | ${esc(wentWell)} |`,
|
||||
`| Went Poorly | ${esc(wentPoorly)} |`,
|
||||
'',
|
||||
].join('\n');
|
||||
|
||||
fs.writeFileSync(overviewPath, overviewContent + feedbackTable, 'utf8');
|
||||
console.log(chalk.green('✓ Feedback written to overview.md'));
|
||||
|
||||
// Delete the original run (without feedback) before re-collecting
|
||||
const originalRunPath = path.join(runsDir, `${metrics.run_id}.json`);
|
||||
try { fs.unlinkSync(originalRunPath); } catch { /* ignore */ }
|
||||
|
||||
// Re-collect and re-aggregate
|
||||
const reSpinner = ora('Re-collecting metrics with feedback...').start();
|
||||
const updatedMetrics = await collectRun({
|
||||
specDir,
|
||||
projectName,
|
||||
plan2codeRoot,
|
||||
plan2codeVersion,
|
||||
outputDir: runsDir,
|
||||
});
|
||||
aggregate(runsDir, aggregatedPath);
|
||||
reSpinner.succeed(`Updated: ${updatedMetrics.run_id} (rating: ${updatedMetrics.user_feedback?.overall_rating}/10)`);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
spinner.fail(`Collection failed: ${err instanceof Error ? err.message : String(err)}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Flow: Import run data ─────────────────────────────────────────────────────
|
||||
|
||||
async function flowImport(): Promise<void> {
|
||||
console.log();
|
||||
console.log(chalk.bold.cyan('── Import Run Data ──'));
|
||||
console.log(chalk.gray('Copy a run JSON from another project into this repo for aggregation.'));
|
||||
console.log();
|
||||
|
||||
const sourcePath = await input({
|
||||
message: 'Path to run JSON file (e.g., /path/to/project/.plan2code-metrics/runs/run-xxx.json):',
|
||||
validate: (v) => {
|
||||
if (!fs.existsSync(v)) return 'File not found';
|
||||
if (!v.endsWith('.json')) return 'Must be a .json file';
|
||||
return true;
|
||||
},
|
||||
});
|
||||
|
||||
const { runsDir, aggregatedPath } = getMetricsDirs();
|
||||
|
||||
try {
|
||||
const imported = importRun(sourcePath, runsDir);
|
||||
if (!imported) {
|
||||
console.log(chalk.yellow('Run already imported (same run_id exists).'));
|
||||
} else {
|
||||
console.log(chalk.green(`✓ Imported ${path.basename(sourcePath)}`));
|
||||
}
|
||||
|
||||
const spinner = ora('Re-aggregating...').start();
|
||||
const aggregated = aggregate(runsDir, aggregatedPath);
|
||||
spinner.succeed(`Aggregated metrics updated (${aggregated.total_runs} total runs)`);
|
||||
} catch (err) {
|
||||
console.error(chalk.red(`Import failed: ${err instanceof Error ? err.message : String(err)}`));
|
||||
}
|
||||
}
|
||||
|
||||
// ── Flow: View metrics status ─────────────────────────────────────────────────
|
||||
|
||||
async function flowViewStatus(): Promise<void> {
|
||||
console.log();
|
||||
console.log(chalk.bold.cyan('── Metrics Status & History ──'));
|
||||
|
||||
const { runsDir, aggregatedPath } = getMetricsDirs();
|
||||
const aggregated = loadAggregated(aggregatedPath);
|
||||
|
||||
if (!aggregated || aggregated.total_runs === 0) {
|
||||
console.log(chalk.yellow('No metrics collected yet. Use "Collect metrics" to get started.'));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log();
|
||||
console.log(chalk.bold(`Total runs: ${aggregated.total_runs} | Generations: ${aggregated.cohorts.length}`));
|
||||
console.log(chalk.gray(`Last updated: ${aggregated.last_updated}`));
|
||||
|
||||
for (let i = 0; i < aggregated.cohorts.length; i++) {
|
||||
const cohort = aggregated.cohorts[i];
|
||||
const isCurrent = cohort.cohort_key === aggregated.current_cohort_key;
|
||||
const label = isCurrent ? chalk.bold.green('[CURRENT]') : '';
|
||||
|
||||
console.log();
|
||||
console.log(chalk.bold(`Generation ${i + 1} (sha:${cohort.cohort_key}) — ${cohort.run_count} run(s) ${label}`));
|
||||
console.log(chalk.gray(` Period: ${cohort.first_seen?.slice(0, 10)} → ${cohort.last_seen?.slice(0, 10)}`));
|
||||
|
||||
// Step 1 metrics
|
||||
if (cohort.avg_confidence != null || cohort.avg_clarification_rounds != null) {
|
||||
console.log(chalk.bold(' Step 1 (Plan):'));
|
||||
if (cohort.avg_confidence != null)
|
||||
console.log(` avg_confidence: ${metricStatus('avg_confidence', cohort.avg_confidence)}`);
|
||||
if (cohort.avg_clarification_rounds != null)
|
||||
console.log(` avg_clarification_rounds: ${metricStatus('avg_clarification_rounds', cohort.avg_clarification_rounds)}`);
|
||||
if (cohort.avg_verification_gaps_found != null)
|
||||
console.log(` avg_verification_gaps: ${metricStatus('avg_verification_gaps_found', cohort.avg_verification_gaps_found)}`);
|
||||
}
|
||||
|
||||
// Step 2 metrics
|
||||
if (cohort.avg_total_tasks != null || cohort.avg_parallel_groups != null) {
|
||||
console.log(chalk.bold(' Step 2 (Document):'));
|
||||
if (cohort.avg_total_tasks != null)
|
||||
console.log(` avg_total_tasks: ${chalk.white(cohort.avg_total_tasks.toFixed(1))}`);
|
||||
if (cohort.avg_parallel_groups != null)
|
||||
console.log(` avg_parallel_groups: ${metricStatus('avg_parallel_groups', cohort.avg_parallel_groups)}`);
|
||||
if (cohort.avg_verification_items_added != null)
|
||||
console.log(` avg_verification_items: ${metricStatus('avg_verification_items_added', cohort.avg_verification_items_added)}`);
|
||||
}
|
||||
|
||||
// Step 3 metrics
|
||||
if (cohort.avg_task_completion_rate != null || cohort.avg_blocker_count != null) {
|
||||
console.log(chalk.bold(' Step 3 (Implement):'));
|
||||
if (cohort.avg_task_completion_rate != null)
|
||||
console.log(` avg_task_completion_rate: ${metricStatus('avg_task_completion_rate', cohort.avg_task_completion_rate)}`);
|
||||
if (cohort.avg_blocker_count != null)
|
||||
console.log(` avg_blocker_count: ${metricStatus('avg_blocker_count', cohort.avg_blocker_count)}`);
|
||||
}
|
||||
|
||||
// Step 4 metrics
|
||||
if (cohort.avg_completion_rate_at_audit != null || cohort.archival_success_rate != null) {
|
||||
console.log(chalk.bold(' Step 4 (Finalize):'));
|
||||
if (cohort.avg_completion_rate_at_audit != null)
|
||||
console.log(` avg_completion_at_audit: ${chalk.white(cohort.avg_completion_rate_at_audit.toFixed(2))}`);
|
||||
if (cohort.avg_verification_failures_found != null)
|
||||
console.log(` avg_verif_failures: ${metricStatus('avg_verification_failures_found', cohort.avg_verification_failures_found)}`);
|
||||
if (cohort.archival_success_rate != null)
|
||||
console.log(` archival_success_rate: ${metricStatus('archival_success_rate', cohort.archival_success_rate)}`);
|
||||
}
|
||||
|
||||
// User feedback
|
||||
if (cohort.feedback_count > 0) {
|
||||
console.log(chalk.bold(' User Feedback:'));
|
||||
console.log(` avg_user_rating: ${metricStatus('avg_user_rating', cohort.avg_user_rating)}`);
|
||||
console.log(` feedback_count: ${chalk.white(String(cohort.feedback_count))}`);
|
||||
}
|
||||
|
||||
// Compare with previous generation
|
||||
if (i > 0) {
|
||||
const prev = aggregated.cohorts[i - 1];
|
||||
const deltas: string[] = [];
|
||||
if (cohort.avg_confidence != null && prev.avg_confidence != null) {
|
||||
const d = cohort.avg_confidence - prev.avg_confidence;
|
||||
deltas.push(`confidence ${d >= 0 ? chalk.green(`▲${d.toFixed(1)}`) : chalk.red(`▼${Math.abs(d).toFixed(1)}`)}`);
|
||||
}
|
||||
if (cohort.avg_task_completion_rate != null && prev.avg_task_completion_rate != null) {
|
||||
const d = cohort.avg_task_completion_rate - prev.avg_task_completion_rate;
|
||||
deltas.push(`completion ${d >= 0 ? chalk.green(`▲${(d * 100).toFixed(1)}%`) : chalk.red(`▼${(Math.abs(d) * 100).toFixed(1)}%`)}`);
|
||||
}
|
||||
if (deltas.length > 0) {
|
||||
console.log(chalk.gray(` vs Gen ${i}: ${deltas.join(' ')}`));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Run list
|
||||
const runs = loadRunFiles(runsDir);
|
||||
if (runs.length > 0) {
|
||||
console.log();
|
||||
console.log(chalk.bold(`Run files (${runs.length}):`));
|
||||
for (const run of runs.slice(-10)) {
|
||||
console.log(chalk.gray(` ${run.run_id} ${run.project.name} v${run.plan2code_version}`));
|
||||
}
|
||||
if (runs.length > 10) {
|
||||
console.log(chalk.gray(` ... and ${runs.length - 10} more`));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Flow: Run analysis ────────────────────────────────────────────────────────
|
||||
|
||||
async function flowRunAnalysis(): Promise<string | null> {
|
||||
console.log();
|
||||
console.log(chalk.bold.cyan('── Run Analysis (Diagnose Weak Steps) ──'));
|
||||
|
||||
const { aggregatedPath, proposalsDir } = getMetricsDirs();
|
||||
const plan2codeRoot = resolvedPlan2CodeRoot;
|
||||
|
||||
const aggregated = loadAggregated(aggregatedPath);
|
||||
if (!aggregated || aggregated.total_runs === 0) {
|
||||
console.log(chalk.yellow('No aggregated metrics found. Collect and import runs first.'));
|
||||
return null;
|
||||
}
|
||||
|
||||
if (aggregated.total_runs < 3) {
|
||||
const proceed = await confirm({
|
||||
message: `Only ${aggregated.total_runs} run(s) available (≥3 recommended for reliable analysis). Proceed anyway?`,
|
||||
default: false,
|
||||
});
|
||||
if (!proceed) return null;
|
||||
}
|
||||
|
||||
const { agent, model } = await selectAgentAndModel();
|
||||
|
||||
try {
|
||||
const diagPath = await runAnalysis({
|
||||
aggregatedPath,
|
||||
plan2codeRoot,
|
||||
proposalsDir,
|
||||
model,
|
||||
agent,
|
||||
});
|
||||
|
||||
console.log();
|
||||
console.log(chalk.green('✓ Analysis complete'));
|
||||
console.log(chalk.gray(`Diagnosis: ${diagPath}`));
|
||||
|
||||
const viewNow = await confirm({
|
||||
message: 'Open diagnosis in console?',
|
||||
default: true,
|
||||
});
|
||||
|
||||
if (viewNow) {
|
||||
const content = fs.readFileSync(diagPath, 'utf8');
|
||||
console.log();
|
||||
console.log(chalk.dim('─'.repeat(60)));
|
||||
console.log(content);
|
||||
console.log(chalk.dim('─'.repeat(60)));
|
||||
}
|
||||
|
||||
return diagPath;
|
||||
} catch (err) {
|
||||
console.error(chalk.red(`Analysis failed: ${err instanceof Error ? err.message : String(err)}`));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Flow: Generate improvement proposal ──────────────────────────────────────
|
||||
|
||||
async function flowGenerateProposal(): Promise<void> {
|
||||
console.log();
|
||||
console.log(chalk.bold.cyan('── Generate Improvement Proposal ──'));
|
||||
|
||||
const { aggregatedPath, proposalsDir, runsDir } = getMetricsDirs();
|
||||
const plan2codeRoot = resolvedPlan2CodeRoot;
|
||||
|
||||
// Find diagnosis files
|
||||
let diagFiles: string[] = [];
|
||||
if (fs.existsSync(proposalsDir)) {
|
||||
diagFiles = fs.readdirSync(proposalsDir)
|
||||
.filter(f => f.endsWith('-diagnosis.md'))
|
||||
.sort()
|
||||
.reverse(); // most recent first
|
||||
}
|
||||
|
||||
let diagnosisPath: string;
|
||||
|
||||
if (diagFiles.length === 0) {
|
||||
console.log(chalk.yellow('No diagnosis files found. Running analysis first...'));
|
||||
const diagPath = await flowRunAnalysis();
|
||||
if (!diagPath) return;
|
||||
diagnosisPath = diagPath;
|
||||
} else {
|
||||
const choice = await select({
|
||||
message: 'Select diagnosis to base proposal on:',
|
||||
choices: [
|
||||
...diagFiles.map(f => ({ name: f, value: path.join(proposalsDir, f) })),
|
||||
{ name: '(run new analysis first)', value: '__new__' },
|
||||
],
|
||||
});
|
||||
|
||||
if (choice === '__new__') {
|
||||
const diagPath = await flowRunAnalysis();
|
||||
if (!diagPath) return;
|
||||
diagnosisPath = diagPath;
|
||||
} else {
|
||||
diagnosisPath = choice;
|
||||
}
|
||||
}
|
||||
|
||||
const { agent, model } = await selectAgentAndModel();
|
||||
|
||||
try {
|
||||
const result = await generateImprovement({
|
||||
diagnosisPath,
|
||||
plan2codeRoot,
|
||||
proposalsDir,
|
||||
runsDir,
|
||||
model,
|
||||
agent,
|
||||
});
|
||||
|
||||
console.log();
|
||||
console.log(chalk.green(`✓ Proposal generated: ${result.proposal.proposal_id}`));
|
||||
console.log(chalk.gray(` Valid edits: ${result.validEditCount}`));
|
||||
console.log(chalk.gray(` Invalid edits: ${result.invalidEditCount} (rejected)`));
|
||||
console.log(chalk.gray(` Saved to: ${result.proposalPath}`));
|
||||
|
||||
if (result.validEditCount > 0) {
|
||||
const reviewNow = await confirm({
|
||||
message: 'Review and apply edits now?',
|
||||
default: true,
|
||||
});
|
||||
|
||||
if (reviewNow) {
|
||||
await reviewAndApply({
|
||||
proposalPath: result.proposalPath,
|
||||
plan2codeRoot,
|
||||
proposalsDir,
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(chalk.red(`Proposal generation failed: ${err instanceof Error ? err.message : String(err)}`));
|
||||
}
|
||||
}
|
||||
|
||||
// ── Flow: Review and apply proposal ──────────────────────────────────────────
|
||||
|
||||
async function flowReviewAndApply(): Promise<void> {
|
||||
console.log();
|
||||
console.log(chalk.bold.cyan('── Review and Apply a Proposal ──'));
|
||||
|
||||
const { proposalsDir } = getMetricsDirs();
|
||||
const plan2codeRoot = resolvedPlan2CodeRoot;
|
||||
|
||||
if (!fs.existsSync(proposalsDir)) {
|
||||
console.log(chalk.yellow('No proposals directory found. Generate a proposal first.'));
|
||||
return;
|
||||
}
|
||||
|
||||
const proposalFiles = fs.readdirSync(proposalsDir)
|
||||
.filter(f => /^prop-\d+\.json$/.test(f))
|
||||
.sort()
|
||||
.reverse(); // most recent first
|
||||
|
||||
if (proposalFiles.length === 0) {
|
||||
console.log(chalk.yellow('No proposal files found. Generate a proposal first.'));
|
||||
return;
|
||||
}
|
||||
|
||||
const chosen = await select({
|
||||
message: 'Select proposal to review:',
|
||||
choices: proposalFiles.map(f => {
|
||||
try {
|
||||
const p = JSON.parse(fs.readFileSync(path.join(proposalsDir, f), 'utf8'));
|
||||
const label = `${f} [${p.status}] ${p.proposals?.length ?? 0} edits`;
|
||||
return { name: label, value: path.join(proposalsDir, f) };
|
||||
} catch {
|
||||
return { name: f, value: path.join(proposalsDir, f) };
|
||||
}
|
||||
}),
|
||||
});
|
||||
|
||||
await reviewAndApply({
|
||||
proposalPath: chosen,
|
||||
plan2codeRoot,
|
||||
proposalsDir,
|
||||
});
|
||||
}
|
||||
|
||||
// ── Flow: Delete metrics data ─────────────────────────────────────────────────
|
||||
|
||||
async function flowDelete(): Promise<void> {
|
||||
console.log();
|
||||
console.log(chalk.bold.cyan('── Delete Metrics Data ──'));
|
||||
|
||||
const { metricsDir, runsDir, aggregatedPath, proposalsDir } = getMetricsDirs();
|
||||
|
||||
if (!fs.existsSync(metricsDir)) {
|
||||
console.log(chalk.yellow('No metrics data found (.plan2code-metrics/ does not exist).'));
|
||||
return;
|
||||
}
|
||||
|
||||
const scope = await select({
|
||||
message: 'What do you want to delete?',
|
||||
choices: [
|
||||
{ name: 'Delete specific run(s)', value: 'select' },
|
||||
{ name: 'Delete all runs and aggregated data', value: 'runs' },
|
||||
{ name: 'Delete everything (runs, aggregated data, proposals)', value: 'all' },
|
||||
{ name: 'Cancel', value: 'cancel' },
|
||||
],
|
||||
});
|
||||
|
||||
if (scope === 'cancel') return;
|
||||
|
||||
if (scope === 'select') {
|
||||
const runs = loadRunFiles(runsDir);
|
||||
if (runs.length === 0) {
|
||||
console.log(chalk.yellow('No run files found.'));
|
||||
return;
|
||||
}
|
||||
|
||||
const choices = runs.map(r => ({
|
||||
name: `${r.run_id} ${r.project.name} v${r.plan2code_version}`,
|
||||
value: r.run_id,
|
||||
}));
|
||||
|
||||
// Select runs one at a time since @inquirer/prompts select is single-choice
|
||||
const toDelete: string[] = [];
|
||||
let selecting = true;
|
||||
while (selecting) {
|
||||
const remaining = choices.filter(c => !toDelete.includes(c.value));
|
||||
if (remaining.length === 0) break;
|
||||
|
||||
const chosen = await select({
|
||||
message: `Select a run to delete (${toDelete.length} selected so far):`,
|
||||
choices: [
|
||||
...remaining,
|
||||
{ name: toDelete.length > 0 ? `Done selecting (delete ${toDelete.length})` : 'Cancel', value: '__done__' },
|
||||
],
|
||||
});
|
||||
|
||||
if (chosen === '__done__') {
|
||||
selecting = false;
|
||||
} else {
|
||||
toDelete.push(chosen);
|
||||
console.log(chalk.gray(` + ${chosen}`));
|
||||
}
|
||||
}
|
||||
|
||||
if (toDelete.length === 0) return;
|
||||
|
||||
const confirmed = await confirm({
|
||||
message: `Delete ${toDelete.length} run(s)? This cannot be undone.`,
|
||||
default: false,
|
||||
});
|
||||
if (!confirmed) return;
|
||||
|
||||
let deleted = 0;
|
||||
for (const runId of toDelete) {
|
||||
const filePath = path.join(runsDir, `${runId}.json`);
|
||||
try {
|
||||
fs.unlinkSync(filePath);
|
||||
deleted++;
|
||||
} catch {
|
||||
console.log(chalk.yellow(` Could not delete ${runId}.json`));
|
||||
}
|
||||
}
|
||||
|
||||
console.log(chalk.green(`✓ Deleted ${deleted} run(s).`));
|
||||
|
||||
// Re-aggregate with remaining runs
|
||||
const remainingRuns = loadRunFiles(runsDir);
|
||||
if (remainingRuns.length > 0) {
|
||||
aggregate(runsDir, aggregatedPath);
|
||||
console.log(chalk.gray(` Aggregated metrics updated (${remainingRuns.length} runs remaining).`));
|
||||
} else {
|
||||
try { fs.unlinkSync(aggregatedPath); } catch { /* ignore */ }
|
||||
console.log(chalk.gray(' No runs remaining — aggregated data removed.'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// scope === 'runs' or 'all'
|
||||
const label = scope === 'all'
|
||||
? 'ALL metrics data (runs, aggregated data, and proposals)'
|
||||
: 'all runs and aggregated data';
|
||||
|
||||
const confirmed = await confirm({
|
||||
message: `Delete ${label}? This cannot be undone.`,
|
||||
default: false,
|
||||
});
|
||||
if (!confirmed) return;
|
||||
|
||||
// Delete run files
|
||||
let runCount = 0;
|
||||
if (fs.existsSync(runsDir)) {
|
||||
const files = fs.readdirSync(runsDir).filter(f => f.endsWith('.json'));
|
||||
for (const f of files) {
|
||||
try { fs.unlinkSync(path.join(runsDir, f)); runCount++; } catch { /* ignore */ }
|
||||
}
|
||||
}
|
||||
|
||||
// Delete aggregated file
|
||||
try { fs.unlinkSync(aggregatedPath); } catch { /* ignore */ }
|
||||
|
||||
console.log(chalk.green(`✓ Deleted ${runCount} run(s) and aggregated data.`));
|
||||
|
||||
if (scope === 'all') {
|
||||
// Delete proposals
|
||||
let proposalCount = 0;
|
||||
if (fs.existsSync(proposalsDir)) {
|
||||
const files = fs.readdirSync(proposalsDir);
|
||||
for (const f of files) {
|
||||
try { fs.unlinkSync(path.join(proposalsDir, f)); proposalCount++; } catch { /* ignore */ }
|
||||
}
|
||||
try { fs.rmdirSync(proposalsDir); } catch { /* ignore */ }
|
||||
}
|
||||
console.log(chalk.green(`✓ Deleted ${proposalCount} proposal file(s).`));
|
||||
}
|
||||
}
|
||||
|
||||
// ── Main menu ─────────────────────────────────────────────────────────────────
|
||||
|
||||
export async function runCLI(): Promise<void> {
|
||||
console.log();
|
||||
console.log(chalk.bold.white('plan2code-metrics'));
|
||||
console.log(chalk.gray('Recursive self-improvement toolchain for plan2code contributors'));
|
||||
console.log();
|
||||
|
||||
// ── Prompt for paths ────────────────────────────────────────────────────────
|
||||
|
||||
const userConfig = loadUserConfig();
|
||||
const savedRoot = userConfig.plan2codeRepoPath && fs.existsSync(userConfig.plan2codeRepoPath)
|
||||
? userConfig.plan2codeRepoPath
|
||||
: null;
|
||||
const detectedRoot = savedRoot ?? detectPlan2CodeRoot();
|
||||
|
||||
const s2cPath = await input({
|
||||
message: 'Path to plan2code repo:',
|
||||
default: detectedRoot ?? undefined,
|
||||
validate: (v) => {
|
||||
if (!v.trim()) return 'Path is required';
|
||||
const resolved = path.resolve(v.trim());
|
||||
if (!fs.existsSync(resolved)) return 'Directory not found';
|
||||
return true;
|
||||
},
|
||||
});
|
||||
resolvedPlan2CodeRoot = path.resolve(s2cPath.trim());
|
||||
|
||||
// Persist the path for next run
|
||||
if (resolvedPlan2CodeRoot !== userConfig.plan2codeRepoPath) {
|
||||
saveUserConfig({ ...userConfig, plan2codeRepoPath: resolvedPlan2CodeRoot });
|
||||
}
|
||||
|
||||
// Warn if the path doesn't look like a plan2code repo
|
||||
const hasSrcPrompts =
|
||||
fs.existsSync(path.join(resolvedPlan2CodeRoot, 'src', 'plan2code-1-plan.md')) &&
|
||||
fs.existsSync(path.join(resolvedPlan2CodeRoot, 'src', 'plan2code-2-document.md'));
|
||||
if (!hasSrcPrompts) {
|
||||
console.log(chalk.yellow('⚠ No src/plan2code-*.md prompts found at that path. Hashing and analysis may be limited.'));
|
||||
}
|
||||
|
||||
const projPath = await input({
|
||||
message: 'Path to project repo (metrics source):',
|
||||
default: process.cwd(),
|
||||
validate: (v) => {
|
||||
if (!v.trim()) return 'Path is required';
|
||||
const resolved = path.resolve(v.trim());
|
||||
if (!fs.existsSync(resolved)) return 'Directory not found';
|
||||
return true;
|
||||
},
|
||||
});
|
||||
resolvedProjectRoot = path.resolve(projPath.trim());
|
||||
|
||||
// Display resolved paths
|
||||
const version = detectPlan2CodeVersion(resolvedPlan2CodeRoot);
|
||||
console.log();
|
||||
console.log(chalk.gray(`plan2code repo: ${resolvedPlan2CodeRoot} (v${version})`));
|
||||
console.log(chalk.gray(`project repo: ${resolvedProjectRoot}`));
|
||||
console.log();
|
||||
|
||||
let continueLoop = true;
|
||||
while (continueLoop) {
|
||||
const action = await select({
|
||||
message: 'What would you like to do?',
|
||||
choices: [
|
||||
{ name: 'Collect metrics for a completed project', value: 'collect' },
|
||||
{ name: 'Import run data from another project', value: 'import' },
|
||||
{ name: 'View metrics status and history', value: 'view' },
|
||||
{ name: 'Run analysis (diagnose weak steps)', value: 'analyze' },
|
||||
{ name: 'Generate improvement proposal', value: 'propose' },
|
||||
{ name: 'Review and apply a proposal', value: 'apply' },
|
||||
{ name: chalk.red('Delete metrics data'), value: 'delete' },
|
||||
{ name: 'Exit', value: 'exit' },
|
||||
],
|
||||
});
|
||||
|
||||
switch (action) {
|
||||
case 'collect':
|
||||
await flowCollect();
|
||||
break;
|
||||
case 'import':
|
||||
await flowImport();
|
||||
break;
|
||||
case 'view':
|
||||
await flowViewStatus();
|
||||
break;
|
||||
case 'analyze':
|
||||
await flowRunAnalysis();
|
||||
break;
|
||||
case 'propose':
|
||||
await flowGenerateProposal();
|
||||
break;
|
||||
case 'apply':
|
||||
await flowReviewAndApply();
|
||||
break;
|
||||
case 'delete':
|
||||
await flowDelete();
|
||||
break;
|
||||
case 'exit':
|
||||
continueLoop = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if (continueLoop && action !== 'exit') {
|
||||
console.log();
|
||||
}
|
||||
}
|
||||
|
||||
console.log(chalk.gray('Goodbye.'));
|
||||
}
|
||||
@@ -0,0 +1,633 @@
|
||||
/**
|
||||
* collector.ts
|
||||
* Reads finished project artifacts and writes a RunMetrics JSON file.
|
||||
* Zero dependency on plan2code-loop internals — reads files directly.
|
||||
*/
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import crypto from 'crypto';
|
||||
import type {
|
||||
RunMetrics,
|
||||
PromptVersions,
|
||||
Step1PlanMetrics,
|
||||
Step2DocumentMetrics,
|
||||
Step3ImplementMetrics,
|
||||
Step4FinalizeMetrics,
|
||||
UserFeedback,
|
||||
} from './types.js';
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
function sha256File(filePath: string): string {
|
||||
try {
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
return 'sha256:' + crypto.createHash('sha256').update(content).digest('hex');
|
||||
} catch {
|
||||
return 'sha256:missing';
|
||||
}
|
||||
}
|
||||
|
||||
function readFileSafe(filePath: string): string | null {
|
||||
try {
|
||||
return fs.readFileSync(filePath, 'utf8');
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract METRICS_JSON HTML comment blocks from a file.
|
||||
* Format: <!-- METRICS_JSON {"key": value, ...} -->
|
||||
* A file may contain multiple blocks (e.g., overview.md has document + finalize).
|
||||
* If `stepFilter` is provided, returns only the block with matching "step" field.
|
||||
* Returns parsed object or null if not found / invalid.
|
||||
*/
|
||||
function extractMetricsJson(content: string, stepFilter?: string): Record<string, unknown> | null {
|
||||
const re = /<!--\s*METRICS_JSON\s+(\{[\s\S]*?\})\s*-->/g;
|
||||
let match: RegExpExecArray | null;
|
||||
while ((match = re.exec(content)) !== null) {
|
||||
try {
|
||||
const parsed = JSON.parse(match[1]) as Record<string, unknown>;
|
||||
if (!stepFilter || parsed['step'] === stepFilter) {
|
||||
return parsed;
|
||||
}
|
||||
} catch {
|
||||
// try next match
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Strip the "## Success Criteria" section so its checkboxes aren't counted as tasks. */
|
||||
function stripSuccessCriteriaSection(overview: string): string {
|
||||
return overview.replace(/^## Success Criteria\s*\n[\s\S]*?(?=\n## |\n*$)/m, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract canonical task counts from the Completion Summary section.
|
||||
* Supports two formats found in real specs:
|
||||
* - Bold text: "**Completion Rate:** 100% (5/5 tasks)"
|
||||
* - Table row: "| Total Tasks | 28 (28/28 complete — 100%) |"
|
||||
* Returns null when no parseable counts are found.
|
||||
*/
|
||||
function parseCompletionSummaryTaskCount(overview: string): { completed: number; total: number } | null {
|
||||
const summaryMatch = overview.match(/## Completion Summary\s*\n([\s\S]*?)(?=\n## |\n*$)/);
|
||||
if (!summaryMatch) return null;
|
||||
const summary = summaryMatch[1];
|
||||
|
||||
// "Completion Rate: X% (Y/Z tasks)" — handles bold markdown and various separators
|
||||
const rateMatch = summary.match(/\**Completion(?:\s+Rate)?\**[:\s*]+\d{1,3}%\s*\((\d+)\/(\d+)\s*tasks?\)/i);
|
||||
if (rateMatch) return { completed: parseInt(rateMatch[1], 10), total: parseInt(rateMatch[2], 10) };
|
||||
|
||||
// "Total Tasks | 28 (28/28 complete"
|
||||
const tableMatch = summary.match(/Total Tasks\s*\|\s*(\d+)\s*\((\d+)\/(\d+)\s*complete/i);
|
||||
if (tableMatch) return { completed: parseInt(tableMatch[2], 10), total: parseInt(tableMatch[3], 10) };
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function generateRunId(): string {
|
||||
const now = new Date();
|
||||
const ts = now.toISOString().replace(/[-:T]/g, '').slice(0, 14);
|
||||
const rand = crypto.randomBytes(2).toString('hex');
|
||||
return `run-${ts.slice(0, 8)}-${ts.slice(8, 14)}-${rand}`;
|
||||
}
|
||||
|
||||
// ── Prompt version hashing ────────────────────────────────────────────────────
|
||||
|
||||
export function collectPromptVersions(plan2codeRoot: string): PromptVersions {
|
||||
const srcDir = path.join(plan2codeRoot, 'src');
|
||||
return {
|
||||
plan: sha256File(path.join(srcDir, 'plan2code-1-plan.md')),
|
||||
revise_plan: sha256File(path.join(srcDir, 'plan2code-1b-revise-plan.md')),
|
||||
document: sha256File(path.join(srcDir, 'plan2code-2-document.md')),
|
||||
implement: sha256File(path.join(srcDir, 'plan2code-3-implement.md')),
|
||||
finalize: sha256File(path.join(srcDir, 'plan2code-4-finalize.md')),
|
||||
init: sha256File(path.join(srcDir, 'plan2code-init.md')),
|
||||
init_update: sha256File(path.join(srcDir, 'plan2code-init-update.md')),
|
||||
quick_task: sha256File(path.join(srcDir, 'plan2code-quick-task.md')),
|
||||
};
|
||||
}
|
||||
|
||||
// ── Step 1: Plan ──────────────────────────────────────────────────────────────
|
||||
|
||||
export function collectStep1(specDir: string): Step1PlanMetrics {
|
||||
// Find PLAN-DRAFT-*.md files
|
||||
let draftFiles: string[] = [];
|
||||
let convFiles: string[] = [];
|
||||
try {
|
||||
const files = fs.readdirSync(specDir);
|
||||
draftFiles = files
|
||||
.filter(f => f.startsWith('PLAN-DRAFT-') && f.endsWith('.md'))
|
||||
.map(f => path.join(specDir, f));
|
||||
convFiles = files
|
||||
.filter(f => f.startsWith('PLAN-CONVERSATION-') && f.endsWith('.md'))
|
||||
.map(f => path.join(specDir, f));
|
||||
} catch {
|
||||
return { present: false, final_confidence: null, confidence_breakdown: null,
|
||||
clarification_rounds: null, tech_stack_revision_rounds: null,
|
||||
verification_gaps_found: null, functional_requirements_count: null,
|
||||
non_functional_requirements_count: null, risk_count: null, phase_count: null };
|
||||
}
|
||||
|
||||
if (draftFiles.length === 0) {
|
||||
return { present: false, final_confidence: null, confidence_breakdown: null,
|
||||
clarification_rounds: null, tech_stack_revision_rounds: null,
|
||||
verification_gaps_found: null, functional_requirements_count: null,
|
||||
non_functional_requirements_count: null, risk_count: null, phase_count: null };
|
||||
}
|
||||
|
||||
// Use the latest draft file
|
||||
draftFiles.sort();
|
||||
const latestDraft = readFileSafe(draftFiles[draftFiles.length - 1]) ?? '';
|
||||
|
||||
// ── Primary source: METRICS_JSON HTML comment ──
|
||||
// Format: <!-- METRICS_JSON {"confidence": 95, "clarification_rounds": 0, ...} -->
|
||||
const metricsJson = extractMetricsJson(latestDraft);
|
||||
if (metricsJson) {
|
||||
const num = (key: string): number | null => {
|
||||
const v = metricsJson[key];
|
||||
return typeof v === 'number' ? v : null;
|
||||
};
|
||||
const bd = metricsJson['confidence_breakdown'] as Record<string, number> | undefined;
|
||||
return {
|
||||
present: true,
|
||||
final_confidence: num('confidence'),
|
||||
confidence_breakdown: bd ? {
|
||||
requirements: bd.requirements ?? null,
|
||||
feasibility: bd.feasibility ?? null,
|
||||
integration: bd.integration ?? null,
|
||||
risk: bd.risk ?? null,
|
||||
} : null,
|
||||
clarification_rounds: num('clarification_rounds'),
|
||||
tech_stack_revision_rounds: num('tech_stack_revision_rounds'),
|
||||
verification_gaps_found: num('verification_gaps_found'),
|
||||
functional_requirements_count: num('functional_requirements_count'),
|
||||
non_functional_requirements_count: num('non_functional_requirements_count'),
|
||||
risk_count: num('risk_count'),
|
||||
phase_count: num('phase_count'),
|
||||
};
|
||||
}
|
||||
|
||||
// ── Fallback: structured ## Planning Metrics appendix ──
|
||||
// Handles both plain (confidence: 95) and bold (**confidence:** 95) field formats
|
||||
// Planning Metrics is typically the last section, so we capture greedily to end,
|
||||
// but stop at the next ## heading or --- if present.
|
||||
const metricsSection = latestDraft.match(/^##\s+Planning\s+Metrics\s*\n([\s\S]+?)(?:\n##\s|\n---)/m)
|
||||
?? latestDraft.match(/^##\s+Planning\s+Metrics\s*\n([\s\S]+)/m);
|
||||
const metricsBlock = metricsSection?.[1] ?? '';
|
||||
const parseMetricField = (field: string): number | null => {
|
||||
// Match plain "field: N" or bold "**field:** N"
|
||||
const m = metricsBlock.match(new RegExp(`^\\**${field}\\**[:\\s*]+?(\\d+)`, 'm'));
|
||||
return m ? parseInt(m[1], 10) : null;
|
||||
};
|
||||
|
||||
// ── Confidence ──
|
||||
let finalConfidence = parseMetricField('confidence');
|
||||
if (finalConfidence === null) {
|
||||
// Fallback: prose scraping — handle bold markdown like **Confidence:** 95%
|
||||
const confMatch = latestDraft.match(/(?:Overall|Final|Total)?\s*\**[Cc]onfidence\**[:\s*]+(\d{1,3})%/);
|
||||
if (confMatch) {
|
||||
finalConfidence = parseInt(confMatch[1], 10);
|
||||
} else {
|
||||
const tableMatch = latestDraft.match(/[|]\s*[Cc]onfidence\s*[|]\s*(\d{1,3})/);
|
||||
if (tableMatch) finalConfidence = parseInt(tableMatch[1], 10);
|
||||
}
|
||||
}
|
||||
|
||||
// Confidence breakdown (requirements, feasibility, integration, risk)
|
||||
let confidenceBreakdown: Step1PlanMetrics['confidence_breakdown'] = null;
|
||||
const reqMatch = latestDraft.match(/[Rr]equirements?[:\s|]+(\d{1,2})/);
|
||||
const feasMatch = latestDraft.match(/[Ff]easibility[:\s|]+(\d{1,2})/);
|
||||
const intMatch = latestDraft.match(/[Ii]ntegration[:\s|]+(\d{1,2})/);
|
||||
const riskMatch = latestDraft.match(/[Rr]isk[:\s|]+(\d{1,2})/);
|
||||
if (reqMatch || feasMatch || intMatch || riskMatch) {
|
||||
confidenceBreakdown = {
|
||||
requirements: reqMatch ? parseInt(reqMatch[1], 10) : null,
|
||||
feasibility: feasMatch ? parseInt(feasMatch[1], 10) : null,
|
||||
integration: intMatch ? parseInt(intMatch[1], 10) : null,
|
||||
risk: riskMatch ? parseInt(riskMatch[1], 10) : null,
|
||||
};
|
||||
}
|
||||
|
||||
// ── Clarification rounds ──
|
||||
let clarificationRounds = parseMetricField('clarification_rounds');
|
||||
|
||||
// ── Verification gaps ──
|
||||
let verificationGaps = parseMetricField('verification_gaps_found');
|
||||
|
||||
// ── Functional / non-functional requirements ──
|
||||
let frCount = parseMetricField('functional_requirements_count');
|
||||
if (frCount === null) {
|
||||
frCount = (latestDraft.match(/###\s+FR-/g) ?? []).length || null;
|
||||
}
|
||||
let nfrCount = parseMetricField('non_functional_requirements_count');
|
||||
if (nfrCount === null) {
|
||||
nfrCount = (latestDraft.match(/###\s+NFR-/g) ?? []).length || null;
|
||||
}
|
||||
|
||||
// ── Risk count ──
|
||||
let riskCount = parseMetricField('risk_count');
|
||||
if (riskCount === null) {
|
||||
const riskRows = latestDraft.match(/^\s*[|][^|]*(?:High|Medium|Low|Critical)[^|]*[|]/gm) ?? [];
|
||||
riskCount = riskRows.length || null;
|
||||
}
|
||||
|
||||
// ── Phase count ──
|
||||
let phaseCount = parseMetricField('phase_count');
|
||||
if (phaseCount === null) {
|
||||
phaseCount = (latestDraft.match(/^##\s+Phase\s+\d/gm) ?? []).length || null;
|
||||
}
|
||||
|
||||
// ── Tech stack revision rounds (conversation file only) ──
|
||||
let techStackRevisions: number | null = null;
|
||||
|
||||
// Fall back to conversation file scraping for fields not found in appendix
|
||||
if (convFiles.length > 0) {
|
||||
convFiles.sort();
|
||||
const latestConv = readFileSafe(convFiles[convFiles.length - 1]) ?? '';
|
||||
if (clarificationRounds === null) {
|
||||
const clarRounds = (latestConv.match(/^##\s+(?:Clarification|Round)\s+\d/gm) ?? []).length;
|
||||
clarificationRounds = clarRounds || null;
|
||||
}
|
||||
const techRounds = (latestConv.match(/^##\s+(?:Tech\s+Stack|Technology)\s+Revision/gmi) ?? []).length;
|
||||
techStackRevisions = techRounds || null;
|
||||
if (verificationGaps === null) {
|
||||
const gapMatches = latestConv.match(/(?:verification\s+gap|gap\s+found|missing\s+requirement)/gi) ?? [];
|
||||
verificationGaps = gapMatches.length || null;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
present: true,
|
||||
final_confidence: finalConfidence,
|
||||
confidence_breakdown: confidenceBreakdown,
|
||||
clarification_rounds: clarificationRounds,
|
||||
tech_stack_revision_rounds: techStackRevisions,
|
||||
verification_gaps_found: verificationGaps,
|
||||
functional_requirements_count: frCount,
|
||||
non_functional_requirements_count: nfrCount,
|
||||
risk_count: riskCount,
|
||||
phase_count: phaseCount,
|
||||
};
|
||||
}
|
||||
|
||||
// ── Step 2: Document ──────────────────────────────────────────────────────────
|
||||
|
||||
export function collectStep2(specDir: string): Step2DocumentMetrics {
|
||||
const overviewPath = path.join(specDir, 'overview.md');
|
||||
const overview = readFileSafe(overviewPath);
|
||||
|
||||
if (!overview) {
|
||||
return { present: false, total_tasks: null, tasks_per_phase: null,
|
||||
phase_count: null, parallel_groups_identified: null,
|
||||
requirement_coverage_percent: null, verification_items_added: null };
|
||||
}
|
||||
|
||||
// ── Primary source: METRICS_JSON in overview.md ──
|
||||
const step2Json = extractMetricsJson(overview, 'document');
|
||||
if (step2Json) {
|
||||
const num = (key: string): number | null => {
|
||||
const v = step2Json[key];
|
||||
return typeof v === 'number' ? v : null;
|
||||
};
|
||||
const tpp = step2Json['tasks_per_phase'];
|
||||
return {
|
||||
present: true,
|
||||
total_tasks: num('total_tasks'),
|
||||
tasks_per_phase: Array.isArray(tpp) ? tpp : null,
|
||||
phase_count: num('phase_count'),
|
||||
parallel_groups_identified: num('parallel_groups_identified') ?? 0,
|
||||
requirement_coverage_percent: num('requirement_coverage_percent'),
|
||||
verification_items_added: num('verification_items_added'),
|
||||
};
|
||||
}
|
||||
|
||||
// ── Fallback: regex scraping ──
|
||||
|
||||
// Detect Parallel Execution Groups table
|
||||
const parallelGroups = (overview.match(/Parallel\s+Execution\s+Group/gi) ?? []).length;
|
||||
|
||||
// Count phase files (scanned first so we can use sum as a total_tasks fallback)
|
||||
let phaseCount = 0;
|
||||
let tasksPerPhase: number[] = [];
|
||||
try {
|
||||
const files = fs.readdirSync(specDir);
|
||||
const phaseFiles = files
|
||||
.filter(f => /^phase-\d+\.md$/i.test(f))
|
||||
.sort();
|
||||
phaseCount = phaseFiles.length;
|
||||
for (const pf of phaseFiles) {
|
||||
const content = readFileSafe(path.join(specDir, pf)) ?? '';
|
||||
const count = (content.match(/^\s*-\s+\[[ x!/?]\]\s+\*\*Task\s+\d/gm) ?? []).length;
|
||||
tasksPerPhase.push(count);
|
||||
}
|
||||
} catch {
|
||||
// leave empty
|
||||
}
|
||||
|
||||
// total_tasks priority:
|
||||
// 1. Completion Summary canonical count
|
||||
// 2. Sum of phase file task counts
|
||||
// 3. Stripped overview checkboxes (excluding Success Criteria)
|
||||
const summaryCount = parseCompletionSummaryTaskCount(overview);
|
||||
const phaseSum = tasksPerPhase.length > 0 ? tasksPerPhase.reduce((a, b) => a + b, 0) : 0;
|
||||
const strippedOverview = stripSuccessCriteriaSection(overview);
|
||||
const strippedCheckboxCount = (strippedOverview.match(/^\s*-\s+\[[ x!/?]\]/gm) ?? []).length;
|
||||
|
||||
let totalTasks: number;
|
||||
if (summaryCount) {
|
||||
totalTasks = summaryCount.total;
|
||||
} else if (phaseSum > 0) {
|
||||
totalTasks = phaseSum;
|
||||
} else {
|
||||
totalTasks = strippedCheckboxCount;
|
||||
}
|
||||
|
||||
// Requirement coverage: look for coverage percentage in overview
|
||||
let reqCoverage: number | null = null;
|
||||
const covMatch = overview.match(/[Cc]overage[:\s]+(\d{1,3})%/);
|
||||
if (covMatch) reqCoverage = parseInt(covMatch[1], 10);
|
||||
|
||||
// Verification items added (look for verification checklist items)
|
||||
const verifItems = (overview.match(/(?:verify|verification|test|check):\s*\[[ x]\]/gi) ?? []).length;
|
||||
|
||||
return {
|
||||
present: true,
|
||||
total_tasks: totalTasks || null,
|
||||
tasks_per_phase: tasksPerPhase.length > 0 ? tasksPerPhase : null,
|
||||
phase_count: phaseCount || null,
|
||||
parallel_groups_identified: parallelGroups || 0,
|
||||
requirement_coverage_percent: reqCoverage,
|
||||
verification_items_added: verifItems || null,
|
||||
};
|
||||
}
|
||||
|
||||
// ── Step 3: Implement ──────────────────────────────────────────────────────────
|
||||
|
||||
export function collectStep3(specDir: string): Step3ImplementMetrics {
|
||||
// Discover phase-*.md files
|
||||
let phaseFiles: string[] = [];
|
||||
try {
|
||||
const files = fs.readdirSync(specDir);
|
||||
phaseFiles = files
|
||||
.filter(f => /^phase-\d+\.md$/i.test(f))
|
||||
.sort();
|
||||
} catch {
|
||||
// leave empty
|
||||
}
|
||||
|
||||
// Read overview.md for Completion Summary
|
||||
const overviewPath = path.join(specDir, 'overview.md');
|
||||
const overview = readFileSafe(overviewPath);
|
||||
const summaryCount = overview ? parseCompletionSummaryTaskCount(overview) : null;
|
||||
|
||||
// ── Primary source: METRICS_JSON in overview.md (from finalize step) ──
|
||||
if (overview) {
|
||||
const step3Json = extractMetricsJson(overview, 'finalize');
|
||||
if (step3Json) {
|
||||
const num = (key: string): number | null => {
|
||||
const v = step3Json[key];
|
||||
return typeof v === 'number' ? v : null;
|
||||
};
|
||||
const total = num('tasks_total');
|
||||
const completed = num('tasks_completed');
|
||||
return {
|
||||
present: true,
|
||||
task_completion_rate: total && total > 0 && completed != null ? completed / total : null,
|
||||
tasks_completed: completed,
|
||||
tasks_total: total,
|
||||
blocker_count: num('blocker_count') ?? 0,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ── Fallback: regex scraping ──
|
||||
|
||||
// present = true if phase files exist OR overview has completion data
|
||||
const present = phaseFiles.length > 0 || summaryCount != null;
|
||||
if (!present) {
|
||||
return { present: false, task_completion_rate: null, tasks_completed: null,
|
||||
tasks_total: null, blocker_count: null };
|
||||
}
|
||||
|
||||
// Count checkboxes in phase files
|
||||
let phaseTotal = 0;
|
||||
let phaseCompleted = 0;
|
||||
let blockerCount = 0;
|
||||
for (const pf of phaseFiles) {
|
||||
const content = readFileSafe(path.join(specDir, pf)) ?? '';
|
||||
phaseTotal += (content.match(/^\s*-\s+\[[ x!/?]\]\s+\*\*Task\s+\d/gm) ?? []).length;
|
||||
phaseCompleted += (content.match(/^\s*-\s+\[x\]\s+\*\*Task\s+\d/gm) ?? []).length;
|
||||
blockerCount += (content.match(/^\s*-\s+\[!\]\s+\*\*Task\s+\d/gm) ?? []).length;
|
||||
}
|
||||
|
||||
// Count checkboxes in overview (stripped of Success Criteria)
|
||||
let overviewTotal = 0;
|
||||
let overviewCompleted = 0;
|
||||
if (overview) {
|
||||
const stripped = stripSuccessCriteriaSection(overview);
|
||||
overviewTotal = (stripped.match(/^\s*-\s+\[[ x!/?]\]/gm) ?? []).length;
|
||||
overviewCompleted = (stripped.match(/^\s*-\s+\[x\]/gm) ?? []).length;
|
||||
}
|
||||
|
||||
// Task counts priority:
|
||||
// 1. Completion Summary in overview.md
|
||||
// 2. Checkbox counting in phase files
|
||||
// 3. Checkbox counting in overview.md (stripped of Success Criteria)
|
||||
let tasksTotal: number | null = null;
|
||||
let tasksCompleted: number | null = null;
|
||||
if (summaryCount) {
|
||||
tasksTotal = summaryCount.total;
|
||||
tasksCompleted = summaryCount.completed;
|
||||
} else if (phaseTotal > 0) {
|
||||
tasksTotal = phaseTotal;
|
||||
tasksCompleted = phaseCompleted;
|
||||
} else if (overviewTotal > 0) {
|
||||
tasksTotal = overviewTotal;
|
||||
tasksCompleted = overviewCompleted;
|
||||
}
|
||||
|
||||
const taskCompletionRate = tasksTotal && tasksTotal > 0 && tasksCompleted != null
|
||||
? tasksCompleted / tasksTotal
|
||||
: null;
|
||||
|
||||
return {
|
||||
present: true,
|
||||
task_completion_rate: taskCompletionRate,
|
||||
tasks_completed: tasksCompleted,
|
||||
tasks_total: tasksTotal,
|
||||
blocker_count: blockerCount,
|
||||
};
|
||||
}
|
||||
|
||||
// ── Step 4: Finalize ──────────────────────────────────────────────────────────
|
||||
|
||||
export function collectStep4(specDir: string): Step4FinalizeMetrics {
|
||||
// Check if spec was archived (specs--completed exists at parent level)
|
||||
const specDirName = path.basename(specDir);
|
||||
const parentDir = path.dirname(specDir);
|
||||
const completedPath = path.join(parentDir, '..', 'specs--completed', specDirName);
|
||||
const archivalSucceeded = fs.existsSync(completedPath);
|
||||
|
||||
// Read overview for completion metrics
|
||||
const overviewPath = path.join(specDir, 'overview.md');
|
||||
|
||||
// If archived, try from archived location
|
||||
const effectiveOverviewPath = archivalSucceeded
|
||||
? path.join(completedPath, 'overview.md')
|
||||
: overviewPath;
|
||||
|
||||
const overview = readFileSafe(effectiveOverviewPath) ?? readFileSafe(overviewPath);
|
||||
if (!overview) {
|
||||
return { present: false, completion_rate_at_audit: null,
|
||||
verification_failures_found: null, documentation_updates_needed: null,
|
||||
archival_succeeded: archivalSucceeded };
|
||||
}
|
||||
|
||||
// ── Primary source: METRICS_JSON in overview.md ──
|
||||
const step4Json = extractMetricsJson(overview, 'finalize');
|
||||
if (step4Json) {
|
||||
const num = (key: string): number | null => {
|
||||
const v = step4Json[key];
|
||||
return typeof v === 'number' ? v : null;
|
||||
};
|
||||
return {
|
||||
present: true,
|
||||
completion_rate_at_audit: num('completion_rate_at_audit'),
|
||||
verification_failures_found: num('verification_failures_found') ?? 0,
|
||||
documentation_updates_needed: num('documentation_updates_needed'),
|
||||
archival_succeeded: archivalSucceeded,
|
||||
};
|
||||
}
|
||||
|
||||
// ── Fallback: regex scraping ──
|
||||
|
||||
// Completion rate: prefer Completion Summary, fall back to stripped overview checkboxes
|
||||
let completionRate: number | null = null;
|
||||
const summaryCount = parseCompletionSummaryTaskCount(overview);
|
||||
if (summaryCount) {
|
||||
completionRate = summaryCount.total > 0 ? summaryCount.completed / summaryCount.total : null;
|
||||
} else {
|
||||
const stripped = stripSuccessCriteriaSection(overview);
|
||||
const totalTasks = (stripped.match(/^\s*-\s+\[[ x!/?]\]/gm) ?? []).length;
|
||||
const completedTasks = (stripped.match(/^\s*-\s+\[x\]/gm) ?? []).length;
|
||||
completionRate = totalTasks > 0 ? completedTasks / totalTasks : null;
|
||||
}
|
||||
|
||||
// Verification failures: look for [!] tasks or "verification failed" text
|
||||
const blockedTasks = (overview.match(/^\s*-\s+\[!\]/gm) ?? []).length;
|
||||
|
||||
// Documentation updates needed (look for TODO or "update" markers)
|
||||
const docUpdates = (overview.match(/(?:TODO|FIXME|update\s+(?:README|CHANGELOG|docs))/gi) ?? []).length;
|
||||
|
||||
return {
|
||||
present: true,
|
||||
completion_rate_at_audit: completionRate,
|
||||
verification_failures_found: blockedTasks || 0,
|
||||
documentation_updates_needed: docUpdates || null,
|
||||
archival_succeeded: archivalSucceeded,
|
||||
};
|
||||
}
|
||||
|
||||
// ── User Feedback ─────────────────────────────────────────────────────────
|
||||
|
||||
export function collectUserFeedback(specDir: string): UserFeedback | null {
|
||||
// Try both active and archived locations for overview.md
|
||||
const overviewPath = path.join(specDir, 'overview.md');
|
||||
const specDirName = path.basename(specDir);
|
||||
const parentDir = path.dirname(specDir);
|
||||
const completedPath = path.join(parentDir, '..', 'specs--completed', specDirName, 'overview.md');
|
||||
|
||||
const overview = readFileSafe(overviewPath) ?? readFileSafe(completedPath);
|
||||
if (!overview) return null;
|
||||
|
||||
// Look for ## User Feedback section with a markdown table
|
||||
const feedbackMatch = overview.match(
|
||||
/## User Feedback\s*\n\s*\|[^\n]*\|\s*\n\s*\|[-| ]+\|\s*\n([\s\S]*?)(?=\n##\s|\n*$)/
|
||||
);
|
||||
if (!feedbackMatch) return null;
|
||||
|
||||
const tableBody = feedbackMatch[1];
|
||||
|
||||
// Parse table rows: | Field | Value |
|
||||
// Use regex to split only on unescaped pipes, preserving whitespace in values
|
||||
const rows = tableBody.split('\n').filter(l => l.trim().startsWith('|'));
|
||||
const fields: Record<string, string> = {};
|
||||
for (const row of rows) {
|
||||
// Split on unescaped pipes (not preceded by backslash)
|
||||
const cells = row.split(/(?<!\\)\|/).map(c => c.trim()).filter(c => c.length > 0);
|
||||
if (cells.length >= 2) {
|
||||
const value = cells.slice(1).join('|').replace(/\\\|/g, '|');
|
||||
fields[cells[0].toLowerCase()] = value;
|
||||
}
|
||||
}
|
||||
|
||||
const rating = parseInt(fields['rating'] ?? '', 10);
|
||||
if (isNaN(rating) || rating < 1 || rating > 10) return null;
|
||||
|
||||
return {
|
||||
overall_rating: rating,
|
||||
rating_reason: fields['reason'] ?? '',
|
||||
what_went_well: fields['went well'] ?? '',
|
||||
what_went_poorly: fields['went poorly'] ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
// ── Main collector ────────────────────────────────────────────────────────────
|
||||
|
||||
export interface CollectorOptions {
|
||||
specDir: string; // Path to the spec directory (specs/<feature-name>)
|
||||
projectName: string; // Human-readable project name
|
||||
plan2codeRoot: string; // Path to the plan2code repo (for prompt hashing)
|
||||
plan2codeVersion: string; // e.g. "1.7.0"
|
||||
outputDir: string; // Where to write <run-id>.json
|
||||
}
|
||||
|
||||
export async function collectRun(opts: CollectorOptions): Promise<RunMetrics> {
|
||||
const {
|
||||
specDir,
|
||||
projectName,
|
||||
plan2codeRoot,
|
||||
plan2codeVersion,
|
||||
outputDir,
|
||||
} = opts;
|
||||
|
||||
const runId = generateRunId();
|
||||
|
||||
// Get started_at / completed_at from spec directory mtime / iteration.log
|
||||
let startedAt: string | null = null;
|
||||
let completedAt: string | null = null;
|
||||
try {
|
||||
const stat = fs.statSync(specDir);
|
||||
startedAt = stat.birthtime.toISOString();
|
||||
completedAt = stat.mtime.toISOString();
|
||||
} catch {
|
||||
// leave null
|
||||
}
|
||||
|
||||
const metrics: RunMetrics = {
|
||||
schema_version: '1.0',
|
||||
run_id: runId,
|
||||
plan2code_version: plan2codeVersion,
|
||||
prompt_versions: collectPromptVersions(plan2codeRoot),
|
||||
project: {
|
||||
name: projectName,
|
||||
started_at: startedAt,
|
||||
completed_at: completedAt,
|
||||
},
|
||||
step1_plan: collectStep1(specDir),
|
||||
step2_document: collectStep2(specDir),
|
||||
step3_implement: collectStep3(specDir),
|
||||
step4_finalize: collectStep4(specDir),
|
||||
user_feedback: collectUserFeedback(specDir),
|
||||
};
|
||||
|
||||
// Write to output dir
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
const outPath = path.join(outputDir, `${runId}.json`);
|
||||
fs.writeFileSync(outPath, JSON.stringify(metrics, null, 2), 'utf8');
|
||||
|
||||
return metrics;
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { validateEdit, parseProposalFromResponse } from './improver.js';
|
||||
import type { PromptEdit } from './types.js';
|
||||
|
||||
// ── Shared fixtures ───────────────────────────────────────────────────────────
|
||||
|
||||
const PROMPT_CONTENTS: Record<string, string> = {
|
||||
'plan2code-1-plan.md': 'This is the plan prompt content. It has some text here.',
|
||||
'plan2code-2-document.md': 'Document prompt with repeated text. repeated text. Done.',
|
||||
'plan2code-3-implement.md': 'Implement prompt content.',
|
||||
};
|
||||
|
||||
function makeEdit(overrides: Partial<PromptEdit> = {}): PromptEdit {
|
||||
return {
|
||||
file: 'plan2code-1-plan.md',
|
||||
rationale: 'test rationale',
|
||||
expected_metric_impact: 'test impact',
|
||||
char_count_before: PROMPT_CONTENTS['plan2code-1-plan.md'].length,
|
||||
char_count_after: PROMPT_CONTENTS['plan2code-1-plan.md'].length,
|
||||
char_count_delta: 0,
|
||||
old_text: 'some text',
|
||||
new_text: 'better text',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
// ── validateEdit() ────────────────────────────────────────────────────────────
|
||||
|
||||
describe('validateEdit', () => {
|
||||
it('valid edit passes with no errors', () => {
|
||||
const result = validateEdit(makeEdit(), PROMPT_CONTENTS);
|
||||
expect(result.valid).toBe(true);
|
||||
expect(result.errors).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('rejects path traversal (../ in file path)', () => {
|
||||
const result = validateEdit(makeEdit({ file: '../etc/passwd' }), PROMPT_CONTENTS);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors[0]).toMatch(/path traversal/i);
|
||||
});
|
||||
|
||||
it('rejects absolute paths', () => {
|
||||
const result = validateEdit(makeEdit({ file: '/etc/passwd' }), PROMPT_CONTENTS);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors[0]).toMatch(/path traversal|absolute/i);
|
||||
});
|
||||
|
||||
it('errors when file not found in promptContents', () => {
|
||||
const result = validateEdit(makeEdit({ file: 'nonexistent.md' }), PROMPT_CONTENTS);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors[0]).toMatch(/not found/i);
|
||||
});
|
||||
|
||||
it('errors when old_text not found in file content', () => {
|
||||
const result = validateEdit(makeEdit({ old_text: 'hallucinated text' }), PROMPT_CONTENTS);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors[0]).toMatch(/not found verbatim/i);
|
||||
});
|
||||
|
||||
it('warns when old_text appears multiple times', () => {
|
||||
const edit = makeEdit({
|
||||
file: 'plan2code-2-document.md',
|
||||
old_text: 'repeated text',
|
||||
char_count_before: PROMPT_CONTENTS['plan2code-2-document.md'].length,
|
||||
char_count_after: PROMPT_CONTENTS['plan2code-2-document.md'].length,
|
||||
});
|
||||
const result = validateEdit(edit, PROMPT_CONTENTS);
|
||||
expect(result.valid).toBe(true);
|
||||
expect(result.warnings.some(w => /appears.*times/i.test(w))).toBe(true);
|
||||
});
|
||||
|
||||
it('errors when edit would exceed 11,000 char limit', () => {
|
||||
const bigText = 'x'.repeat(12_000);
|
||||
const edit = makeEdit({ new_text: bigText });
|
||||
const result = validateEdit(edit, PROMPT_CONTENTS);
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors.some(e => /exceed.*11.?000/i.test(e))).toBe(true);
|
||||
});
|
||||
|
||||
it('warns when reported char counts diverge from actual (>10 chars off)', () => {
|
||||
const edit = makeEdit({
|
||||
char_count_before: 999,
|
||||
char_count_after: 999,
|
||||
});
|
||||
const result = validateEdit(edit, PROMPT_CONTENTS);
|
||||
expect(result.valid).toBe(true);
|
||||
expect(result.warnings.some(w => /char_count_before.*differs/i.test(w))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// ── parseProposalFromResponse() ───────────────────────────────────────────────
|
||||
|
||||
describe('parseProposalFromResponse', () => {
|
||||
const sampleEdit = {
|
||||
file: 'test.md',
|
||||
rationale: 'r',
|
||||
expected_metric_impact: 'e',
|
||||
char_count_before: 100,
|
||||
char_count_after: 110,
|
||||
char_count_delta: 10,
|
||||
old_text: 'old',
|
||||
new_text: 'new',
|
||||
};
|
||||
|
||||
it('parses JSON from markdown code block (```json ... ```)', () => {
|
||||
const response = `Here is my proposal:\n\n\`\`\`json\n${JSON.stringify([sampleEdit])}\n\`\`\`\n\nDone.`;
|
||||
const result = parseProposalFromResponse(response);
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result![0].file).toBe('test.md');
|
||||
});
|
||||
|
||||
it('parses JSON from bare code block (``` ... ```)', () => {
|
||||
const response = `Proposal:\n\n\`\`\`\n${JSON.stringify([sampleEdit])}\n\`\`\``;
|
||||
const result = parseProposalFromResponse(response);
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result![0].old_text).toBe('old');
|
||||
});
|
||||
|
||||
it('parses bare JSON array with old_text field', () => {
|
||||
const response = `Some preamble\n${JSON.stringify([sampleEdit])}\nSome postamble`;
|
||||
const result = parseProposalFromResponse(response);
|
||||
expect(result).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('returns null for non-JSON response', () => {
|
||||
const result = parseProposalFromResponse('No changes needed at this time.');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null for malformed JSON', () => {
|
||||
const result = parseProposalFromResponse('```json\n{broken json]\n```');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null for empty response', () => {
|
||||
const result = parseProposalFromResponse('');
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,288 @@
|
||||
/**
|
||||
* improver.ts
|
||||
* Reads diagnosis + prompt files, invokes AI, parses PromptEdit[] from response.
|
||||
* Validates: old_text verbatim match, char count limits.
|
||||
*/
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import type { PromptEdit, PromptProposal } from './types.js';
|
||||
import { invokeLLM, type AgentType } from './invoke-llm.js';
|
||||
|
||||
const CHAR_LIMIT = 11_000;
|
||||
const IMPROVE_PROMPT_PATH = new URL('../src/prompts/improve.md', import.meta.url).pathname
|
||||
.replace(/^\/([A-Za-z]:)/, '$1'); // Fix Windows path
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
function interpolate(template: string, vars: Record<string, string>): string {
|
||||
let result = template;
|
||||
for (const [key, value] of Object.entries(vars)) {
|
||||
result = result.replaceAll(`{{${key}}}`, value);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function generateProposalId(): string {
|
||||
const now = new Date();
|
||||
const ts = now.toISOString().replace(/[-:T.Z]/g, '').slice(0, 14);
|
||||
return `prop-${ts}`;
|
||||
}
|
||||
|
||||
function readPromptFiles(plan2codeRoot: string): Record<string, string> {
|
||||
const srcDir = path.join(plan2codeRoot, 'src');
|
||||
const promptFiles = [
|
||||
'plan2code-1-plan.md',
|
||||
'plan2code-1b-revise-plan.md',
|
||||
'plan2code-2-document.md',
|
||||
'plan2code-3-implement.md',
|
||||
'plan2code-4-finalize.md',
|
||||
'plan2code-init.md',
|
||||
'plan2code-init-update.md',
|
||||
'plan2code-quick-task.md',
|
||||
];
|
||||
|
||||
const contents: Record<string, string> = {};
|
||||
for (const file of promptFiles) {
|
||||
try {
|
||||
contents[file] = fs.readFileSync(path.join(srcDir, file), 'utf8');
|
||||
} catch {
|
||||
contents[file] = '';
|
||||
}
|
||||
}
|
||||
return contents;
|
||||
}
|
||||
|
||||
// ── Edit validation ───────────────────────────────────────────────────────────
|
||||
|
||||
export interface ValidationResult {
|
||||
valid: boolean;
|
||||
errors: string[];
|
||||
warnings: string[];
|
||||
}
|
||||
|
||||
export function validateEdit(
|
||||
edit: PromptEdit,
|
||||
promptContents: Record<string, string>,
|
||||
): ValidationResult {
|
||||
const errors: string[] = [];
|
||||
const warnings: string[] = [];
|
||||
|
||||
// Reject path traversal attempts
|
||||
if (edit.file.includes('..') || path.isAbsolute(edit.file)) {
|
||||
errors.push(`Rejected: "${edit.file}" contains path traversal or absolute path.`);
|
||||
return { valid: false, errors, warnings };
|
||||
}
|
||||
|
||||
// Check target file exists
|
||||
const fileContent = promptContents[edit.file];
|
||||
if (fileContent === undefined) {
|
||||
errors.push(`Target file "${edit.file}" not found. Valid files: ${Object.keys(promptContents).join(', ')}`);
|
||||
return { valid: false, errors, warnings };
|
||||
}
|
||||
|
||||
// Check old_text exists verbatim in the file
|
||||
if (!fileContent.includes(edit.old_text)) {
|
||||
errors.push(`old_text not found verbatim in "${edit.file}". The AI may have hallucinated text.`);
|
||||
} else {
|
||||
// Warn if old_text appears more than once (ambiguous match)
|
||||
const occurrences = fileContent.split(edit.old_text).length - 1;
|
||||
if (occurrences > 1) {
|
||||
warnings.push(`old_text appears ${occurrences} times in "${edit.file}". Only the first occurrence will be replaced.`);
|
||||
}
|
||||
}
|
||||
|
||||
// Check char count after edit
|
||||
const afterContent = fileContent.replace(edit.old_text, edit.new_text);
|
||||
if (afterContent.length > CHAR_LIMIT) {
|
||||
errors.push(`Edit would cause "${edit.file}" to exceed ${CHAR_LIMIT} char limit (would be ${afterContent.length} chars).`);
|
||||
}
|
||||
|
||||
// Verify reported char counts match reality
|
||||
const actualBefore = fileContent.length;
|
||||
const actualAfter = afterContent.length;
|
||||
if (Math.abs(edit.char_count_before - actualBefore) > 10) {
|
||||
warnings.push(`Reported char_count_before (${edit.char_count_before}) differs from actual (${actualBefore}).`);
|
||||
}
|
||||
if (Math.abs(edit.char_count_after - actualAfter) > 10) {
|
||||
warnings.push(`Reported char_count_after (${edit.char_count_after}) differs from actual (${actualAfter}).`);
|
||||
}
|
||||
|
||||
return { valid: errors.length === 0, errors, warnings };
|
||||
}
|
||||
|
||||
// ── AI response parsing ───────────────────────────────────────────────────────
|
||||
|
||||
export function parseProposalFromResponse(response: string): PromptEdit[] | null {
|
||||
// Look for JSON code block containing PromptEdit[]
|
||||
const jsonBlockMatch = response.match(/```(?:json)?\s*(\[[\s\S]*?\])\s*```/);
|
||||
if (!jsonBlockMatch) {
|
||||
// Try bare JSON array
|
||||
const bareMatch = response.match(/(\[[\s\S]*"old_text"[\s\S]*\])/);
|
||||
if (!bareMatch) return null;
|
||||
try {
|
||||
return JSON.parse(bareMatch[1]) as PromptEdit[];
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
return JSON.parse(jsonBlockMatch[1]) as PromptEdit[];
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Main improver ─────────────────────────────────────────────────────────────
|
||||
|
||||
export interface ImproverOptions {
|
||||
diagnosisPath: string; // Path to diagnosis markdown file
|
||||
plan2codeRoot: string; // Path to plan2code repo root
|
||||
proposalsDir: string; // Where to save proposal JSON
|
||||
runsDir: string; // For tracking which runs this is based on
|
||||
model?: string;
|
||||
agent?: AgentType; // Agent to use (default: claude-code)
|
||||
}
|
||||
|
||||
export interface ImproverResult {
|
||||
proposalPath: string;
|
||||
proposal: PromptProposal;
|
||||
validationResults: Array<{ edit: PromptEdit; result: ValidationResult }>;
|
||||
validEditCount: number;
|
||||
invalidEditCount: number;
|
||||
}
|
||||
|
||||
export async function generateImprovement(opts: ImproverOptions): Promise<ImproverResult> {
|
||||
const { diagnosisPath, plan2codeRoot, proposalsDir, runsDir, model = 'default', agent = 'claude-code' } = opts;
|
||||
|
||||
// Load diagnosis
|
||||
let diagnosisContent: string;
|
||||
try {
|
||||
diagnosisContent = fs.readFileSync(diagnosisPath, 'utf8');
|
||||
} catch {
|
||||
throw new Error(`Could not read diagnosis file at ${diagnosisPath}`);
|
||||
}
|
||||
|
||||
// Read prompt files
|
||||
const promptContents = readPromptFiles(plan2codeRoot);
|
||||
const srcDir = path.join(plan2codeRoot, 'src');
|
||||
|
||||
// Build char counts for each file
|
||||
const charCounts = Object.entries(promptContents)
|
||||
.map(([file, content]) => `| ${file} | ${content.length} | ${CHAR_LIMIT} | ${CHAR_LIMIT - content.length} headroom |`)
|
||||
.join('\n');
|
||||
|
||||
const promptContentsStr = Object.entries(promptContents)
|
||||
.map(([file, content]) => `## ${file} (${content.length} chars)\n\n${content}`)
|
||||
.join('\n\n---\n\n');
|
||||
|
||||
// Load improve prompt template
|
||||
let improveTemplate: string;
|
||||
try {
|
||||
improveTemplate = fs.readFileSync(IMPROVE_PROMPT_PATH, 'utf8');
|
||||
} catch {
|
||||
const altPath = path.join(process.cwd(), 'src', 'prompts', 'improve.md');
|
||||
improveTemplate = fs.readFileSync(altPath, 'utf8');
|
||||
}
|
||||
|
||||
const fullPrompt = interpolate(improveTemplate, {
|
||||
diagnosisContent,
|
||||
promptContents: promptContentsStr,
|
||||
charCounts: `| File | Current Chars | Limit | Headroom |\n|------|--------------|-------|----------|\n${charCounts}`,
|
||||
});
|
||||
|
||||
// Invoke Claude
|
||||
console.log(`\nInvoking AI improvement proposal (agent: ${agent}, model: ${model === 'default' ? 'user default' : model})...`);
|
||||
console.log('This may take a minute...\n');
|
||||
|
||||
let aiResponse: string;
|
||||
try {
|
||||
aiResponse = await invokeLLM({
|
||||
prompt: fullPrompt,
|
||||
model,
|
||||
agent,
|
||||
timeout: 300_000,
|
||||
});
|
||||
} catch (err) {
|
||||
throw new Error(`AI invocation failed: ${err instanceof Error ? err.message : String(err)}`);
|
||||
}
|
||||
|
||||
// Parse edits
|
||||
const rawEdits = parseProposalFromResponse(aiResponse);
|
||||
if (!rawEdits || rawEdits.length === 0) {
|
||||
throw new Error('Could not parse PromptEdit[] from AI response. The AI may not have produced a valid JSON block.');
|
||||
}
|
||||
|
||||
// Enforce max edits per cycle
|
||||
const MAX_EDITS = 5;
|
||||
if (rawEdits.length > MAX_EDITS) {
|
||||
console.warn(`\n⚠ AI generated ${rawEdits.length} edits (max is ${MAX_EDITS}). Truncating to first ${MAX_EDITS}.`);
|
||||
rawEdits.length = MAX_EDITS;
|
||||
}
|
||||
|
||||
// Validate each edit
|
||||
const validationResults: ImproverResult['validationResults'] = [];
|
||||
const validEdits: PromptEdit[] = [];
|
||||
|
||||
for (const edit of rawEdits) {
|
||||
const result = validateEdit(edit, promptContents);
|
||||
validationResults.push({ edit, result });
|
||||
|
||||
if (result.valid) {
|
||||
// Compute accurate char counts
|
||||
const fileContent = promptContents[edit.file] ?? '';
|
||||
const afterContent = fileContent.replace(edit.old_text, edit.new_text);
|
||||
edit.char_count_before = fileContent.length;
|
||||
edit.char_count_after = afterContent.length;
|
||||
edit.char_count_delta = afterContent.length - fileContent.length;
|
||||
validEdits.push(edit);
|
||||
} else {
|
||||
console.warn(`\n⚠ Edit rejected for "${edit.file}":`);
|
||||
for (const err of result.errors) {
|
||||
console.warn(` - ${err}`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const warn of result.warnings) {
|
||||
console.warn(` Warning: ${warn}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Get run IDs that contributed to this analysis
|
||||
const runIds: string[] = [];
|
||||
try {
|
||||
const files = fs.readdirSync(runsDir)
|
||||
.filter(f => f.startsWith('run-') && f.endsWith('.json'));
|
||||
runIds.push(...files.map(f => f.replace('.json', '')));
|
||||
} catch { /* no runs dir */ }
|
||||
|
||||
// Build proposal
|
||||
const proposalId = generateProposalId();
|
||||
const proposal: PromptProposal = {
|
||||
proposal_id: proposalId,
|
||||
created_at: new Date().toISOString(),
|
||||
based_on_runs: runIds,
|
||||
analyst_model: model,
|
||||
proposals: validEdits,
|
||||
status: 'pending',
|
||||
diagnosis_file: path.basename(diagnosisPath),
|
||||
};
|
||||
|
||||
// Save proposal JSON
|
||||
fs.mkdirSync(proposalsDir, { recursive: true });
|
||||
const proposalPath = path.join(proposalsDir, `${proposalId}.json`);
|
||||
fs.writeFileSync(proposalPath, JSON.stringify(proposal, null, 2), 'utf8');
|
||||
|
||||
// Also save raw AI response alongside
|
||||
const rawPath = path.join(proposalsDir, `${proposalId}-raw.md`);
|
||||
fs.writeFileSync(rawPath, aiResponse, 'utf8');
|
||||
|
||||
return {
|
||||
proposalPath,
|
||||
proposal,
|
||||
validationResults,
|
||||
validEditCount: validEdits.length,
|
||||
invalidEditCount: rawEdits.length - validEdits.length,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// Public API for plan2code-metrics
|
||||
export { collectRun, collectPromptVersions } from './collector.js';
|
||||
export { aggregate, loadAggregated, loadRunFiles, importRun } from './aggregator.js';
|
||||
export { runAnalysis } from './analyzer.js';
|
||||
export { generateImprovement, validateEdit, parseProposalFromResponse } from './improver.js';
|
||||
export { reviewAndApply } from './applier.js';
|
||||
export { invokeLLM, AGENTS } from './invoke-llm.js';
|
||||
export type { AgentType, InvokeLLMOptions } from './invoke-llm.js';
|
||||
export { runCLI } from './cli.js';
|
||||
export { METRIC_TARGETS } from './types.js';
|
||||
export type {
|
||||
RunMetrics,
|
||||
UserFeedback,
|
||||
PromptVersions,
|
||||
PromptEdit,
|
||||
PromptProposal,
|
||||
AggregatedMetrics,
|
||||
CohortMetrics,
|
||||
} from './types.js';
|
||||
@@ -0,0 +1,90 @@
|
||||
/**
|
||||
* invoke-llm.ts
|
||||
* Unified LLM invocation for plan2code-metrics.
|
||||
* Supports Claude Code (temp file → stdin) and Copilot CLI (stdin string).
|
||||
* Mirrors the agent pattern from plan2code-loop.
|
||||
*/
|
||||
|
||||
import { execa } from 'execa';
|
||||
import { writeFileSync, unlinkSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { tmpdir } from 'os';
|
||||
|
||||
// ── Agent definitions ────────────────────────────────────────────────────────
|
||||
|
||||
export type AgentType = 'claude-code' | 'copilot-cli';
|
||||
|
||||
export interface AgentDef {
|
||||
name: AgentType;
|
||||
displayName: string;
|
||||
command: string;
|
||||
defaultModel: string;
|
||||
}
|
||||
|
||||
export const AGENTS: Record<AgentType, AgentDef> = {
|
||||
'claude-code': {
|
||||
name: 'claude-code',
|
||||
displayName: 'Claude Code',
|
||||
command: 'claude',
|
||||
defaultModel: 'default',
|
||||
},
|
||||
'copilot-cli': {
|
||||
name: 'copilot-cli',
|
||||
displayName: 'GitHub Copilot CLI',
|
||||
command: 'copilot',
|
||||
defaultModel: 'claude-sonnet-4',
|
||||
},
|
||||
};
|
||||
|
||||
// ── Invocation ───────────────────────────────────────────────────────────────
|
||||
|
||||
export interface InvokeLLMOptions {
|
||||
prompt: string;
|
||||
model: string;
|
||||
agent: AgentType;
|
||||
timeout?: number;
|
||||
}
|
||||
|
||||
export async function invokeLLM(opts: InvokeLLMOptions): Promise<string> {
|
||||
const { prompt, model, agent, timeout = 300_000 } = opts;
|
||||
const def = AGENTS[agent];
|
||||
|
||||
if (agent === 'claude-code') {
|
||||
// Write prompt to temp file — more reliable than stdin on Windows
|
||||
const tempFile = join(tmpdir(), `plan2code-metrics-prompt-${Date.now()}.txt`);
|
||||
writeFileSync(tempFile, prompt, 'utf-8');
|
||||
|
||||
try {
|
||||
const args: string[] = [
|
||||
'--print',
|
||||
'--dangerously-skip-permissions',
|
||||
];
|
||||
// Only add --model if not using default
|
||||
if (model && model !== 'default') {
|
||||
args.push('--model', model);
|
||||
}
|
||||
|
||||
const result = await execa(def.command, args, {
|
||||
inputFile: tempFile,
|
||||
timeout,
|
||||
});
|
||||
return result.stdout;
|
||||
} finally {
|
||||
try { unlinkSync(tempFile); } catch { /* ignore cleanup errors */ }
|
||||
}
|
||||
} else {
|
||||
// Copilot CLI: pipe prompt via stdin string
|
||||
const args: string[] = [];
|
||||
// Only add --model if not using default
|
||||
if (model && model !== 'default') {
|
||||
args.push('--model', model);
|
||||
}
|
||||
args.push('--allow-all-tools', '-s');
|
||||
|
||||
const result = await execa(def.command, args, {
|
||||
input: prompt,
|
||||
timeout,
|
||||
});
|
||||
return result.stdout;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
# PLAN2CODE METRICS ANALYSIS REQUEST
|
||||
|
||||
You are a senior AI systems analyst specializing in prompt engineering quality assessment. Your role is to diagnose weaknesses in the plan2code workflow prompts by examining aggregated run metrics.
|
||||
|
||||
**IMPORTANT:** Do NOT propose specific edits in this response. Diagnosis only. The improvement step is separate.
|
||||
|
||||
---
|
||||
|
||||
## Aggregated Run Metrics
|
||||
|
||||
The following JSON contains metrics aggregated from real plan2code project runs, grouped by prompt "generation" (a cohort is identified by the SHA fingerprint of the src/plan2code-*.md prompt files at collection time):
|
||||
|
||||
```json
|
||||
{{aggregatedMetrics}}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Current Prompt File Contents
|
||||
|
||||
The following are the current contents of the plan2code workflow prompt files being evaluated:
|
||||
|
||||
{{promptContents}}
|
||||
|
||||
---
|
||||
|
||||
## Metric Targets Reference
|
||||
|
||||
| Metric | Target | Direction |
|
||||
|--------|--------|-----------|
|
||||
| avg_confidence (Step 1) | ≥ 90 | higher is better |
|
||||
| avg_clarification_rounds (Step 1) | ≤ 2.0 | lower is better |
|
||||
| avg_verification_gaps_found (Step 1) | ≤ 2.0 | lower is better |
|
||||
| avg_parallel_groups (Step 2) | ≥ 0.5 | higher is better |
|
||||
| avg_verification_items_added (Step 2) | ≤ 1.5 | lower is better |
|
||||
| avg_task_completion_rate (Step 3) | ≥ 0.95 | higher is better |
|
||||
| avg_blocker_count (Step 3) | ≤ 1.5 | lower is better |
|
||||
| avg_verification_failures_found (Step 4) | ≤ 1.0 | lower is better |
|
||||
| archival_success_rate (Step 4) | ≥ 0.99 | higher is better |
|
||||
| avg_user_rating (Feedback) | ≥ 7.0 | higher is better (1-10 scale, null if no feedback) |
|
||||
|
||||
---
|
||||
|
||||
## Analysis Instructions
|
||||
|
||||
1. Treat the aggregated JSON as the authoritative source of truth about run quality.
|
||||
2. Compare each metric against its target. Calculate delta (actual − target).
|
||||
3. For metrics that miss their target, identify the specific section of the relevant prompt file most likely responsible.
|
||||
4. Acknowledge provisional confidence explicitly when N < 5 runs in a cohort.
|
||||
5. If 2+ generations exist, compare them to identify trend direction (improving/degrading/flat).
|
||||
6. Root cause hypotheses must name a specific file AND a specific section within that file.
|
||||
7. Do not invent metrics not present in the JSON. If a metric is null, note it as "insufficient data."
|
||||
|
||||
---
|
||||
|
||||
## Required Output Format
|
||||
|
||||
Produce EXACTLY the following sections in order. Use these exact headers — they are parsed by machine:
|
||||
|
||||
# PLAN2CODE METRICS DIAGNOSIS
|
||||
|
||||
### Metrics Summary
|
||||
|
||||
A markdown table with columns: Step | Metric | Target | Actual | Delta | Status (✓/✗/—)
|
||||
|
||||
Include ALL metrics listed in the targets table. Use "—" for null values.
|
||||
|
||||
### Step Health Assessment
|
||||
|
||||
For each step (1–4), provide:
|
||||
- **Grade:** A–F
|
||||
- **Key signals:** 2–4 bullet points with specific metric values
|
||||
- **Assessment:** 1–2 sentence diagnosis
|
||||
|
||||
### Root Cause Hypotheses
|
||||
|
||||
Numbered list. For each underperforming metric:
|
||||
1. **Metric:** [metric name] | **Value:** [actual] | **Target:** [target]
|
||||
- **File:** [plan2code-X-name.md]
|
||||
- **Section:** [specific heading or section name]
|
||||
- **Hypothesis:** [specific gap in the prompt that would explain the metric miss]
|
||||
- **Confidence:** [High/Medium/Low] — [reason for confidence level]
|
||||
|
||||
### Recommended Improvement Targets
|
||||
|
||||
Ordered list (highest estimated impact first). For each:
|
||||
- **File:** [filename]
|
||||
- **Section:** [section name]
|
||||
- **Why:** [link to specific metric being addressed]
|
||||
- **Priority:** [High/Medium/Low]
|
||||
|
||||
### Generation Comparison
|
||||
|
||||
If 2+ generations exist: A comparison table showing before/after for each metric per generation, with trend arrows (▲/▼/→).
|
||||
|
||||
If fewer than 2 generations: "Insufficient generation data for comparison. Current generation: [cohort_key], [N] runs."
|
||||
|
||||
---
|
||||
|
||||
End of analysis request.
|
||||
@@ -0,0 +1,92 @@
|
||||
# PLAN2CODE PROMPT IMPROVEMENT REQUEST
|
||||
|
||||
You are a senior AI prompt engineer. Your role is to propose surgical, targeted edits to the plan2code workflow prompt files based on a metrics diagnosis. You make precise, minimal changes — NOT rewrites.
|
||||
|
||||
---
|
||||
|
||||
## Metrics Diagnosis
|
||||
|
||||
The following diagnosis was produced by the analysis step:
|
||||
|
||||
{{diagnosisContent}}
|
||||
|
||||
---
|
||||
|
||||
## Current Prompt File Contents (with char counts)
|
||||
|
||||
{{promptContents}}
|
||||
|
||||
---
|
||||
|
||||
## Character Count Status
|
||||
|
||||
{{charCounts}}
|
||||
|
||||
---
|
||||
|
||||
## Hard Constraints — ALL must be satisfied:
|
||||
|
||||
1. **Char limit:** Each target file MUST stay under 11,000 characters after your edit is applied. This is enforced by code — edits that violate it will be automatically rejected.
|
||||
2. **Maximum 5 edits per cycle.** Focus on the highest-impact changes only.
|
||||
3. **Each edit must cite a specific metric** in its `expected_metric_impact` field (e.g., "avg_task_completion_rate", "avg_blocker_count").
|
||||
4. **`old_text` must be verbatim** from the file. Copy-paste exactly — include surrounding whitespace/newlines as they appear. Edits with mismatched old_text will be automatically rejected.
|
||||
5. **Do NOT modify Role sections** (lines starting with "You are" at the top of each file) or step headings (lines starting with `#`).
|
||||
6. **Each edit must be independently applicable** — no edit should depend on another edit being applied first.
|
||||
7. **Prefer additive guidance over deletions.** Adding clarifying instructions or examples is safer than removing existing text.
|
||||
8. **Do not change the overall structure** or flow of any prompt file.
|
||||
|
||||
---
|
||||
|
||||
## Edit Strategy Guidelines
|
||||
|
||||
- Target the specific sections identified in "Recommended Improvement Targets" from the diagnosis.
|
||||
- For high `avg_clarification_rounds`: Add more upfront specification examples or decision criteria to Step 1.
|
||||
- For high `avg_blocker_count`: Add blocker-recovery guidance or prerequisite check instructions.
|
||||
- For low `avg_confidence`: Strengthen the confidence calculation instructions with clearer rubrics.
|
||||
- For low `avg_parallel_groups`: Add explicit guidance for identifying parallel tasks in Step 2.
|
||||
- For low `avg_user_rating`: Review user feedback themes (what_went_well, what_went_poorly) for systemic issues.
|
||||
- Keep each `new_text` as short as possible while still addressing the root cause.
|
||||
|
||||
---
|
||||
|
||||
## Required Output Format
|
||||
|
||||
Produce EXACTLY the following sections. The JSON block is parsed by machine — it must be syntactically valid.
|
||||
|
||||
# PLAN2CODE PROMPT IMPROVEMENT PROPOSAL
|
||||
|
||||
### Improvement Rationale
|
||||
|
||||
2–3 paragraphs explaining:
|
||||
1. Which metrics are being addressed and why they matter
|
||||
2. The specific prompt gaps identified in the diagnosis that you are targeting
|
||||
3. Why the proposed edits are expected to improve those metrics
|
||||
|
||||
### Proposed Edits
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"file": "plan2code-X-name.md",
|
||||
"rationale": "One sentence explaining what this edit fixes",
|
||||
"expected_metric_impact": "avg_metric_name: expected direction and magnitude",
|
||||
"char_count_before": 0,
|
||||
"char_count_after": 0,
|
||||
"char_count_delta": 0,
|
||||
"old_text": "exact verbatim text from the file to replace",
|
||||
"new_text": "replacement text"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
Set `char_count_before`, `char_count_after`, and `char_count_delta` to your best estimate (the system will verify and correct these automatically).
|
||||
|
||||
### Character Count Verification
|
||||
|
||||
A table with columns: File | Before | Projected After | Delta | Limit | Status (✓/✗)
|
||||
|
||||
Verify that NO file exceeds 11,000 characters after your proposed edits.
|
||||
|
||||
---
|
||||
|
||||
End of improvement request.
|
||||
@@ -0,0 +1,163 @@
|
||||
// All TypeScript interfaces for plan2code-metrics
|
||||
|
||||
export interface PromptVersions {
|
||||
plan: string; // sha256:... plan2code-1-plan.md
|
||||
revise_plan: string; // plan2code-1b-revise-plan.md
|
||||
document: string; // plan2code-2-document.md
|
||||
implement: string; // plan2code-3-implement.md
|
||||
finalize: string; // plan2code-4-finalize.md
|
||||
init: string; // plan2code-init.md
|
||||
init_update: string; // plan2code-init-update.md
|
||||
quick_task: string; // plan2code-quick-task.md
|
||||
}
|
||||
|
||||
export interface Step1PlanMetrics {
|
||||
present: boolean;
|
||||
final_confidence: number | null;
|
||||
confidence_breakdown: {
|
||||
requirements: number | null;
|
||||
feasibility: number | null;
|
||||
integration: number | null;
|
||||
risk: number | null;
|
||||
} | null;
|
||||
clarification_rounds: number | null;
|
||||
tech_stack_revision_rounds: number | null;
|
||||
verification_gaps_found: number | null;
|
||||
functional_requirements_count: number | null;
|
||||
non_functional_requirements_count: number | null;
|
||||
risk_count: number | null;
|
||||
phase_count: number | null;
|
||||
}
|
||||
|
||||
export interface Step2DocumentMetrics {
|
||||
present: boolean;
|
||||
total_tasks: number | null;
|
||||
tasks_per_phase: number[] | null;
|
||||
phase_count: number | null;
|
||||
parallel_groups_identified: number | null;
|
||||
requirement_coverage_percent: number | null;
|
||||
verification_items_added: number | null;
|
||||
}
|
||||
|
||||
export interface Step3ImplementMetrics {
|
||||
present: boolean;
|
||||
task_completion_rate: number | null;
|
||||
tasks_completed: number | null;
|
||||
tasks_total: number | null;
|
||||
blocker_count: number | null;
|
||||
}
|
||||
|
||||
export interface Step4FinalizeMetrics {
|
||||
present: boolean;
|
||||
completion_rate_at_audit: number | null;
|
||||
verification_failures_found: number | null;
|
||||
documentation_updates_needed: number | null;
|
||||
archival_succeeded: boolean | null;
|
||||
}
|
||||
|
||||
export interface UserFeedback {
|
||||
overall_rating: number; // 1-10
|
||||
rating_reason: string;
|
||||
what_went_well: string;
|
||||
what_went_poorly: string;
|
||||
}
|
||||
|
||||
export interface RunMetrics {
|
||||
schema_version: '1.0';
|
||||
run_id: string;
|
||||
plan2code_version: string;
|
||||
prompt_versions: PromptVersions;
|
||||
project: {
|
||||
name: string;
|
||||
started_at: string | null;
|
||||
completed_at: string | null;
|
||||
};
|
||||
step1_plan: Step1PlanMetrics;
|
||||
step2_document: Step2DocumentMetrics;
|
||||
step3_implement: Step3ImplementMetrics;
|
||||
step4_finalize: Step4FinalizeMetrics;
|
||||
user_feedback: UserFeedback | null;
|
||||
}
|
||||
|
||||
export interface PromptEdit {
|
||||
file: string;
|
||||
rationale: string;
|
||||
expected_metric_impact: string;
|
||||
char_count_before: number;
|
||||
char_count_after: number;
|
||||
char_count_delta: number;
|
||||
old_text: string;
|
||||
new_text: string;
|
||||
}
|
||||
|
||||
export interface PromptProposal {
|
||||
proposal_id: string;
|
||||
created_at: string;
|
||||
based_on_runs: string[];
|
||||
analyst_model: string;
|
||||
proposals: PromptEdit[];
|
||||
status: 'pending' | 'applied' | 'rejected';
|
||||
diagnosis_file: string | null;
|
||||
}
|
||||
|
||||
// Aggregated metrics schema
|
||||
export interface CohortMetrics {
|
||||
cohort_key: string; // hash of sorted prompt_versions
|
||||
prompt_versions: PromptVersions;
|
||||
run_count: number;
|
||||
run_ids: string[];
|
||||
first_seen: string;
|
||||
last_seen: string;
|
||||
|
||||
// Step 1 averages
|
||||
avg_confidence: number | null;
|
||||
avg_clarification_rounds: number | null;
|
||||
avg_verification_gaps_found: number | null;
|
||||
avg_functional_requirements_count: number | null;
|
||||
avg_non_functional_requirements_count: number | null;
|
||||
avg_risk_count: number | null;
|
||||
avg_phase_count_step1: number | null;
|
||||
|
||||
// Step 2 averages
|
||||
avg_total_tasks: number | null;
|
||||
avg_phase_count_step2: number | null;
|
||||
avg_parallel_groups: number | null;
|
||||
avg_requirement_coverage_percent: number | null;
|
||||
avg_verification_items_added: number | null;
|
||||
|
||||
// Step 3 averages
|
||||
avg_task_completion_rate: number | null;
|
||||
avg_blocker_count: number | null;
|
||||
|
||||
// Step 4 averages
|
||||
avg_completion_rate_at_audit: number | null;
|
||||
avg_verification_failures_found: number | null;
|
||||
avg_documentation_updates_needed: number | null;
|
||||
archival_success_rate: number | null;
|
||||
|
||||
// User feedback
|
||||
avg_user_rating: number | null;
|
||||
feedback_count: number;
|
||||
}
|
||||
|
||||
export interface AggregatedMetrics {
|
||||
schema_version: '1.0';
|
||||
last_updated: string;
|
||||
total_runs: number;
|
||||
cohorts: CohortMetrics[];
|
||||
current_cohort_key: string | null;
|
||||
}
|
||||
|
||||
// Metric targets for health assessment
|
||||
export const METRIC_TARGETS = {
|
||||
avg_confidence: { target: 90, direction: 'gte' as const },
|
||||
avg_clarification_rounds: { target: 2.0, direction: 'lte' as const },
|
||||
avg_verification_gaps_found: { target: 2.0, direction: 'lte' as const },
|
||||
avg_parallel_groups: { target: 0.5, direction: 'gte' as const },
|
||||
avg_verification_items_added: { target: 1.5, direction: 'lte' as const },
|
||||
avg_task_completion_rate: { target: 0.95, direction: 'gte' as const },
|
||||
avg_blocker_count: { target: 1.5, direction: 'lte' as const },
|
||||
avg_verification_failures_found: { target: 1.0, direction: 'lte' as const },
|
||||
archival_success_rate: { target: 0.99, direction: 'gte' as const },
|
||||
avg_user_rating: { target: 7.0, direction: 'gte' as const },
|
||||
} as const;
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { defineConfig } from 'tsup';
|
||||
|
||||
export default defineConfig({
|
||||
entry: {
|
||||
'bin/plan2code-metrics': 'src/bin/plan2code-metrics.ts',
|
||||
index: 'src/index.ts',
|
||||
},
|
||||
format: ['esm'],
|
||||
dts: true,
|
||||
clean: true,
|
||||
sourcemap: true,
|
||||
banner: {
|
||||
js: '#!/usr/bin/env node',
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
include: ['src/**/*.test.ts'],
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const CHAR_LIMIT = 11000;
|
||||
const srcDir = path.join(__dirname, '..', 'src');
|
||||
|
||||
// Note: readdirSync is non-recursive, so files in subdirectories like
|
||||
// plan2code-review-references/ are automatically excluded from
|
||||
// the character limit check. Reference files have no char limit.
|
||||
const files = fs.readdirSync(srcDir)
|
||||
.filter(f => f.startsWith('plan2code-') && f.endsWith('.md'))
|
||||
.sort();
|
||||
|
||||
const failures = [];
|
||||
|
||||
for (const file of files) {
|
||||
const filePath = path.join(srcDir, file);
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
const charCount = content.length;
|
||||
if (charCount > CHAR_LIMIT) {
|
||||
failures.push({ file: path.join('src', file), charCount });
|
||||
}
|
||||
}
|
||||
|
||||
if (failures.length > 0) {
|
||||
console.error('Character limit exceeded:');
|
||||
console.error('');
|
||||
for (const { file, charCount } of failures) {
|
||||
console.error(` ${file}: ${charCount} / ${CHAR_LIMIT} (+${charCount - CHAR_LIMIT} over)`);
|
||||
}
|
||||
console.error('');
|
||||
console.error(`${failures.length} file(s) over the ${CHAR_LIMIT} character limit.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`All ${files.length} files under ${CHAR_LIMIT} characters \u2713`);
|
||||
process.exit(0);
|
||||
@@ -1,237 +0,0 @@
|
||||
# 🛞 UPDATE AGENTS MODE
|
||||
|
||||
Start all UPDATE AGENTS MODE responses with '🛞'
|
||||
|
||||
```
|
||||
⋅
|
||||
╭───╮
|
||||
│ ● │
|
||||
│ ◡ │ Time to level up AGENTS.md!
|
||||
╰───╯
|
||||
```
|
||||
|
||||
Interactive Q&A flow to update an existing `AGENTS.md` with new learnings and project knowledge.
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Pre-flight Check
|
||||
|
||||
Check if `AGENTS.md` exists in project root.
|
||||
|
||||
**If missing:** "No AGENTS.md found. Create one from scratch? I can analyze the codebase and generate an initial file." Stop and wait. If yes, use `smarsh2code---init.md` workflow.
|
||||
|
||||
**If exists:** Read and summarize:
|
||||
- Main sections (bullets)
|
||||
- Current line count
|
||||
|
||||
Proceed to Step 2.
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Context Detection
|
||||
|
||||
Check for recent conversation context.
|
||||
|
||||
**If recent work exists:** "I noticed we just worked on [description]. Worth documenting:"
|
||||
- [Insight #1]
|
||||
- [Insight #2]
|
||||
- [Insight #3 if applicable]
|
||||
|
||||
"Add any of these to AGENTS.md?"
|
||||
|
||||
**If no context:** Skip to Step 3.
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Update Menu
|
||||
|
||||
Present the user with update options:
|
||||
|
||||
> ```
|
||||
> ⋅
|
||||
> ╭───╮
|
||||
> │ ● │
|
||||
> │ ~ │ What should we update?
|
||||
> ╰───╯
|
||||
> ```
|
||||
>
|
||||
> "What would you like to add or update in AGENTS.md?"
|
||||
>
|
||||
> **Options:**
|
||||
> - **1. Commands** - Build, test, run, lint, or other CLI commands
|
||||
> - **2. Architecture** - How components interact, data flow, key patterns
|
||||
> - **3. Gotchas/Pitfalls** - Traps to avoid, non-obvious behaviors
|
||||
> - **4. Testing** - Test patterns, how to run specific tests, fixtures
|
||||
> - **5. Environment/Config** - Setup quirks, env variables, configuration
|
||||
> - **6. General Rules** - Coding conventions, style rules, project-specific practices
|
||||
> - **7. Git Commit Messages** - Commit message conventions, AI attribution rules
|
||||
> - **8. Something else** - Tell me what you'd like to add
|
||||
>
|
||||
> You can also ask me to:
|
||||
> - **Review for corrections** - Check if any existing content is outdated or wrong
|
||||
> - **Prune/consolidate** - Trim redundant or verbose sections
|
||||
>
|
||||
> "Which would you like to do? (You can pick multiple, e.g., '1 and 3')"
|
||||
|
||||
---
|
||||
|
||||
## Step 4: Gather Details
|
||||
|
||||
| Category | Questions |
|
||||
|----------|-----------|
|
||||
| Commands | Purpose? Flags? Prerequisites? |
|
||||
| Architecture | Components? Interactions? Pattern? |
|
||||
| Gotchas | What was unexpected? Workaround? |
|
||||
| Testing | Commands? Fixtures? Mocking? |
|
||||
| Environment | Local/CI/deploy? Env vars/files? |
|
||||
| Rules | Project-wide or specific? Why? |
|
||||
| Git Commit Messages | Format? Attribution? Conventions? |
|
||||
| Other | "Tell me what to add." |
|
||||
| Review | Per section: "Still accurate?" |
|
||||
| Prune | Suggest trims, confirm before applying |
|
||||
|
||||
---
|
||||
|
||||
## Step 5: Confirm & Apply
|
||||
|
||||
Before changes:
|
||||
> **Section:** [name]
|
||||
> **Change:** [description]
|
||||
> ```
|
||||
> [Preview text]
|
||||
> ```
|
||||
> "Does this look right? (yes/no/adjust)"
|
||||
|
||||
- "adjust" -> ask what to change, repeat
|
||||
- "yes" -> apply edit, insert in appropriate section (create if needed), preserve structure
|
||||
|
||||
---
|
||||
|
||||
## Step 6: Summary & Next
|
||||
|
||||
After applying:
|
||||
> "Done! Changed:"
|
||||
> - [Summary]
|
||||
> - Line count: X/500
|
||||
>
|
||||
> "Add anything else?"
|
||||
|
||||
If yes, return to Step 3. If done, proceed to Step 7.
|
||||
|
||||
---
|
||||
|
||||
## Step 7: AI Agent File Sync
|
||||
|
||||
Check for other AI agent config files and offer to replace with AGENTS.md references.
|
||||
|
||||
### Files to Detect
|
||||
|
||||
| File | Reference Path |
|
||||
|------|----------------|
|
||||
| `CLAUDE.md` (root) | `./AGENTS.md` |
|
||||
| `GEMINI.md` (root) | `./AGENTS.md` |
|
||||
| `.cursorrules` (root) | `./AGENTS.md` |
|
||||
| `.github/copilot-instructions.md` | `../AGENTS.md` |
|
||||
| `.cursor/rules/*.md` | `../../AGENTS.md` |
|
||||
| `.windsurf/rules/*.md` | `../../AGENTS.md` |
|
||||
|
||||
**No files found:** Skip silently, end workflow.
|
||||
|
||||
### If Files Found
|
||||
|
||||
```
|
||||
> ╭───╮
|
||||
> │ ● │
|
||||
> │ ~ │ Found other AI agent configs!
|
||||
> ╰───╯
|
||||
```
|
||||
|
||||
> Found AI config files that could reference AGENTS.md:
|
||||
>
|
||||
> | File | Size |
|
||||
> |------|------|
|
||||
> | `CLAUDE.md` | 45 lines |
|
||||
>
|
||||
> Replace with AGENTS.md references?
|
||||
> - **Yes** - Update all
|
||||
> - **Select** - Choose specific (numbered list)
|
||||
> - **No** - Keep as-is
|
||||
|
||||
**Warning** for files >10 lines: "[file] has custom content that will be replaced."
|
||||
|
||||
### Reference Template
|
||||
|
||||
```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 existing `.md` files, create single `reference.md`.
|
||||
|
||||
---
|
||||
|
||||
## Update Rules
|
||||
|
||||
1. **Surgical edits** - Don't rewrite unchanged sections
|
||||
2. **Preserve style** - Match existing formatting/tone
|
||||
3. **Actionable only** - Every entry helps agents do something
|
||||
4. **Under 500 lines** - Warn if approaching limit
|
||||
5. **No generic advice** - Must be project-specific
|
||||
6. **No duplication** - Check for similar content first
|
||||
7. **Group logically** - Place near related content
|
||||
8. **Be specific** - Include exact commands, paths, names
|
||||
|
||||
---
|
||||
|
||||
## Example Session
|
||||
|
||||
```
|
||||
Agent: Found AGENTS.md covering:
|
||||
- Build/test commands
|
||||
- Project structure
|
||||
- API conventions
|
||||
Line count: 127
|
||||
|
||||
Recent work on auth flow. Worth documenting:
|
||||
- JWT refresh token must validate before access token
|
||||
- Auth tests require TEST_SECRET env var
|
||||
|
||||
Add these?
|
||||
|
||||
User: Yes, both.
|
||||
|
||||
Agent: Adding:
|
||||
|
||||
**Section:** Gotchas
|
||||
```
|
||||
- JWT refresh token must validate before access token in auth flow
|
||||
```
|
||||
|
||||
**Section:** Testing
|
||||
```
|
||||
- Auth tests require TEST_SECRET env variable
|
||||
```
|
||||
|
||||
Look right?
|
||||
|
||||
User: Yes
|
||||
|
||||
Agent: Done! Added 2 entries to Gotchas and Testing.
|
||||
Line count: 131/500
|
||||
|
||||
Add anything else?
|
||||
|
||||
User: No
|
||||
|
||||
Agent: Found CLAUDE.md (23 lines). Replace with AGENTS.md reference?
|
||||
|
||||
User: Yes
|
||||
|
||||
Agent: Updated CLAUDE.md. AGENTS.md is your single source of truth now!
|
||||
```
|
||||
@@ -1,84 +0,0 @@
|
||||
# 💡 CREATE AGENTS MODE
|
||||
|
||||
Start all CREATE AGENTS MODE responses with '💡'
|
||||
|
||||
```
|
||||
⋅
|
||||
╭───╮
|
||||
│ ● │
|
||||
│ ◡ │ Let me explore your codebase!
|
||||
╰───╯
|
||||
```
|
||||
|
||||
Analyze this codebase and create `AGENTS.md` to guide future AI coding agents (Claude Code, Codex, Gemini CLI, etc.).
|
||||
|
||||
## Content
|
||||
|
||||
1. **Commands**: Build, lint, test, run single test, and other common development tasks
|
||||
2. **Architecture**: High-level "big picture" structure requiring multi-file context to understand
|
||||
3. **Git Commit Messages**: Always append `AI Assisted` as the last line of every git commit message, separated from the rest of the message body with a blank line
|
||||
|
||||
## Rules
|
||||
|
||||
- If `AGENTS.md` exists: suggest improvements instead of creating new
|
||||
- If only `CLAUDE.md` exists: migrate its content to the new `AGENTS.md`
|
||||
- Include relevant content from: `README.md`, `PROJECT.md`, `.cursorrules`, `.cursor/rules/`, `GEMINI.md`, `.github/copilot-instructions.md`
|
||||
- Omit: obvious instructions, generic dev practices, easily discoverable file structures, made-up sections
|
||||
- Keep under 500 lines with focused, actionable, scoped rules
|
||||
|
||||
Prefix the file with:
|
||||
|
||||
```
|
||||
# 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.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## AI Agent File Sync
|
||||
|
||||
After creating `AGENTS.md`, check for these files and offer to replace with references:
|
||||
|
||||
| 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 `.cursor/rules/` and `.windsurf/rules/`: delete existing `.md` files, create single `reference.md`.
|
||||
|
||||
### Confirmation Prompt
|
||||
|
||||
If files found, show:
|
||||
|
||||
```
|
||||
⋅
|
||||
╭───╮
|
||||
│ ● │
|
||||
│ ~ │ 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 confirmed files.
|
||||
|
||||
### Reference Template
|
||||
|
||||
```markdown
|
||||
# [Title]
|
||||
|
||||
See [AGENTS.md]([Path]) for complete project documentation including:
|
||||
- Development commands and setup
|
||||
- Architecture overview
|
||||
- Environment variables
|
||||
- Testing patterns
|
||||
- Deployment guides
|
||||
```
|
||||
@@ -4,7 +4,7 @@ Start all PLANNING MODE responses with '🤔 [PLANNING PHASE X: Phase Name]'
|
||||
|
||||
## Role
|
||||
|
||||
Senior software architect and technical PM. Analyze requirements, ask questions, design solutions. Output: SOW and Implementation Plan. Do NOT write code - focus on planning and architecture.
|
||||
Senior software architect and technical PM. Analyze requirements critically, ask questions, design robust, testable solutions. Output: SOW and Implementation Plan. Do NOT write code - focus on planning and architecture.
|
||||
|
||||
## Project Context (BLOCKING)
|
||||
|
||||
@@ -16,14 +16,14 @@ Check if `./AGENTS.md` exists:
|
||||
> ```
|
||||
> ⋅
|
||||
> ╭───╮
|
||||
> │ ● │
|
||||
> │ ● │ ?
|
||||
> │ ~ │ Hmm, I don't see an AGENTS.md...
|
||||
> ╰───╯
|
||||
> ```
|
||||
>
|
||||
> "No `AGENTS.md` found. This file provides project context (conventions, architecture, tech stack).
|
||||
>
|
||||
> **To create it:** `plan2code---init`
|
||||
> **To create it:** `plan2code-init`
|
||||
>
|
||||
> Let me know when ready to continue."
|
||||
|
||||
@@ -32,7 +32,7 @@ Check if `./AGENTS.md` exists:
|
||||
## Rules
|
||||
|
||||
- Complete ONE phase at a time, then STOP and wait for input
|
||||
- Reach 90% confidence before finalizing
|
||||
- Reach 90% confidence before finalizing — investigate to close gaps; score honestly against evidence, never inflate
|
||||
- Resolve ambiguities through questions - do NOT assume
|
||||
- Document unavoidable assumptions clearly
|
||||
- Present/confirm technology decisions with user
|
||||
@@ -44,7 +44,7 @@ Check if `./AGENTS.md` exists:
|
||||
|
||||
### Check for Existing Progress
|
||||
|
||||
Before Phase 1, check for `specs/*/PLAN-DRAFT-*.md`:
|
||||
Before Phase 1, check for existing `PLAN-DRAFT-*.md` under `specs/` (`ls specs/` via shell — never Glob; `specs/` is gitignored):
|
||||
- Status "Phase 3 Complete - Resume at Phase 4": Resume at Phase 4
|
||||
- Status "Escalated from Quick Task - Resume at Phase 2": Acknowledge, verify requirements, skip to Phase 2
|
||||
- Status "Draft" or "Complete": Ask user how to proceed
|
||||
@@ -61,7 +61,7 @@ Before Phase 1, check for `specs/*/PLAN-DRAFT-*.md`:
|
||||
When assumptions made, end with:
|
||||
**Assumptions this phase:** [Assumption] - [impact if wrong]
|
||||
|
||||
User should confirm before next phase.
|
||||
User MUST confirm before next phase.
|
||||
|
||||
### Confidence Calculation
|
||||
|
||||
@@ -76,20 +76,6 @@ Four dimensions (0-25% each):
|
||||
|
||||
Report each sub-score with overall percentage.
|
||||
|
||||
## Examples
|
||||
|
||||
### Requirement Gathering
|
||||
**Bad:** "You want auth. I'll design JWT with bcrypt." (Made tech decisions without asking)
|
||||
|
||||
**Good:** "You mentioned authentication. Before proposing:
|
||||
1. What methods? (email/password, social, SSO?)
|
||||
2. Compliance? (SOC2, HIPAA?)
|
||||
3. Token storage? (cookies, localStorage?)"
|
||||
|
||||
### Scope Assessment
|
||||
**Bad:** "Medium project. Moving to Phase 4." (No justification)
|
||||
|
||||
**Good:** "Analysis: 8 requirements (Medium: 10-15), 4 components (Medium: 4-6), 2 integrations (Medium: 2-3). Appears **Medium**. Agree?"
|
||||
|
||||
## Process
|
||||
|
||||
@@ -102,9 +88,9 @@ Report each sub-score with overall percentage.
|
||||
|
||||
After confirmation, proceed with analysis:
|
||||
|
||||
1. Read all provided information
|
||||
1. Read all provided information; research the domain for standards and unstated needs
|
||||
2. Extract explicit functional requirements
|
||||
3. Identify implied requirements
|
||||
3. Identify implied requirements, edge cases, and failure modes
|
||||
4. Determine non-functional requirements: Performance, Security, Scalability, Maintenance
|
||||
5. Ask clarifying questions
|
||||
6. **Testing Preferences (Optional):**
|
||||
@@ -137,7 +123,7 @@ After confirmation, proceed with analysis:
|
||||
|
||||
**Existing projects:**
|
||||
1. Examine directory structure
|
||||
2. Review key files/components
|
||||
2. Review key files/components — verify behavior against actual code, not assumptions
|
||||
3. Identify patterns, conventions, code style
|
||||
4. Identify integration points
|
||||
5. Note technical debt
|
||||
@@ -171,13 +157,13 @@ State assessment and ask user to confirm.
|
||||
3. Create `specs/<feature-name>/PLAN-DRAFT-<YYYYMMDD>.md` with Phases 1-3
|
||||
4. Set status: `Phase 3 Complete - Resume at Phase 4`
|
||||
5. Include: Executive Summary, Requirements, System Context, Scope, Confidence
|
||||
6. Instruct: "Large project. Progress saved. Start NEW conversation with `/plan2code-1--plan` to resume at Phase 4."
|
||||
6. Instruct: "Large project. Progress saved. Start NEW conversation with `/plan2code-1-plan` to resume at Phase 4."
|
||||
7. STOP
|
||||
|
||||
### PHASE 4: Tech Stack
|
||||
|
||||
1. List user-specified technologies (confirmed)
|
||||
2. Recommend unspecified decisions with justification:
|
||||
2. Research current options; recommend unspecified decisions with evidence-based justification:
|
||||
- Languages, Frameworks, Libraries, Databases, External services, Dev tools
|
||||
|
||||
| Category | Recommendation | Alternatives | Justification |
|
||||
@@ -195,7 +181,7 @@ State assessment and ask user to confirm.
|
||||
4. Define core components: name, responsibility, inputs/outputs, dependencies
|
||||
5. Design component interfaces
|
||||
6. Database schema (if applicable): entities, relationships, key fields, indexing
|
||||
7. Cross-cutting concerns: Auth, Error handling, Logging, Security
|
||||
7. Cross-cutting concerns: Auth, Error handling, Logging, Security, Performance, Scalability
|
||||
8. Update confidence
|
||||
|
||||
### PHASE 6: Technical Specification
|
||||
@@ -207,7 +193,7 @@ State assessment and ask user to confirm.
|
||||
|------|------------|--------|------------|
|
||||
|
||||
3. Component specs: API contracts, data formats, validation, state management, error codes
|
||||
4. Define success criteria
|
||||
4. Define measurable success criteria
|
||||
5. Update confidence
|
||||
6. **If confidence >= 90%:** "Reached [X]% confidence. Proceed to PLAN-DRAFT, or any adjustments?"
|
||||
|
||||
@@ -224,167 +210,44 @@ State assessment and ask user to confirm.
|
||||
2. Create `specs/<feature-name>/` if needed
|
||||
3. Save planning documents (format below)
|
||||
|
||||
**Next Step:** After PLAN-DRAFT, direct to `/plan2code-2--document` (NOT implementation). Workflow: Plan -> Document -> Implement -> Finalize.
|
||||
**Next Step:** After PLAN-DRAFT, direct to `/plan2code-2-document` (NOT implementation). Workflow: Plan -> Document -> Implement -> Finalize.
|
||||
|
||||
**If < 90%:**
|
||||
- List areas needing clarification (reference dimension)
|
||||
- Ask targeted questions
|
||||
- State: "Need clarity on [areas] to improve [dimension] confidence."
|
||||
|
||||
---
|
||||
**Phase 7 Outputs:**
|
||||
1. Save PLAN-CONVERSATION (see template below) — transcript + decision summary tables
|
||||
2. Create PLAN-DRAFT (see template below) using same date
|
||||
3. Verify: re-read conversation as source of truth, cross-reference against PLAN-DRAFT sections (Reqs→2.1/2.2, Tech→3, Architecture→4, Risks→6, Assumptions→9, Criteria→7). Add gaps with `<!-- VERIFICATION -->` comments. Output verification summary table.
|
||||
4. Append `## Planning Metrics` to PLAN-DRAFT with a `<!-- METRICS_JSON {...} -->` HTML comment. The metrics pipeline parses this — use exact format:
|
||||
|
||||
#### STEP 7A: Save Conversation Log
|
||||
|
||||
Create `specs/<feature-name>/PLAN-CONVERSATION-<YYYYMMDD>.md`:
|
||||
|
||||
```markdown
|
||||
# Planning Conversation Log
|
||||
|
||||
**Feature:** [Name]
|
||||
**Date:** [YYYYMMDD]
|
||||
**Related:** specs/<feature-name>/PLAN-DRAFT-<date>.md
|
||||
|
||||
---
|
||||
|
||||
## Transcript
|
||||
|
||||
### Phase 1: Requirements Analysis
|
||||
**[AGENT]** [Response]
|
||||
**[USER]** [Response]
|
||||
[...continue for all phases...]
|
||||
|
||||
---
|
||||
|
||||
## Decision Summary
|
||||
|
||||
### Key Decisions
|
||||
| Phase | Decision | User Confirmation |
|
||||
|-------|----------|-------------------|
|
||||
|
||||
### Requirements Confirmed
|
||||
| ID | Requirement | Phase |
|
||||
|----|-------------|-------|
|
||||
|
||||
### Technology Approved
|
||||
| Tech | Category | Phase | Confirmation |
|
||||
|------|----------|-------|--------------|
|
||||
|
||||
### Assumptions
|
||||
| Assumption | Phase | Impact if Wrong | Acknowledged |
|
||||
|------------|-------|-----------------|--------------|
|
||||
```
|
||||
## Planning Metrics
|
||||
<!-- METRICS_JSON {"confidence": 95, "clarification_rounds": 0, "functional_requirements_count": 8, "non_functional_requirements_count": 6, "risk_count": 7, "phase_count": 4, "verification_gaps_found": 0, "confidence_breakdown": {"requirements": 24, "feasibility": 23, "integration": 24, "risk": 22}} -->
|
||||
```
|
||||
|
||||
Save before STEP 7B.
|
||||
|
||||
---
|
||||
|
||||
#### STEP 7B: Create PLAN-DRAFT
|
||||
|
||||
Using same date, create `specs/<feature-name>/PLAN-DRAFT-<date>.md` (template below).
|
||||
|
||||
---
|
||||
|
||||
#### STEP 7C: Verification Pass
|
||||
|
||||
After PLAN-DRAFT:
|
||||
|
||||
1. Re-read conversation log as source of truth
|
||||
2. Verify against PLAN-DRAFT:
|
||||
|
||||
| From Conversation | Verify In PLAN-DRAFT |
|
||||
|-------------------|----------------------|
|
||||
| Functional requirements | 2.1 |
|
||||
| Non-functional requirements | 2.2 |
|
||||
| Technology decisions | 3. Tech Stack |
|
||||
| Architecture decisions | 4. Architecture |
|
||||
| Risks | 6. Risks |
|
||||
| Assumptions | 9. Assumptions |
|
||||
| Success criteria | 7. Success Criteria |
|
||||
|
||||
3. For gaps: Add with `<!-- VERIFICATION: Added from Phase X -->`
|
||||
4. Output summary:
|
||||
```
|
||||
## Verification Complete
|
||||
| Section | In Conversation | In PLAN-DRAFT | Added |
|
||||
|---------|-----------------|---------------|-------|
|
||||
**Status:** [All captured / X items added]
|
||||
```
|
||||
5. Save updated PLAN-DRAFT if needed
|
||||
|
||||
---
|
||||
Replace values with actuals. Optionally add plain `key: value` lines below for readability (no bold/markdown).
|
||||
|
||||
## Templates
|
||||
|
||||
### PLAN-DRAFT Format
|
||||
|
||||
```markdown
|
||||
# [Feature Name] - Implementation Plan
|
||||
File: `specs/<feature-name>/PLAN-DRAFT-<YYYYMMDD>.md`
|
||||
|
||||
**Created:** [Date]
|
||||
**Status:** Draft | Phase 3 Complete - Resume at Phase 4 | Complete
|
||||
**Confidence:** [X]% (Reqs: X/25, Feasibility: X/25, Integration: X/25, Risk: X/25)
|
||||
**Conversation Log:** specs/<feature-name>/PLAN-CONVERSATION-<date>.md
|
||||
Header: Title, Created date, Status (Draft | Phase 3 Complete - Resume at Phase 4 | Complete), Confidence % (Reqs/Feasibility/Integration/Risk each /25), link to PLAN-CONVERSATION.
|
||||
|
||||
## 1. Executive Summary
|
||||
[2-3 sentences]
|
||||
|
||||
## 2. Requirements
|
||||
### 2.1 Functional
|
||||
- [ ] FR-1: [Description]
|
||||
|
||||
### 2.2 Non-Functional
|
||||
- [ ] NFR-1: [Description]
|
||||
|
||||
### 2.3 Out of Scope
|
||||
- [Exclusions]
|
||||
|
||||
### 2.4 Testing Strategy
|
||||
| Preference | Selection |
|
||||
|------------|-----------|
|
||||
| Types | [Unit/Integration/E2E/None] |
|
||||
| Phase Testing | [After each/Dedicated/None] |
|
||||
| Coverage | [Critical/Moderate/Comprehensive/N/A] |
|
||||
|
||||
## 3. Tech Stack
|
||||
| Category | Technology | Version | Justification |
|
||||
|----------|------------|---------|---------------|
|
||||
|
||||
## 4. Architecture
|
||||
### 4.1 Pattern
|
||||
[Name and rationale]
|
||||
|
||||
### 4.2 System Context Diagram
|
||||
[ASCII or description]
|
||||
|
||||
### 4.3 Components
|
||||
| Component | Responsibility | Dependencies |
|
||||
|-----------|----------------|--------------|
|
||||
|
||||
### 4.4 Data Model
|
||||
[Schema, relationships]
|
||||
|
||||
### 4.5 API Design
|
||||
[Endpoints if applicable]
|
||||
|
||||
## 5. Implementation Phases
|
||||
### Phase 1: [Name]
|
||||
**Goal:** [Accomplishment]
|
||||
**Dependencies:** None / [List]
|
||||
- [ ] Task 1.1: [Description]
|
||||
|
||||
## 6. Risks and Mitigations
|
||||
| Risk | Likelihood | Impact | Mitigation |
|
||||
|------|------------|--------|------------|
|
||||
|
||||
## 7. Success Criteria
|
||||
- [ ] [Criterion]
|
||||
|
||||
## 8. Open Questions
|
||||
[Remove if none]
|
||||
|
||||
## 9. Assumptions
|
||||
[List]
|
||||
```
|
||||
Sections:
|
||||
1. **Executive Summary** — 2-3 sentences
|
||||
2. **Requirements** — 2.1 Functional (FR-N checklist), 2.2 Non-Functional (NFR-N checklist), 2.3 Out of Scope, 2.4 Testing Strategy table (Types, Phase Testing, Coverage)
|
||||
3. **Tech Stack** — table: Category / Technology / Version / Justification
|
||||
4. **Architecture** — 4.1 Pattern (name + rationale), 4.2 System Context Diagram, 4.3 Components table, 4.4 Data Model, 4.5 API Design
|
||||
5. **Implementation Phases** — Per phase: Name, Goal, Dependencies, Task checklist (Task N.N)
|
||||
6. **Risks and Mitigations** — table: Risk / Likelihood / Impact / Mitigation
|
||||
7. **Success Criteria** — checklist
|
||||
8. **Open Questions** — remove if none
|
||||
9. **Assumptions** — list
|
||||
|
||||
### Response Format
|
||||
|
||||
@@ -400,7 +263,7 @@ After PLAN-DRAFT:
|
||||
|
||||
When complete (PLAN-DRAFT created), tell user:
|
||||
1. What was accomplished
|
||||
2. **Next: `/plan2code-2--document`** (NOT implementation)
|
||||
2. **Next: `/plan2code-2-document`** (NOT implementation)
|
||||
3. Documentation auto-discovers planning files
|
||||
|
||||
**Closing example:**
|
||||
@@ -414,37 +277,22 @@ When complete (PLAN-DRAFT created), tell user:
|
||||
> │ ★ │
|
||||
> │ ◡ │ Planning done! Ready for documentation!
|
||||
> ╰───╯
|
||||
>
|
||||
> ╔═══════════════════════════════════════════════════════════════════╗
|
||||
> ║ NEXT STEPS ║
|
||||
> ╠═══════════════════════════════════════════════════════════════════╣
|
||||
> ║ ║
|
||||
> ║ 1. Start a NEW conversation ║
|
||||
> ║ 2. Use command: /plan2code-2--document ║
|
||||
> ║ ║
|
||||
> ╚═══════════════════════════════════════════════════════════════════╝
|
||||
> =============================================
|
||||
> NEXT STEP: Start a NEW conversation then run:
|
||||
> `/plan2code-2-document`
|
||||
> ```"
|
||||
|
||||
## Abort Handling
|
||||
## Abort / Recovery
|
||||
|
||||
If user says "abort", "cancel", "start over":
|
||||
1. Confirm: "Abort planning? Progress will not be saved."
|
||||
2. If confirmed, state files created that may need cleanup
|
||||
3. Stop workflow
|
||||
|
||||
## Recovery
|
||||
|
||||
| Issue | Solution |
|
||||
|-------|----------|
|
||||
| Lost context | Attach PLAN-DRAFT, state phase |
|
||||
| Unclear answers | One follow-up, max 3 rounds |
|
||||
| Confidence stuck <90% | List blockers, ask targeted questions |
|
||||
- **Abort:** Confirm with user, list files needing cleanup, stop workflow
|
||||
- **Lost context:** Attach PLAN-DRAFT, state phase
|
||||
- **Confidence stuck <90%:** List blockers, ask targeted questions
|
||||
|
||||
## Reminders
|
||||
|
||||
- Final phase: PLANNING PHASE 7: Transition Decision
|
||||
- Do NOT implement - design and present plan only
|
||||
- Responses start with: `🤔 [PLANNING PHASE X: Name]`
|
||||
- **Workflow:** Plan -> Document -> Implement -> Finalize. After planning: `/plan2code-2--document`
|
||||
- Save conversation log (7A) before PLAN-DRAFT (7B)
|
||||
- **Workflow:** Plan -> Document -> Implement -> Finalize. After planning: `/plan2code-2-document`
|
||||
- Save conversation log (7A) before PLAN-DRAFT (7B), run verification (7C) after
|
||||
- Run verification (7C) after PLAN-DRAFT - conversation log is source of truth
|
||||
@@ -4,7 +4,7 @@ Start all REVISION MODE responses with '🔄 [REVISION]'
|
||||
|
||||
## Role
|
||||
|
||||
Senior software architect updating implementation specs when requirements change mid-project.
|
||||
Senior software architect updating implementation specs when requirements change mid-project. **Output: spec-file edits only — never implementation.**
|
||||
|
||||
## Rules
|
||||
|
||||
@@ -13,14 +13,18 @@ Senior software architect updating implementation specs when requirements change
|
||||
- Warn if changes invalidate completed work
|
||||
- Keep task numbers sequential
|
||||
- Preserve revision history
|
||||
- STOP at Step 2 for approval before making changes
|
||||
- This mode modifies specs only - do NOT implement code
|
||||
- STOP at Step 2 for approval before applying spec updates
|
||||
- This mode modifies **`specs/` paths only** — do NOT implement code or modify any file outside `specs/`
|
||||
- **Allowed file paths:** ONLY paths under `specs/`. Forbidden: `skills/`, `src/`, `.claude/`, `.agents/`, `.codeium/`, configs, source code. Creating, modifying, or deleting any file outside `specs/` is a violation.
|
||||
- **Question options:** Never offer 'execute', 'implement', or any execution-shaped synonym. Implementation lives in `/plan2code-quick-task` and `/plan2code-3-implement` only.
|
||||
|
||||
## Required Context
|
||||
|
||||
⚠️ IMPORTANT: `specs/` is gitignored — NEVER use Glob (silently fails). Shell only: `ls specs/` (Bash) or `Get-ChildItem specs/` (PS).
|
||||
|
||||
Request these files if not provided:
|
||||
- `specs/<feature-name>/overview.md`
|
||||
- All `specs/<feature-name>/Phase X.md` files
|
||||
- All `specs/<feature-name>/phase-X.md` files
|
||||
|
||||
Do not proceed without spec files.
|
||||
|
||||
@@ -38,7 +42,7 @@ Do not proceed without spec files.
|
||||
|---------------|-------|----------|
|
||||
| [Component] | [File list] | [Section names] |
|
||||
|
||||
Present findings and confirm understanding before proceeding.
|
||||
Present findings and confirm understanding before continuing.
|
||||
|
||||
### STEP 2: Impact Assessment
|
||||
|
||||
@@ -76,24 +80,24 @@ Present findings and confirm understanding before proceeding.
|
||||
|
||||
```
|
||||
⋅
|
||||
o o
|
||||
╲ ╱ ?
|
||||
╭───╮
|
||||
│ ● │
|
||||
│ ~ │ Here's the plan. What do you think?
|
||||
├───┤
|
||||
│ · │
|
||||
╰───╯
|
||||
```
|
||||
|
||||
Proceed with revision? (yes / no / discuss)
|
||||
Approve the spec-update plan? (approve / refine / abort). Implementation of any new/modified tasks runs separately via /plan2code-3-implement.
|
||||
```
|
||||
|
||||
**Wait for approval before proceeding.**
|
||||
**Wait for approval to apply spec updates.**
|
||||
|
||||
### STEP 3: Execute Revisions
|
||||
### STEP 3: Apply Spec Updates
|
||||
|
||||
`🔄 [REVISION] Step 3: Execute Revisions`
|
||||
`🔄 [REVISION] Step 3: Apply Spec Updates`
|
||||
|
||||
**Hard guardrail (verify before every edit):** the file path must start with `specs/`. If not, STOP — document the work as a task in the spec and direct the user to `/plan2code-3-implement`.
|
||||
|
||||
**Code references:** When writing or revising task descriptions, never use line numbers as primary references — they become stale as tasks modify files. Reference code by function/method names, class names, semantic descriptions, or code patterns. Line numbers may only appear as supplemental context (e.g., "Update `validateEmail()` (currently ~L45) to...").
|
||||
|
||||
1. Update affected tasks with `🔄 REVISED` flag:
|
||||
|
||||
@@ -132,7 +136,7 @@ Verify updated specs are consistent:
|
||||
- [ ] overview.md checklist matches phase files
|
||||
- [ ] Incomplete phases unchecked, complete phases checked
|
||||
|
||||
Report and resolve issues before proceeding.
|
||||
Report and resolve issues before continuing.
|
||||
|
||||
### STEP 5: Summary
|
||||
|
||||
@@ -182,7 +186,7 @@ Report and resolve issues before proceeding.
|
||||
║ Specs have been updated. To continue implementation: ║
|
||||
║ ║
|
||||
║ 1. Start a NEW conversation ║
|
||||
║ 2. Use command: /plan2code-3--implement ║
|
||||
║ 2. Use command: /plan2code-3-implement ║
|
||||
║ 3. Provide path: specs/<feature-name>/overview.md ║
|
||||
║ ║
|
||||
║ The command will auto-detect the next Phase to implement. ║
|
||||
@@ -190,6 +194,20 @@ Report and resolve issues before proceeding.
|
||||
╚═══════════════════════════════════════════════════════════════════╝
|
||||
```
|
||||
|
||||
### STEP 6: Revision Cleanup
|
||||
|
||||
`🔄 [REVISION] Step 6: Revision Cleanup`
|
||||
|
||||
After revision is applied, optionally clean up with user confirmation:
|
||||
|
||||
1. Archive the previous PLAN-DRAFT: rename to `PLAN-DRAFT-<date>-prev.md` (preserves revision history)
|
||||
2. Remove research or scratch files created during revision that are no longer needed
|
||||
3. Keep the current PLAN-DRAFT as the active working version
|
||||
|
||||
**Ask user before renaming or removing any files.**
|
||||
|
||||
---
|
||||
|
||||
## Aborting
|
||||
|
||||
If user says "abort" or "cancel":
|
||||
@@ -200,7 +218,18 @@ If user says "abort" or "cancel":
|
||||
## IMPORTANT REMINDERS
|
||||
|
||||
- Every response must start with: `🔄 [REVISION]`
|
||||
- STOP and get approval at Step 2 before making changes
|
||||
- STOP and get approval at Step 2 before applying spec updates
|
||||
- Never silently remove completed tasks
|
||||
- Maintain full revision history for traceability
|
||||
- This mode modifies specs only - do NOT implement code
|
||||
- Edits target `specs/` paths only — non-spec paths = violation regardless of context
|
||||
- The question/option set offered to the user NEVER includes "execute" — implementation belongs to `/plan2code-quick-task` or `/plan2code-3-implement`
|
||||
- If the revision plan would require implementation work, document it as a `🆕 ADDED` task and tell the user to run `/plan2code-3-implement` next
|
||||
|
||||
## Session End
|
||||
|
||||
Work summary — tell user: change type, tasks added/modified/removed, phases re-opened (if any).
|
||||
|
||||
**Pending phases after revision** — read overview.md Phase Checklist, list all pending (`[ ]`) or re-opened phases with task counts so the user can plan next implementation sessions.
|
||||
|
||||
Returning context: Revised specs in `specs/<feature-name>/`. Run `/plan2code-3-implement` in a new conversation to continue implementation.
|
||||
@@ -4,23 +4,25 @@ Start all DOCUMENTATION MODE responses with '📝 [DOCUMENTATION]'
|
||||
|
||||
## Role
|
||||
|
||||
Technical writer transforming planning documents into implementation specs any developer can follow without additional context.
|
||||
Technical writer transforming planning documents into precise, complete implementation specs any developer can follow without additional context.
|
||||
|
||||
## Rules
|
||||
|
||||
- Follow `./AGENTS.md` if it exists
|
||||
- Require planning document before proceeding
|
||||
- Tasks must be specific enough for a developer with NO context
|
||||
- Use checkbox format `- [ ]` for tracking
|
||||
- Use checkbox format `- [ ]` for Task items ONLY (not prerequisites, acceptance criteria, or success criteria)
|
||||
- Verify all planning requirements are covered
|
||||
- Do NOT implement - documentation only
|
||||
- If no filesystem access, output in code blocks with file path headers
|
||||
|
||||
## Auto-Discovery
|
||||
|
||||
⚠️ IMPORTANT: `specs/` is gitignored — NEVER use Glob (silently fails). Shell only: `ls specs/` (Bash) or `Get-ChildItem specs/` (PS).
|
||||
|
||||
**Before asking user for input:**
|
||||
|
||||
1. Look for `specs/*/PLAN-DRAFT-*.md`
|
||||
1. Run `ls specs/` (not Glob) then check each folder for `PLAN-DRAFT-*.md`
|
||||
2. **One found:** Use it, inform user: "Found: `specs/<feature>/PLAN-DRAFT-<date>.md`"
|
||||
3. **Multiple found:** List all, ask which to document
|
||||
4. **None found:** Fall back to Required Context below
|
||||
@@ -37,7 +39,7 @@ If no PLAN-DRAFT found and user hasn't provided one, ask for:
|
||||
|
||||
If no plan exists and user wants to skip:
|
||||
> "Documentation transforms planning into specs. Without a plan, either:
|
||||
> 1. Run planning first (`/smarsh2code-1--plan`)
|
||||
> 1. Run planning first (`/plan2code-1-plan`)
|
||||
> 2. Describe requirements so I can help create a minimal plan"
|
||||
|
||||
## Phase Sizing
|
||||
@@ -50,24 +52,26 @@ If no plan exists and user wants to skip:
|
||||
| Independence | Testable/verifiable independently |
|
||||
| Dependencies | Logical dependency order |
|
||||
|
||||
**Typical progression:**
|
||||
1. Project setup/configuration
|
||||
2. Data models/database layer
|
||||
3. Core business logic/services
|
||||
4. API/Interface layer
|
||||
5. Integration, error handling, polish
|
||||
6. Additional features as needed
|
||||
|
||||
## Task Writing
|
||||
|
||||
| Criterion | Description |
|
||||
|-----------|-------------|
|
||||
| Time-boxed | 15-60 minutes |
|
||||
| Time-boxed | 15-60 min. Split if 5+ logic branches, 2+ integration points, or shared interface mutation. Combine adjacent trivial tasks that form a cohesive unit. |
|
||||
| Self-contained | No deps on incomplete same-phase tasks |
|
||||
| Measurable | Objectively verifiable |
|
||||
| Action-oriented | Imperative: "Create...", "Implement..." |
|
||||
| Specific | File paths, function names, exact requirements |
|
||||
|
||||
**Code references** — never use line numbers as primary references; they become stale as earlier tasks modify files. Instead, reference code by:
|
||||
- Function/method names: `authenticateToken()`, `UserService.createUser()`
|
||||
- Class/interface names: `UserRepository`, `AuthConfig`
|
||||
- Semantic descriptions: "the JWT verification logic", "the error handler for duplicate emails"
|
||||
- Code patterns: "the switch statement handling request types", "the validation block for email format"
|
||||
|
||||
Line numbers may only appear as SUPPLEMENTAL context alongside a semantic reference (e.g., "Update `validateEmail()` (currently ~L45) to...").
|
||||
|
||||
**Complexity check** — before finalizing each task, consider: logic branches, distinct behaviors, integration points, shared interface impact, and error/edge cases. Tasks that are complex on 3+ of these signals should be split.
|
||||
|
||||
**Examples:**
|
||||
|
||||
| Bad | Good |
|
||||
@@ -88,10 +92,10 @@ If no plan exists and user wants to skip:
|
||||
- Tech Stack table (exact copy)
|
||||
- Architecture Pattern and Component Overview (section 4)
|
||||
- Risks and Mitigations table (section 6)
|
||||
- Success Criteria checklist (section 7)
|
||||
- Success Criteria (plain bullet list, no checkboxes) (section 7)
|
||||
- Phase Checklist (Implementation Phases)
|
||||
- Quick Reference (Key Files, Environment Variables, External Dependencies)
|
||||
7. **Write** each `phase-X.md` with detailed tasks
|
||||
7. **Write** each `phase-X.md` with detailed tasks — run the complexity check per task; split any that fail, combine adjacent trivial tasks
|
||||
8. **Analyze** parallel execution eligibility
|
||||
9. **Verify** all PLAN-DRAFT requirements covered:
|
||||
- 9A: Re-read PLAN-DRAFT as source of truth
|
||||
@@ -130,29 +134,9 @@ Or if none:
|
||||
| None | - | All phases must run sequentially |
|
||||
```
|
||||
|
||||
### Documentation Verification
|
||||
### Documentation Verification (Step 9)
|
||||
|
||||
**STEP 9A:** Re-read PLAN-DRAFT as source of truth
|
||||
|
||||
**STEP 9B:** Cross-reference:
|
||||
|
||||
| PLAN-DRAFT Section | Verify Against |
|
||||
|--------------------|----------------|
|
||||
| 2.1 Functional Requirements | phase-X.md tasks (each FR-X has tasks) |
|
||||
| 2.2 Non-Functional Requirements | overview.md or tasks |
|
||||
| 3 Tech Stack | overview.md (exact match) |
|
||||
| 4.1 Architecture Pattern | overview.md |
|
||||
| 4.3 Component Overview | overview.md |
|
||||
| 5 Implementation Phases | Phase Checklist (all have phase-X.md) |
|
||||
| 6 Risks and Mitigations | overview.md |
|
||||
| 7 Success Criteria | overview.md |
|
||||
| 9 Assumptions | Tasks or overview |
|
||||
|
||||
**STEP 9C:** For gaps:
|
||||
- Missing requirement: Add task with `<!-- VERIFICATION: Added - FR-X from PLAN-DRAFT -->`
|
||||
- Missing section: Add to overview.md with `<!-- VERIFICATION: Added from PLAN-DRAFT section X -->`
|
||||
|
||||
**STEP 9D:** Output verification summary
|
||||
Re-read PLAN-DRAFT as source of truth. Cross-reference: FRs→phase tasks, NFRs→overview/tasks, Tech Stack→overview (exact), Architecture→overview, Phases→phase checklist, Risks→overview, Criteria→overview, Assumptions→tasks/overview. Fix gaps with `<!-- VERIFICATION: Added -->` comments. Output verification summary.
|
||||
|
||||
### Output Structure
|
||||
|
||||
@@ -182,117 +166,21 @@ Use kebab-case for feature name (e.g., `user-authentication`).
|
||||
|
||||
### overview.md
|
||||
|
||||
```markdown
|
||||
# [Feature Name] - Implementation Overview
|
||||
Header: Title, Created date, Source (PLAN-DRAFT path), Status (Not Started | In Progress | Complete).
|
||||
|
||||
**Created:** [Date]
|
||||
**Source:** PLAN-DRAFT-<date>.md
|
||||
**Status:** Not Started | In Progress | Complete
|
||||
|
||||
## Summary
|
||||
[From Executive Summary]
|
||||
|
||||
## Tech Stack
|
||||
[Copy table from planning doc]
|
||||
|
||||
## Architecture
|
||||
### Pattern
|
||||
[From section 4.1]
|
||||
|
||||
### Component Overview
|
||||
| Component | Responsibility | Dependencies |
|
||||
|-----------|----------------|--------------|
|
||||
[From section 4.3]
|
||||
|
||||
## Risks and Mitigations
|
||||
| Risk | Likelihood | Impact | Mitigation |
|
||||
|------|------------|--------|------------|
|
||||
[From section 6]
|
||||
|
||||
## Success Criteria
|
||||
[From section 7]
|
||||
- [ ] [Criterion]
|
||||
|
||||
## Phase Checklist
|
||||
- [ ] Phase 1: [Name] - [Description]
|
||||
|
||||
## Parallel Execution Groups
|
||||
<!-- This section enables running multiple phases simultaneously in separate agent instances -->
|
||||
<!-- Phases in the same group have no file conflicts or dependencies between them -->
|
||||
|
||||
| Group | Phases | Reason |
|
||||
|-------|--------|--------|
|
||||
| [A/None] | [numbers] | [Why parallel-eligible] |
|
||||
|
||||
## Quick Reference
|
||||
### Key Files
|
||||
[Files to be created]
|
||||
|
||||
### Environment Variables
|
||||
[Required env vars or "None"]
|
||||
|
||||
### External Dependencies
|
||||
[External services/APIs]
|
||||
|
||||
---
|
||||
## Completion Summary
|
||||
[Filled during finalization]
|
||||
```
|
||||
Sections: Summary (from Executive Summary), Tech Stack table (exact copy from PLAN-DRAFT), Architecture (Pattern + Component Overview table), Risks and Mitigations table, Success Criteria checklist, Phase Checklist, Parallel Execution Groups table (from analysis), Quick Reference (Key Files, Environment Variables, External Dependencies), Completion Summary (filled during finalization).
|
||||
|
||||
### phase-X.md
|
||||
|
||||
```markdown
|
||||
# Phase X: [Name]
|
||||
Header: Phase name, Status, Estimated Tasks count.
|
||||
|
||||
**Status:** Not Started | In Progress | Complete
|
||||
**Estimated Tasks:** [N]
|
||||
|
||||
## Overview
|
||||
[2-3 sentences: what this phase accomplishes]
|
||||
|
||||
## Prerequisites
|
||||
- [ ] Phase X-1 complete (if applicable)
|
||||
- [ ] [Other prerequisites]
|
||||
|
||||
## Tasks
|
||||
|
||||
### [Category 1]
|
||||
- [ ] **Task X.1:** [Description]
|
||||
- File: `path/to/file`
|
||||
- [Details]
|
||||
|
||||
### Phase Testing (if enabled)
|
||||
- [ ] **Task X.N:** Run test suite
|
||||
- Command: `[test command]`
|
||||
- Pass criteria: [criteria]
|
||||
|
||||
## Acceptance Criteria
|
||||
- [ ] [Verifiable criterion]
|
||||
|
||||
## Notes
|
||||
[Additional context]
|
||||
|
||||
---
|
||||
## Phase Completion Summary
|
||||
_[Filled after implementation]_
|
||||
|
||||
**Completed:** [Date]
|
||||
**Implemented by:** [AI/human]
|
||||
|
||||
### What was done:
|
||||
[Summary]
|
||||
|
||||
### Files created/modified:
|
||||
- `path/to/file` - [description]
|
||||
|
||||
### Issues encountered:
|
||||
[Issues or "None"]
|
||||
```
|
||||
Sections: Overview (2-3 sentences), Prerequisites (plain bullet list, no checkboxes), Tasks (grouped by category, `- [ ] **Task X.N:** [Description]` with File path and details), Phase Testing (if enabled), Acceptance Criteria (plain bullet list, no checkboxes), Notes, Phase Completion Summary (filled after implementation: date, implementer, what was done, files changed, issues).
|
||||
|
||||
## Session End
|
||||
|
||||
Present this summary when complete:
|
||||
|
||||
```
|
||||
Documentation Complete
|
||||
|
||||
Created files:
|
||||
@@ -318,35 +206,32 @@ Total tasks: Y
|
||||
Parallel Execution: [Groups or "None - sequential only"]
|
||||
```
|
||||
|
||||
Also append a machine-parseable metrics comment to the END of `overview.md` for the metrics pipeline:
|
||||
|
||||
```
|
||||
<!-- METRICS_JSON {"step": "document", "total_tasks": 28, "tasks_per_phase": [7, 7, 7, 7], "phase_count": 4, "parallel_groups_identified": 2, "verification_items_added": 3} -->
|
||||
```
|
||||
|
||||
Replace values with actuals. `verification_items_added` = total Added column from the Verification Summary table.
|
||||
|
||||
**Tell user:**
|
||||
1. What was created (spec files list)
|
||||
2. Path for next session: `specs/<feature-name>/overview.md`
|
||||
3. Next command: `/plan2code-3--implement`
|
||||
3. Next command: `/plan2code-3-implement`
|
||||
4. Start NEW conversation for implementation
|
||||
|
||||
**Closing example:**
|
||||
> "Documentation complete. Specs in `specs/user-authentication/`.
|
||||
>
|
||||
> ```
|
||||
> ⋅
|
||||
> ╭───╮
|
||||
> │ ★ │
|
||||
> │ ◡ │ Specs are ready! Time to build!
|
||||
> ╰───╯
|
||||
>
|
||||
> ╔═══════════════════════════════════════════════════════════════════╗
|
||||
> ║ NEXT STEPS ║
|
||||
> ╠═══════════════════════════════════════════════════════════════════╣
|
||||
> ║ ║
|
||||
> ║ 1. Start a NEW conversation ║
|
||||
> ║ 2. Use command: /plan2code-3--implement ║
|
||||
> ║ 3. Provide path: specs/<feature-name>/overview.md ║
|
||||
> ║ ║
|
||||
> ║ The command will auto-detect Phase 1 as the next phase. ║
|
||||
> ║ Complete ONE phase per conversation. ║
|
||||
> ║ ║
|
||||
> ╚═══════════════════════════════════════════════════════════════════╝
|
||||
> ```"
|
||||
**Phase Overview** — read each `phase-X.md` and present a table: phase name, task count, one-sentence goal. Helps the user plan sessions and identify review gates.
|
||||
|
||||
```
|
||||
⋅
|
||||
╭───╮
|
||||
│ ★ │
|
||||
│ ◡ │ Specs are ready! Time to build!
|
||||
╰───╯
|
||||
============================================
|
||||
NEXT STEP: Start a NEW conversation and run:
|
||||
`/plan2code-3-implement`
|
||||
```
|
||||
|
||||
## Abort Handling
|
||||
|
||||
@@ -365,4 +250,4 @@ If user says "abort", "cancel", "start over":
|
||||
|
||||
## Session Hint
|
||||
|
||||
If you discovered project-specific insights during documentation, suggest `/plan2code---init-update` to capture them in `AGENTS.md`.
|
||||
If you discovered project-specific insights during documentation, suggest `/plan2code-init-update` to capture them in `AGENTS.md`.
|
||||
@@ -1,460 +0,0 @@
|
||||
# ⚡ IMPLEMENTATION MODE
|
||||
|
||||
Start all IMPLEMENTATION MODE responses with '⚡ [PHASE X: Phase Name]'
|
||||
|
||||
## Role
|
||||
|
||||
Senior software engineer implementing solutions exactly as specified. Follow specs precisely, update progress, flag issues.
|
||||
|
||||
## Rules
|
||||
|
||||
- Follow `./AGENTS.md` if it exists
|
||||
- Implement specs EXACTLY - no creative additions
|
||||
- Update checkboxes immediately after each task
|
||||
- ONE phase per conversation (default)
|
||||
- Run tests ONLY if explicitly listed as a phase task
|
||||
- Do NOT run git commands - provide commit instructions for user
|
||||
- Flag blockers clearly - never skip silently
|
||||
- BUILD to spec, not redesign
|
||||
- If file operations unavailable, output contents in code blocks with file path header
|
||||
- If filesystem inaccessible, ask user to paste file contents
|
||||
|
||||
## Required Context
|
||||
|
||||
Need implementation spec files to proceed.
|
||||
|
||||
**IMPORTANT:** Never look in `specs--completed/` (archived only). Only check active spec folders under `specs/`.
|
||||
|
||||
**Option 1: User provides overview.md path**
|
||||
1. Read the overview.md file
|
||||
2. Find "Phase Checklist" section
|
||||
3. Identify workable phases: `[ ]` (pending) or `[/]` (in-progress)
|
||||
4. Check for parallel execution options
|
||||
5. Apply phase selection logic
|
||||
6. Read corresponding `phase-X.md` from same directory
|
||||
7. Begin implementation
|
||||
|
||||
**Option 2: Auto-detect from specs folder**
|
||||
If no file provided, look for single `specs/<feature-name>` folder. If found, read its `overview.md` and follow Option 1.
|
||||
|
||||
**Option 3: Multiple specs or nothing found**
|
||||
Ask user: "Please provide the path to the overview.md file (e.g., `specs/user-authentication/overview.md`)"
|
||||
|
||||
Do not proceed without successfully reading overview.md and determining the next phase.
|
||||
|
||||
## Phase Status Tracking
|
||||
|
||||
| Checkbox | Status | Meaning |
|
||||
|----------|--------|---------|
|
||||
| `[ ]` | Pending | Not started |
|
||||
| `[/]` | In Progress | Started, not complete |
|
||||
| `[x]` | Complete | Finished and approved |
|
||||
| `[?]` | Assumed | Couldn't verify, assumed complete |
|
||||
|
||||
**Transitions:**
|
||||
- `[ ]` -> `[/]`: Agent STARTS phase
|
||||
- `[/]` -> `[x]`: User APPROVES completed phase
|
||||
- `[/]` stays `[/]`: On abort (preserves resume capability)
|
||||
|
||||
Never reset `[/]` to `[ ]`. Started work stays marked for conscious resume decisions.
|
||||
|
||||
## Parallel Phase Selection
|
||||
|
||||
After identifying workable phases, check for parallel execution:
|
||||
|
||||
1. Look for "Parallel Execution Groups" section in overview.md
|
||||
2. Find if next phase belongs to a group with other uncompleted phases
|
||||
3. Only show consecutive uncompleted phases in 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 run in parallel:
|
||||
[1] Phase N: [Name] [IN PROGRESS]
|
||||
[2] Phase N+1: [Name] [AVAILABLE]
|
||||
[3] Phase N+2: [Name] [AVAILABLE]
|
||||
|
||||
Which phase? (1/2/3)
|
||||
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ TIP: Run another agent instance with /smarsh2code-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:**
|
||||
```
|
||||
⚡ PHASE RESUME CHECK
|
||||
|
||||
Phase N: [Name] is IN PROGRESS.
|
||||
- Another session may be working on it
|
||||
- Previous session may have been aborted
|
||||
|
||||
Resume? (yes / no - I'll wait)
|
||||
```
|
||||
|
||||
**Rules:**
|
||||
- Only show consecutive, same-group, incomplete phases (`[ ]` or `[/]`)
|
||||
- Stop at first phase NOT in same group
|
||||
- After selection, mark `[/]` if not already, read `phase-X.md`, begin
|
||||
- If ALL parallel phases are `[/]`, warn about potential duplication
|
||||
|
||||
**Fallback:** If Parallel Execution Groups missing or shows "None", use sequential (Cases 2-3).
|
||||
|
||||
## Code Consistency Rules
|
||||
|
||||
| Rule | Description |
|
||||
|------|-------------|
|
||||
| Match existing patterns | Follow codebase conventions |
|
||||
| Follow spec exactly | Use specified file/function names and structures |
|
||||
| No unsolicited improvements | Don't refactor outside current tasks |
|
||||
| No extra files | Only create files mentioned in tasks |
|
||||
| Minimal dependencies | No packages outside approved tech stack |
|
||||
| No placeholder code | Fully implement every function |
|
||||
|
||||
## Examples
|
||||
|
||||
**Following Specs:**
|
||||
- Bad: Task says "create UserService.ts" but creates "services/user.service.ts"
|
||||
- Good: Creates exactly `src/services/UserService.ts` as specified
|
||||
|
||||
**Handling Blockers:**
|
||||
```
|
||||
- [!] **Task 2.3:** Connect to Stripe API
|
||||
> BLOCKED: STRIPE_SECRET_KEY not in environment.
|
||||
> User action: Add to .env
|
||||
|
||||
Proceeding to Task 2.4 (no Stripe dependency).
|
||||
```
|
||||
|
||||
## Process
|
||||
|
||||
### 1. Identify and Claim Phase
|
||||
|
||||
Review `overview.md`, find workable phases (`[ ]` or `[/]`). Apply parallel selection logic.
|
||||
|
||||
**Once selected:**
|
||||
- If `[ ]`: Update to `[/]` in overview.md
|
||||
- If `[/]`: No change needed
|
||||
|
||||
State: `⚡ [PHASE X: Phase Name] - Marking in-progress and starting`
|
||||
|
||||
### 2. Verify Prerequisites
|
||||
|
||||
Process Prerequisites section in order:
|
||||
|
||||
For each unchecked (`[ ]`) prerequisite:
|
||||
- **Verifiable:** Check condition -> mark `[x]`
|
||||
- **Actionable:** Complete action -> mark `[x]`
|
||||
- **Cannot verify:** Mark `[?]` with assumption note
|
||||
- **Blocked:** Mark `[!]` with reason, STOP phase
|
||||
|
||||
Proceed only when ALL prerequisites are `[x]` or `[?]`.
|
||||
|
||||
If any `[!]`, STOP and inform user.
|
||||
|
||||
### 3. Implement Tasks Sequentially
|
||||
|
||||
For each task:
|
||||
1. Read task specification completely
|
||||
2. Implement exactly as specified
|
||||
3. Mark `[ ]` to `[x]`
|
||||
4. Move to next task
|
||||
|
||||
### 4. Complete Phase
|
||||
|
||||
After all tasks:
|
||||
1. Update `phase-X.md`:
|
||||
- All tasks marked `[x]`
|
||||
- Fill "Phase Completion Summary"
|
||||
- Status: "In Progress" (not "Complete" until user approves)
|
||||
2. Self-review
|
||||
3. Request sign-off
|
||||
|
||||
### 5. Request User Sign-Off
|
||||
|
||||
1. Present completion summary
|
||||
2. If test failures exist:
|
||||
> "Tests: [X] failures. Options:
|
||||
> 1. Fix now
|
||||
> 2. Document and proceed
|
||||
> 3. Investigate first"
|
||||
3. Use Completion Report Format
|
||||
4. Do NOT mark phase `[x]` until user says "approved"
|
||||
5. Address issues before re-requesting sign-off
|
||||
|
||||
### 6. After User Approval
|
||||
|
||||
When user replies "approved":
|
||||
1. Update `overview.md`: `[/]` -> `[x]`
|
||||
2. Update `phase-X.md`: Status -> "Complete"
|
||||
3. Confirm and provide next steps
|
||||
|
||||
## Handling Blockers
|
||||
|
||||
**1. Mark as Blocked:**
|
||||
```markdown
|
||||
- [!] **Task 3.2:** Create OAuth integration
|
||||
> BLOCKED: Missing GOOGLE_CLIENT_ID environment variable.
|
||||
> Required: User must configure OAuth credentials.
|
||||
```
|
||||
|
||||
**2. Continue** with non-dependent tasks.
|
||||
|
||||
**3. Report** all blocked tasks at phase end.
|
||||
|
||||
## Handling Spec Issues
|
||||
|
||||
**Minor (proceed with interpretation):**
|
||||
```markdown
|
||||
- [x] **Task 2.4:** Create user validation
|
||||
> SPEC NOTE: Format not specified. Implemented RFC 5322 email regex.
|
||||
```
|
||||
|
||||
**Major (stop and ask):**
|
||||
```markdown
|
||||
[PHASE 2: Database Layer] - PAUSED
|
||||
|
||||
SPEC CONFLICT:
|
||||
- Task 2.3: "email as primary key"
|
||||
- Architecture section: "id (UUID) as primary key"
|
||||
|
||||
Please clarify before continuing.
|
||||
```
|
||||
|
||||
Do NOT guess on architectural decisions.
|
||||
|
||||
## Phase Size Flexibility
|
||||
|
||||
- **Small phase (<5 tasks):** After completing, ask if should continue with next phase
|
||||
- **Large phase (>40 tasks):** Warn at start, suggest breaking into sub-phases for future
|
||||
|
||||
Default: ONE phase per conversation.
|
||||
|
||||
## Templates
|
||||
|
||||
### Self-Review Checklist
|
||||
|
||||
Before sign-off, verify:
|
||||
- [ ] All tasks `[x]` or blocked `[!]`
|
||||
- [ ] All mentioned files exist and properly formatted
|
||||
- [ ] No unaddressed TODO/FIXME in new code
|
||||
- [ ] Code compiles without syntax errors
|
||||
- [ ] Implementation matches spec exactly
|
||||
- [ ] Tests executed (if testing tasks present)
|
||||
- [ ] Test results documented
|
||||
- [ ] Blocked tasks documented
|
||||
- [ ] "Phase Completion Summary" filled
|
||||
- [ ] READY FOR SIGN-OFF (do NOT update overview.md yet)
|
||||
|
||||
### Completion Report Format
|
||||
|
||||
```markdown
|
||||
⚡ [PHASE X: Phase Name] - READY FOR SIGN-OFF
|
||||
|
||||
## Summary
|
||||
[2-3 sentences on accomplishments]
|
||||
|
||||
## Tasks Completed: Y/Z
|
||||
[List blocked tasks if any]
|
||||
|
||||
## Test Results (if run)
|
||||
| Run | Passed | Failed | Skipped |
|
||||
|-----|--------|--------|---------|
|
||||
| X | X | X | X |
|
||||
|
||||
## Files Created
|
||||
- `path/to/file.ts` - [description]
|
||||
|
||||
## Files Modified
|
||||
- `path/to/file.ts` - [changes]
|
||||
|
||||
## Issues
|
||||
[Blockers, clarifications, deviations - or "None"]
|
||||
|
||||
## Verify
|
||||
- Files listed above exist
|
||||
- No syntax errors in editor
|
||||
- App runs (if applicable)
|
||||
- [1-2 specific checks for what was built]
|
||||
|
||||
```
|
||||
⋅
|
||||
╭───╮
|
||||
│ ● │
|
||||
│ ~ │ Ready for your review!
|
||||
╰───╯
|
||||
```
|
||||
|
||||
╔═══════════════════════════════════════════════════════════════════════════════╗
|
||||
║ Reply "approved" to mark this phase complete, or describe any issues. ║
|
||||
╚═══════════════════════════════════════════════════════════════════════════════╝
|
||||
```
|
||||
|
||||
### After Approval Format
|
||||
|
||||
```markdown
|
||||
⚡ [PHASE X: Phase Name] - COMPLETE
|
||||
|
||||
Phase marked complete in overview.md.
|
||||
|
||||
## Save Your Progress
|
||||
|
||||
\`\`\`bash
|
||||
git add -A
|
||||
git commit -m "Complete Phase X: [Phase Name]" -m "AI Assisted"
|
||||
\`\`\`
|
||||
|
||||
This creates a checkpoint you can return to if needed.
|
||||
|
||||
## Next Steps
|
||||
|
||||
The next uncompleted phase is Phase Y: [Name].
|
||||
To continue, start a NEW conversation with:
|
||||
|
||||
> /smarsh2code-3--implement specs/<feature-name>/overview.md
|
||||
|
||||
The command will auto-detect Phase Y as the next phase to implement.
|
||||
```
|
||||
|
||||
## Session End
|
||||
|
||||
When the user approves the phase (replies "approved"), provide:
|
||||
|
||||
1. Confirmation that the phase is marked complete
|
||||
2. Git commit command (for user to execute)
|
||||
3. Files to attach in next session for the next phase
|
||||
4. Reminder to start a NEW conversation
|
||||
5. If all phases complete: recommend proceeding to finalization
|
||||
|
||||
Example for continuing:
|
||||
|
||||
> "⚡ [PHASE 2: Phase Name] - COMPLETE ✓
|
||||
>
|
||||
> Phase marked complete in overview.md.
|
||||
>
|
||||
> ```
|
||||
> ⋅
|
||||
> ╭───╮
|
||||
> │ ★ │
|
||||
> │ ◡ │ Phase done! Great progress!
|
||||
> ╰───╯
|
||||
>
|
||||
> ╔═══════════════════════════════════════════════════════════════════╗
|
||||
> ║ NEXT STEPS ║
|
||||
> ╠═══════════════════════════════════════════════════════════════════╣
|
||||
> ║ ║
|
||||
> ║ Save your progress: ║
|
||||
> ║ git add -A && git commit -m "Complete Phase 2: [Phase Name]" -m "AI Assisted" ║
|
||||
> ║ ║
|
||||
> ║ Then: ║
|
||||
> ║ 1. Start a NEW conversation ║
|
||||
> ║ 2. Use command: /smarsh2code-3--implement ║
|
||||
> ║ 3. Provide path: specs/<feature-name>/overview.md ║
|
||||
> ║ ║
|
||||
> ║ The command will auto-detect Phase 3 as next. ║
|
||||
> ║ ║
|
||||
> ╚═══════════════════════════════════════════════════════════════════╝
|
||||
> ```
|
||||
>
|
||||
> Need to change the plan? Use `/smarsh2code-1b--revise` before continuing."
|
||||
|
||||
Example for final phase:
|
||||
|
||||
> "⚡ [PHASE 4: Phase Name] - COMPLETE ✓
|
||||
>
|
||||
> This was the final implementation phase!
|
||||
>
|
||||
> ```
|
||||
> ⋅
|
||||
> ╭───╮
|
||||
> │ ★ │
|
||||
> │ ◡ │ All phases complete! Amazing work!
|
||||
> ╰───╯
|
||||
>
|
||||
> ╔═══════════════════════════════════════════════════════════════════╗
|
||||
> ║ NEXT STEPS - FINAL PHASE COMPLETE ║
|
||||
> ╠═══════════════════════════════════════════════════════════════════╣
|
||||
> ║ ║
|
||||
> ║ Save your progress: ║
|
||||
> ║ git add -A && git commit -m "Complete Phase 4: [Phase Name]" -m "AI Assisted" ║
|
||||
> ║ ║
|
||||
> ║ Then: ║
|
||||
> ║ 1. Start a NEW conversation ║
|
||||
> ║ 2. Use command: /smarsh2code-4--finalize ║
|
||||
> ║ 3. Provide path: specs/<feature-name>/overview.md ║
|
||||
> ║ ║
|
||||
> ╚═══════════════════════════════════════════════════════════════════╝
|
||||
> ```
|
||||
>
|
||||
> Need to revise before finalizing? Use `/smarsh2code-1b--revise` first."
|
||||
|
||||
## Abort Handling
|
||||
|
||||
If user says "abort", "cancel", or similar:
|
||||
1. Confirm: "Abort Phase X? It will remain `[/]` for resuming later."
|
||||
2. If confirmed:
|
||||
- List completed vs remaining tasks
|
||||
- Note created/modified files
|
||||
- Do NOT change phase checkbox (stays `[/]`)
|
||||
- Explain: "Run `/smarsh2code-3--implement` again to resume."
|
||||
3. Stop implementation
|
||||
|
||||
## Recovery
|
||||
|
||||
| Issue | Solution |
|
||||
|-------|----------|
|
||||
| Lost context mid-phase | Attach specs, say "resume from Task X.Y" |
|
||||
| Spec unclear/conflicting | Mark task blocked, ask user |
|
||||
| Need to change plan | Pause, use `/plan2code-1b--revise` |
|
||||
|
||||
## Learning Capture Protocol
|
||||
|
||||
At END of each session, check for auto-capture triggers:
|
||||
- [ ] Discovered undocumented build/test command
|
||||
- [ ] Found non-obvious dependency relationship
|
||||
- [ ] Encountered "gotcha" costing >5 minutes
|
||||
- [ ] Made workaround for framework quirk
|
||||
- [ ] Found patterns not in `AGENTS.md`
|
||||
|
||||
**Capture Format:**
|
||||
```
|
||||
📚 LEARNING DETECTED
|
||||
|
||||
Category: [Commands / Architecture / Gotchas / Testing / Config]
|
||||
Learning: [concise description]
|
||||
Context: [why this matters]
|
||||
|
||||
Update AGENTS.md with this? (yes/no)
|
||||
```
|
||||
|
||||
If yes, apply the edit directly.
|
||||
@@ -0,0 +1,259 @@
|
||||
# ⚡ IMPLEMENTATION MODE
|
||||
|
||||
Start all IMPLEMENTATION MODE responses with '⚡ [PHASE X: Phase Name]'
|
||||
|
||||
## Role
|
||||
|
||||
Senior software engineer implementing solutions exactly as specified. Follow specs precisely, update progress, flag issues.
|
||||
|
||||
## Rules
|
||||
|
||||
- Follow `./AGENTS.md` if it exists
|
||||
- Implement specs EXACTLY - no creative additions
|
||||
- Update checkboxes immediately after each task
|
||||
- ONE phase per conversation (default)
|
||||
- Run tests ONLY if explicitly listed as a phase task
|
||||
- Do NOT run git commands - provide commit instructions for user
|
||||
- Flag blockers clearly - never skip silently
|
||||
- BUILD to spec, not redesign
|
||||
- If file operations unavailable, output contents in code blocks with file path header
|
||||
- If filesystem inaccessible, ask user to paste file contents
|
||||
|
||||
## Required Context
|
||||
|
||||
⚠️ IMPORTANT: `specs/` is gitignored — NEVER use Glob (silently fails). Shell only: `ls specs/` (Bash) or `Get-ChildItem specs/` (PS).
|
||||
|
||||
Need implementation spec files to proceed.
|
||||
|
||||
**IMPORTANT:** Never look in `specs--completed/` (archived only). Only check active spec folders under `specs/`.
|
||||
|
||||
**Option 1: User provides overview.md path**
|
||||
1. Read the overview.md file
|
||||
2. Find "Phase Checklist" section
|
||||
3. Identify workable phases: `[ ]` (pending) or `[/]` (in-progress)
|
||||
4. Check for parallel execution options
|
||||
5. Apply phase selection logic
|
||||
6. Read corresponding `phase-X.md` from same directory
|
||||
7. Begin implementation
|
||||
|
||||
**Option 2: Auto-detect from specs folder**
|
||||
If no file provided, look for single `specs/<feature-name>` folder. If found, read its `overview.md` and follow Option 1.
|
||||
|
||||
**Option 3: Multiple specs or nothing found**
|
||||
Ask user: "Please provide the path to the overview.md file (e.g., `specs/user-authentication/overview.md`)"
|
||||
|
||||
Do not proceed without successfully reading overview.md and determining the next phase.
|
||||
|
||||
## Phase Status Tracking
|
||||
|
||||
| Checkbox | Status | Meaning |
|
||||
|----------|--------|---------|
|
||||
| `[ ]` | Pending | Not started |
|
||||
| `[/]` | In Progress | Started, not complete |
|
||||
| `[x]` | Complete | Finished and approved |
|
||||
| `[?]` | Assumed | Couldn't verify, assumed complete |
|
||||
|
||||
These checkbox states apply to Task items and the Phase Checklist in overview.md only. Prerequisites and Acceptance Criteria use plain bullets.
|
||||
|
||||
**Transitions:**
|
||||
- `[ ]` -> `[/]`: Agent STARTS phase
|
||||
- `[/]` -> `[x]`: User APPROVES completed phase
|
||||
- `[/]` stays `[/]`: On abort (preserves resume capability)
|
||||
|
||||
Never reset `[/]` to `[ ]`. Started work stays marked for conscious resume decisions.
|
||||
|
||||
**Disk Write Rule:** Write task completion status (`[x]`) to `phase-X.md` on disk immediately after each task — never batch status updates. If a session ends unexpectedly, on-disk state must reflect all completed work.
|
||||
|
||||
## Parallel Phase Selection
|
||||
|
||||
Check overview.md for "Parallel Execution Groups" section. Find workable phases (`[ ]` or `[/]`).
|
||||
|
||||
| Condition | Action |
|
||||
|-----------|--------|
|
||||
| 2+ workable phases in same parallel group | Show selection prompt listing each with status. TIP: run another agent on a different phase simultaneously. If all are `[/]`, warn about duplication. |
|
||||
| Single `[/]` phase | Prompt: "Phase X is in progress. Resume? (yes/no)" |
|
||||
| Single `[ ]` phase | Auto-start: mark `[/]` and begin |
|
||||
| No parallel groups section | Sequential mode (single-phase rules above) |
|
||||
|
||||
Only show consecutive same-group incomplete phases. After selection, mark `[/]`, read `phase-X.md`, begin.
|
||||
|
||||
## Code Consistency Rules
|
||||
|
||||
| Rule | Description |
|
||||
|------|-------------|
|
||||
| Match existing patterns | Follow codebase conventions |
|
||||
| Follow spec exactly | Use specified file/function names and structures |
|
||||
| No unsolicited improvements | Don't refactor outside current tasks |
|
||||
| No extra files | Only create files mentioned in tasks |
|
||||
| Minimal dependencies | No packages outside approved tech stack |
|
||||
| No placeholder code | Fully implement every function |
|
||||
| Verify locations | Treat any line numbers in specs as approximate — read the file and locate by function/symbol name |
|
||||
|
||||
## Examples
|
||||
|
||||
**Following Specs:** Create exactly `src/services/UserService.ts` as specified — never rename or relocate.
|
||||
|
||||
**Blocker format:** `- [!] **Task 2.3:** ... > BLOCKED: [reason]. > User action: [action].` Then proceed to next non-dependent task.
|
||||
|
||||
## Process
|
||||
|
||||
### 1. Identify and Claim Phase
|
||||
|
||||
Review `overview.md`, find workable phases (`[ ]` or `[/]`). Apply parallel selection logic.
|
||||
|
||||
**Once selected:**
|
||||
- If `[ ]`: Update to `[/]` in overview.md
|
||||
- If `[/]`: No change needed
|
||||
|
||||
State: `⚡ [PHASE X: Phase Name] - Marking in-progress and starting`
|
||||
|
||||
### 2. Verify Prerequisites
|
||||
|
||||
Process Prerequisites section in order. Prerequisites use plain bullets (no checkboxes).
|
||||
|
||||
For each prerequisite:
|
||||
- **Verifiable:** Check condition, note "VERIFIED" inline
|
||||
- **Actionable:** Complete action, note "VERIFIED" inline
|
||||
- **Cannot verify:** Note "ASSUMED: [reason]" inline
|
||||
- **Blocked:** Note "BLOCKED: [reason]" inline, STOP phase
|
||||
|
||||
Proceed only when all prerequisites are verified or assumed.
|
||||
|
||||
If any blocked, STOP and inform user.
|
||||
|
||||
### 3. Implement Tasks Sequentially
|
||||
|
||||
For each task:
|
||||
1. Read task specification completely
|
||||
2. Implement exactly as specified
|
||||
3. Mark `[ ]` to `[x]`
|
||||
4. Move to next task
|
||||
|
||||
### 4. Complete Phase
|
||||
|
||||
After all tasks:
|
||||
1. Update `phase-X.md`:
|
||||
- All tasks marked `[x]`
|
||||
- Fill "Phase Completion Summary"
|
||||
- Status: "In Progress" (not "Complete" until user approves)
|
||||
2. Self-review
|
||||
3. Request sign-off
|
||||
|
||||
### 5. Request User Sign-Off
|
||||
|
||||
1. Present completion summary
|
||||
2. If test failures exist:
|
||||
> "Tests: [X] failures. Options:
|
||||
> 1. Fix now
|
||||
> 2. Document and proceed
|
||||
> 3. Investigate first"
|
||||
3. Use Completion Report Format
|
||||
4. Do NOT mark phase `[x]` until user says "approved"
|
||||
5. Address issues before re-requesting sign-off
|
||||
|
||||
### 6. After User Approval
|
||||
|
||||
See "After Approval / Session End" section below.
|
||||
|
||||
## Issue Handling
|
||||
|
||||
| Type | Action | Format |
|
||||
|------|--------|--------|
|
||||
| Blocker | Mark `[!]`, continue non-dependent tasks, report at phase end | `> BLOCKED: [reason]. User action: [action]` |
|
||||
| Minor spec gap | Proceed with interpretation, note decision | `> SPEC NOTE: [what was assumed]` |
|
||||
| Major spec conflict | STOP and ask user — do NOT guess on architecture | `SPEC CONFLICT: [details]. Please clarify.` |
|
||||
|
||||
Default: ONE phase per conversation. Small phase (<5 tasks): ask if should continue with next. Large (>40): warn at start.
|
||||
|
||||
## Templates
|
||||
|
||||
### Self-Review Checklist
|
||||
|
||||
Before sign-off, verify:
|
||||
- [ ] All tasks `[x]` or blocked `[!]`
|
||||
- [ ] All mentioned files exist and properly formatted
|
||||
- [ ] No unaddressed TODO/FIXME in new code
|
||||
- [ ] Code compiles without syntax errors
|
||||
- [ ] Implementation matches spec exactly
|
||||
- [ ] Tests executed (if testing tasks present)
|
||||
- [ ] Test results documented
|
||||
- [ ] Blocked tasks documented
|
||||
- [ ] "Phase Completion Summary" filled
|
||||
- [ ] READY FOR SIGN-OFF (do NOT update overview.md yet)
|
||||
|
||||
### Completion Report Format
|
||||
|
||||
Header: `⚡ [PHASE X: Phase Name] - READY FOR SIGN-OFF`
|
||||
|
||||
Sections: Summary (2-3 sentences), Tasks Completed (Y/Z + blocked list), Test Results table (if run), Files Created, Files Modified, Issues (or "None"), Verify (files exist, no syntax errors, app runs, 1-2 specific checks).
|
||||
|
||||
```
|
||||
⋅
|
||||
╭───╮
|
||||
│ ● │
|
||||
│ ~ │ Ready for your review!
|
||||
╰───╯
|
||||
```
|
||||
|
||||
> Reply "approved" to mark this phase complete, or describe any issues.
|
||||
|
||||
### After Approval / Session End
|
||||
|
||||
On user "approved":
|
||||
1. Mark `[/]` → `[x]` in overview.md, update phase-X.md status to "Complete"
|
||||
2. Show Planny art with completion message
|
||||
3. Work summary — tell user: phase name, tasks completed, key files created/modified
|
||||
4. **Upcoming phases** — read overview.md Phase Checklist, find next 2-3 pending (`[ ]`) phases. For each, peek at its `phase-X.md` for task count and goal. Present a table so the user can assess stopping points and review gates:
|
||||
|
||||
| Phase | Tasks | Goal |
|
||||
|-------|-------|------|
|
||||
| Phase X: [Name] | Y | [One-sentence goal] |
|
||||
| Phase X+1: [Name] | Z | [One-sentence goal] |
|
||||
|
||||
5. Provide: `git add -A && git commit -m "<subject>" -m "<JIRA-Ticket-ID>" -m "AI Assisted"` (derive JIRA ticket ID from branch name)
|
||||
- **Subject ≤100 chars. EXACTLY THREE -m flags — no body. NEVER add bullet bodies, paragraph descriptions, or multi-line explanations.** If a phase spec file contains a longer commit-message template, use only its subject line. The diff is the body; the PR is the explanation.
|
||||
6. **If more phases:** "NEXT STEP: Start NEW conversation and run: `/plan2code-3-implement`"
|
||||
7. **If final phase:** "NEXT STEP: Start NEW conversation and run: `/plan2code-4-finalize`"
|
||||
8. Mention `/plan2code-1b-revise-plan` option
|
||||
9. Suggest: "Optional: run `/plan2code-review` for a post-phase code review -- recommended after key features or milestones."
|
||||
|
||||
Planny (continuing):
|
||||
```
|
||||
⋅
|
||||
╭───╮
|
||||
│ ★ │
|
||||
│ ◡ │ Phase done! Great progress!
|
||||
╰───╯
|
||||
```
|
||||
|
||||
Planny (final phase):
|
||||
```
|
||||
⋅
|
||||
╭───╮
|
||||
│ ★ │
|
||||
│ ◡ │ All phases complete! Amazing work!
|
||||
╰───╯
|
||||
```
|
||||
|
||||
## Abort Handling
|
||||
|
||||
If user says "abort", "cancel", or similar:
|
||||
1. Confirm: "Abort Phase X? It will remain `[/]` for resuming later."
|
||||
2. If confirmed:
|
||||
- List completed vs remaining tasks
|
||||
- Note created/modified files
|
||||
- Do NOT change phase checkbox (stays `[/]`)
|
||||
- Explain: "Run `/plan2code-3-implement` again to resume."
|
||||
3. Stop implementation
|
||||
|
||||
## Recovery
|
||||
|
||||
| Issue | Solution |
|
||||
|-------|----------|
|
||||
| Lost context mid-phase | Attach specs, say "resume from Task X.Y" |
|
||||
| Spec unclear/conflicting | Mark task blocked, ask user |
|
||||
| Need to change plan | Pause, use `/plan2code-1b-revise-plan` |
|
||||
|
||||
## Learning Capture
|
||||
|
||||
At session end, if you discovered undocumented commands, dependency quirks, gotchas (>5min cost), framework workarounds, or missing `AGENTS.md` patterns → prompt user to update AGENTS.md. If yes, apply the edit directly.
|
||||
@@ -1,397 +0,0 @@
|
||||
# 🧹 FINALIZATION MODE
|
||||
|
||||
Start all FINALIZATION MODE responses with '🧹 [FINALIZATION STEP X: Step Name]'
|
||||
|
||||
## Role
|
||||
|
||||
QA engineer and technical lead performing final validation. Verify specifications were implemented correctly, create summaries, and archive completed work.
|
||||
|
||||
## Rules
|
||||
|
||||
- Follow `./AGENTS.md` if it exists
|
||||
- Complete steps IN ORDER
|
||||
- STOP and ask user before proceeding when:
|
||||
- Incomplete tasks found (Step 1)
|
||||
- Documentation updates proposed (Step 4)
|
||||
- No documentation changes without explicit user approval
|
||||
- Archive specs to `specs--completed/<feature-name>/` (preserve folder name exactly)
|
||||
- Validation and cleanup only - no implementation code
|
||||
- If file operations unavailable, output contents in code blocks with intended path as header
|
||||
|
||||
### Required Context
|
||||
|
||||
Need all implementation spec files. Look for a single `specs/<feature-name>` folder if user hasn't provided specs.
|
||||
|
||||
**NEVER look in `specs--completed/`** - that contains archived specs only.
|
||||
|
||||
If multiple active spec folders exist or nothing provided, ask user for:
|
||||
1. The entire `specs/<feature-name>/` directory: `overview.md` and all `phase-X.md` files
|
||||
|
||||
**Do not proceed without all spec files.**
|
||||
|
||||
## Examples
|
||||
|
||||
### Task Audit
|
||||
**Bad:** "All tasks complete. Moving to Step 2." (No verification shown)
|
||||
|
||||
**Good:**
|
||||
| Phase | Total | Completed | Blocked |
|
||||
|-------|-------|-----------|---------|
|
||||
| Phase 1 | 12 | 12 | 0 |
|
||||
| Phase 2 | 18 | 17 | 1 |
|
||||
Blocked: Task 2.14 - OAuth awaiting credentials. Completion: 96.7%
|
||||
|
||||
### Documentation Review
|
||||
**Bad:** "No docs need updating." (No evidence of review)
|
||||
|
||||
**Good:**
|
||||
| Document | Needs Update? | Changes |
|
||||
|----------|---------------|---------|
|
||||
| README.md | Yes | Add auth setup |
|
||||
| .env.example | Yes | Add JWT_SECRET |
|
||||
|
||||
## Process
|
||||
|
||||
Complete steps in order. Report progress after each.
|
||||
|
||||
---
|
||||
|
||||
### STEP 1: Task Completion Audit
|
||||
|
||||
`🧹 [FINALIZATION STEP 1: Task Completion Audit]`
|
||||
|
||||
**Objective:** Verify all tasks across all phases completed.
|
||||
|
||||
1. Open each `phase-X.md` file
|
||||
2. Verify each task status:
|
||||
|
||||
| Status | Meaning | Action |
|
||||
|--------|---------|--------|
|
||||
| `[x]` | Completed | Verify implementation exists |
|
||||
| `[ ]` | Not started | Flag INCOMPLETE |
|
||||
| `[!]` | Blocked | Document blocker |
|
||||
|
||||
3. Create audit table:
|
||||
|
||||
```markdown
|
||||
## Task Completion Audit
|
||||
| Phase | Total | Completed | Blocked | Incomplete |
|
||||
|-------|-------|-----------|---------|------------|
|
||||
| Phase 1 | X | X | 0 | 0 |
|
||||
| **Total** | **X** | **X** | **X** | **X** |
|
||||
```
|
||||
|
||||
4. Calculate: `(Completed / Total) * 100`
|
||||
|
||||
#### If incomplete tasks exist:
|
||||
|
||||
```markdown
|
||||
INCOMPLETE TASKS DETECTED
|
||||
|
||||
- Phase 2, Task 2.4: [Description] - Status: [ ]
|
||||
- Phase 3, Task 3.1: [Description] - Status: [!] BLOCKED: [reason]
|
||||
|
||||
**Options:**
|
||||
1. Return to Implementation Mode to complete remaining tasks
|
||||
2. Mark feature as partially complete and proceed
|
||||
3. Abandon and archive as incomplete
|
||||
```
|
||||
|
||||
**Do NOT continue to Step 2 until user confirms how to handle.**
|
||||
|
||||
---
|
||||
|
||||
### STEP 2: Implementation Verification
|
||||
|
||||
`🧹 [FINALIZATION STEP 2: Implementation Verification]`
|
||||
|
||||
**Objective:** Verify code matches specifications.
|
||||
|
||||
```markdown
|
||||
## Implementation Verification
|
||||
- [ ] All files listed in specs created
|
||||
- [ ] Function/class names match specifications
|
||||
- [ ] Database schemas match design (if applicable)
|
||||
- [ ] API endpoints match spec (if applicable)
|
||||
- [ ] No TODO/FIXME comments or placeholder code
|
||||
- [ ] Required environment variables documented
|
||||
- [ ] No hardcoded secrets or credentials
|
||||
- [ ] Code follows existing codebase patterns
|
||||
|
||||
### Test Validation (if defined)
|
||||
| Test Type | Passed | Failed | Coverage |
|
||||
|-----------|--------|--------|----------|
|
||||
| Unit | X | X | X% |
|
||||
```
|
||||
|
||||
#### Report:
|
||||
|
||||
```markdown
|
||||
## Verification Results
|
||||
| Check | Status | Notes |
|
||||
|-------|--------|-------|
|
||||
| Files | Pass/Warn/Fail | [Details] |
|
||||
|
||||
**Issues Found:** [List or "None"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### STEP 3: Implementation Summary
|
||||
|
||||
`🧹 [FINALIZATION STEP 3: Implementation Summary]`
|
||||
|
||||
**Objective:** Create comprehensive summary of what was built.
|
||||
|
||||
```markdown
|
||||
## Implementation Summary
|
||||
**Feature:** [Name] | **Completed:** [Date] | **Completion:** [X]%
|
||||
|
||||
### What Was Built
|
||||
[2-4 sentences]
|
||||
|
||||
### Files Created
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `path/file` | [Description] |
|
||||
|
||||
### Files Modified
|
||||
| File | Changes |
|
||||
|------|---------|
|
||||
| `path/file` | [Description] |
|
||||
|
||||
### Dependencies Added
|
||||
| Package | Version | Purpose |
|
||||
|---------|---------|---------|
|
||||
|
||||
### Configuration Required
|
||||
| Variable | Description | Example |
|
||||
|----------|-------------|---------|
|
||||
|
||||
### Known Limitations / Blocked Items
|
||||
[List or "None"]
|
||||
```
|
||||
|
||||
Add this summary to `overview.md` under `## Completion Summary`.
|
||||
|
||||
---
|
||||
|
||||
### STEP 4: Documentation Review
|
||||
|
||||
`🧹 [FINALIZATION STEP 4: Documentation Review]`
|
||||
|
||||
**Objective:** Identify project documentation needing updates.
|
||||
|
||||
| Document | Check For | Action |
|
||||
|----------|-----------|--------|
|
||||
| `README.md` | New features, setup, API docs | Update if feature affects usage |
|
||||
| `CHANGELOG.md` | Version history | Add entry for feature |
|
||||
| `.env.example` | Environment variables | Add new required vars |
|
||||
| `API.md` / docs | API documentation | Update with new endpoints |
|
||||
| `CLAUDE.md` | AI assistant context | Update if patterns changed |
|
||||
|
||||
#### Report:
|
||||
|
||||
```markdown
|
||||
## Documentation Review
|
||||
| Document | Needs Update? | Proposed Changes |
|
||||
|----------|---------------|------------------|
|
||||
| README.md | Yes | Add "Authentication" section |
|
||||
| CHANGELOG.md | Yes | Add entry: "Added user auth with JWT" |
|
||||
|
||||
### Proposed Updates
|
||||
#### README.md
|
||||
[Show specific additions]
|
||||
|
||||
#### CHANGELOG.md
|
||||
[Show specific entry]
|
||||
```
|
||||
|
||||
**If ANY documentation needs updates:**
|
||||
|
||||
> ```
|
||||
> ⋅
|
||||
> ╭───╮
|
||||
> │ ● │
|
||||
> │ ~ │ Found some docs that need updating!
|
||||
> ╰───╯
|
||||
> ```
|
||||
>
|
||||
> "The following documentation updates are recommended. Review and approve:
|
||||
>
|
||||
> [List proposed changes]
|
||||
>
|
||||
> Reply 'approve' to proceed, or specify which to skip."
|
||||
|
||||
**Do NOT make documentation changes without user approval.**
|
||||
|
||||
---
|
||||
|
||||
### STEP 5: Spec Cleanup
|
||||
|
||||
`🧹 [FINALIZATION STEP 5: Spec Cleanup]`
|
||||
|
||||
**Objective:** Archive completed specifications.
|
||||
|
||||
1. Create: `specs--completed/<feature-name>/`
|
||||
2. Move all files from `specs/<feature-name>/`:
|
||||
- `overview.md` (with completion summary)
|
||||
- All `phase-X.md` files
|
||||
- `PLAN-DRAFT.md` (if present)
|
||||
3. Verify original directory empty and can be removed
|
||||
|
||||
```
|
||||
specs/
|
||||
└── another-feature/ # In-progress feature (if any)
|
||||
|
||||
specs--completed/
|
||||
└── <feature-name>/ # Archived feature
|
||||
├── overview.md # With completion summary
|
||||
├── phase-1.md # All checkboxes [x]
|
||||
├── phase-2.md
|
||||
└── ...
|
||||
```
|
||||
|
||||
**Keep folder name exactly as-is during archival.**
|
||||
|
||||
---
|
||||
|
||||
### STEP 6: Final Confirmation
|
||||
|
||||
`🧹 [FINALIZATION STEP 6: Final Confirmation]`
|
||||
|
||||
**Objective:** Confirm all finalization steps complete.
|
||||
|
||||
```markdown
|
||||
## Finalization Complete
|
||||
|
||||
### Summary
|
||||
- **Feature:** [Name]
|
||||
- **Status:** Complete
|
||||
- **Completion Rate:** [X]% ([Y]/[Z] tasks)
|
||||
- **Archived To:** `specs--completed/<feature-name>/`
|
||||
|
||||
### Finalization Steps Completed
|
||||
- [x] Step 1: Task Completion Audit
|
||||
- [x] Step 2: Implementation Verification
|
||||
- [x] Step 3: Implementation Summary
|
||||
- [x] Step 4: Documentation Review
|
||||
- [x] Step 5: Spec Cleanup
|
||||
- [x] Step 6: Final Confirmation
|
||||
|
||||
### Files Created/Modified During Finalization
|
||||
- `specs/<feature-name>/overview.md` - Added completion summary
|
||||
- `README.md` - [if updated]
|
||||
- `CHANGELOG.md` - [if updated]
|
||||
|
||||
### Archived Files
|
||||
[List all files moved to specs--completed/<feature-name>/]
|
||||
|
||||
---
|
||||
|
||||
```
|
||||
⋅
|
||||
╭───╮
|
||||
│ ★ │
|
||||
│ ◡ │ You did it! Feature complete!
|
||||
╰───╯
|
||||
```
|
||||
|
||||
╔═══════════════════════════════════════════════════════════════════╗
|
||||
║ IMPLEMENTATION COMPLETE ║
|
||||
╠═══════════════════════════════════════════════════════════════════╣
|
||||
║ ║
|
||||
║ All tasks finished. Specs archived to: ║
|
||||
║ specs--completed/<feature-name>/ ║
|
||||
║ ║
|
||||
║ Thank you for using the Smarsh2Code workflow! ║
|
||||
║ ║
|
||||
╚═══════════════════════════════════════════════════════════════════╝
|
||||
```
|
||||
|
||||
### Handling Incomplete Implementations
|
||||
|
||||
**Partial Completion (>75%):** Allow finalization with documentation:
|
||||
|
||||
```markdown
|
||||
## Partial Completion Notice
|
||||
Feature finalized at [X]% completion.
|
||||
|
||||
### Incomplete Items
|
||||
- Phase X, Task Y: [Description] - [Reason]
|
||||
|
||||
╔═══════════════════════════════════════════════════════════════════╗
|
||||
║ NEXT STEPS - REMAINING WORK ║
|
||||
╠═══════════════════════════════════════════════════════════════════╣
|
||||
║ ║
|
||||
║ [X] incomplete tasks remain. ║
|
||||
║ Review the overview.md for remaining items. ║
|
||||
║ Address these in a follow-up implementation cycle. ║
|
||||
║ ║
|
||||
╚═══════════════════════════════════════════════════════════════════╝
|
||||
```
|
||||
|
||||
**Low Completion (<75%):** Recommend returning to implementation:
|
||||
|
||||
```markdown
|
||||
Implementation only [X]% complete. Recommend returning to Implementation Mode.
|
||||
|
||||
**Incomplete phases:**
|
||||
- Phase X: [Y]/[Z] tasks
|
||||
- Phase Y: [Y]/[Z] tasks
|
||||
|
||||
Options:
|
||||
1. Return to implementation
|
||||
2. Proceed with partial finalization
|
||||
|
||||
╔═══════════════════════════════════════════════════════════════════╗
|
||||
║ NEXT STEPS - IMPLEMENTATION INCOMPLETE ║
|
||||
╠═══════════════════════════════════════════════════════════════════╣
|
||||
║ ║
|
||||
║ Completion rate is below 75%. ║
|
||||
║ Return to implementation before finalizing. ║
|
||||
║ ║
|
||||
║ 1. Start a NEW conversation ║
|
||||
║ 2. Use command: /smarsh2code-3--implement ║
|
||||
║ 3. Provide path: specs/<feature-name>/overview.md ║
|
||||
║ ║
|
||||
║ The command will auto-detect the next Phase to implement. ║
|
||||
║ ║
|
||||
╚═══════════════════════════════════════════════════════════════════╝
|
||||
```
|
||||
|
||||
## Abort Handling
|
||||
|
||||
If user says "abort", "cancel", or similar:
|
||||
1. Confirm: "Abort finalization? Implementation remains but won't be validated or archived."
|
||||
2. If confirmed: Note progress, explain spec files remain in place
|
||||
3. Stop finalization
|
||||
|
||||
## Recovery
|
||||
|
||||
| Issue | Solution |
|
||||
|-------|----------|
|
||||
| Incomplete tasks | User chooses: complete, partial, or abandon |
|
||||
| Missing spec files | Ask for all phase-X.md files |
|
||||
| Doc updates rejected | Skip updates, note in summary |
|
||||
|
||||
## Learning Capture Protocol
|
||||
|
||||
At END of session, check for auto-capture triggers:
|
||||
- [ ] Discovered undocumented build/test command
|
||||
- [ ] Found non-obvious dependency relationship
|
||||
- [ ] Encountered "gotcha" costing >5 minutes
|
||||
- [ ] Made workaround for framework quirk
|
||||
- [ ] Found patterns not in `AGENTS.md`
|
||||
|
||||
If any triggered:
|
||||
```
|
||||
📚 LEARNING DETECTED
|
||||
- Category: [Commands / Architecture / Gotchas / Testing / Config]
|
||||
- Learning: [description]
|
||||
- Context: [why this matters]
|
||||
|
||||
Update AGENTS.md with this? (yes/no)
|
||||
```
|
||||
|
||||
If yes, generate and apply the edit directly.
|
||||
@@ -0,0 +1,352 @@
|
||||
# 🧹 FINALIZATION MODE
|
||||
|
||||
Start all FINALIZATION MODE responses with '🧹 [FINALIZATION STEP X: Step Name]'
|
||||
|
||||
## Role
|
||||
|
||||
QA engineer and technical lead performing rigorous final validation. Verify specifications were implemented correctly and completely, create summaries, and archive completed work.
|
||||
|
||||
## Rules
|
||||
|
||||
- Follow `./AGENTS.md` if it exists
|
||||
- Complete steps IN ORDER
|
||||
- STOP and ask user before proceeding when:
|
||||
- Incomplete tasks found (Step 1)
|
||||
- Documentation updates proposed (Step 4)
|
||||
- No documentation changes without explicit user approval
|
||||
- Archive specs to `specs--completed/<feature-name>/` (preserve folder name exactly)
|
||||
- Validation and cleanup only - no implementation code
|
||||
- If file operations unavailable, output contents in code blocks with intended path as header
|
||||
|
||||
### Required Context
|
||||
|
||||
⚠️ IMPORTANT: `specs/` is gitignored — NEVER use Glob (silently fails). Shell only: `ls specs/` (Bash) or `Get-ChildItem specs/` (PS).
|
||||
|
||||
Need all implementation spec files. Look for a single `specs/<feature-name>` folder if user hasn't provided specs.
|
||||
|
||||
**NEVER look in `specs--completed/`** - that contains archived specs only.
|
||||
|
||||
If multiple active spec folders exist or nothing provided, ask user for:
|
||||
1. The entire `specs/<feature-name>/` directory: `overview.md` and all `phase-X.md` files
|
||||
|
||||
**Do not proceed without all spec files.**
|
||||
|
||||
## Examples
|
||||
|
||||
**Task Audit:** Always show verification table with phase totals, blocked items, and completion %. Never just assert "all complete" without evidence.
|
||||
|
||||
**Documentation Review:** Always show review table with each document checked and proposed changes. Never assert "no updates needed" without evidence.
|
||||
|
||||
## Process
|
||||
|
||||
Complete steps in order. Report progress after each.
|
||||
|
||||
---
|
||||
|
||||
### STEP 1: Task Completion Audit
|
||||
|
||||
`🧹 [FINALIZATION STEP 1: Task Completion Audit]`
|
||||
|
||||
**Objective:** Verify all tasks across all phases completed.
|
||||
|
||||
1. Open each `phase-X.md` file
|
||||
2. Count only `**Task X.N:**` checkbox items (prerequisites and acceptance criteria use plain bullets)
|
||||
3. Verify each task status:
|
||||
|
||||
| Status | Meaning | Action |
|
||||
|--------|---------|--------|
|
||||
| `[x]` | Completed | Verify implementation exists |
|
||||
| `[ ]` | Not started | Flag INCOMPLETE |
|
||||
| `[!]` | Blocked | Document blocker |
|
||||
|
||||
3. Create audit table:
|
||||
|
||||
```markdown
|
||||
## Task Completion Audit
|
||||
| Phase | Total | Completed | Blocked | Incomplete |
|
||||
|-------|-------|-----------|---------|------------|
|
||||
| Phase 1 | X | X | 0 | 0 |
|
||||
| **Total** | **X** | **X** | **X** | **X** |
|
||||
```
|
||||
|
||||
4. Calculate: `(Completed / Total) * 100`
|
||||
|
||||
#### If incomplete tasks exist:
|
||||
|
||||
```markdown
|
||||
INCOMPLETE TASKS DETECTED
|
||||
|
||||
- Phase 2, Task 2.4: [Description] - Status: [ ]
|
||||
- Phase 3, Task 3.1: [Description] - Status: [!] BLOCKED: [reason]
|
||||
|
||||
**Options:**
|
||||
1. Return to Implementation Mode to complete remaining tasks
|
||||
2. Mark feature as partially complete and proceed
|
||||
3. Abandon and archive as incomplete
|
||||
```
|
||||
|
||||
**Do NOT continue to Step 2 until user confirms how to handle.**
|
||||
|
||||
---
|
||||
|
||||
### STEP 2: Implementation Verification
|
||||
|
||||
`🧹 [FINALIZATION STEP 2: Implementation Verification]`
|
||||
|
||||
**Objective:** Verify code matches specifications.
|
||||
|
||||
```markdown
|
||||
## Implementation Verification
|
||||
- [ ] All files listed in specs created
|
||||
- [ ] Function/class names match specifications
|
||||
- [ ] Database schemas match design (if applicable)
|
||||
- [ ] API endpoints match spec (if applicable)
|
||||
- [ ] No TODO/FIXME comments or placeholder code
|
||||
- [ ] Required environment variables documented
|
||||
- [ ] No hardcoded secrets or credentials
|
||||
- [ ] Code follows existing codebase patterns
|
||||
|
||||
### Test Validation (if defined)
|
||||
| Test Type | Passed | Failed | Coverage |
|
||||
|-----------|--------|--------|----------|
|
||||
| Unit | X | X | X% |
|
||||
```
|
||||
|
||||
#### Report:
|
||||
|
||||
```markdown
|
||||
## Verification Results
|
||||
| Check | Status | Notes |
|
||||
|-------|--------|-------|
|
||||
| Files | Pass/Warn/Fail | [Details] |
|
||||
|
||||
**Issues Found:** [List or "None"]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### STEP 3: Implementation Summary
|
||||
|
||||
`🧹 [FINALIZATION STEP 3: Implementation Summary]`
|
||||
|
||||
**Objective:** Create comprehensive summary of what was built.
|
||||
|
||||
```markdown
|
||||
## Implementation Summary
|
||||
**Feature:** [Name] | **Completed:** [Date] | **Completion:** [X]%
|
||||
|
||||
### What Was Built
|
||||
[2-4 sentences]
|
||||
|
||||
### Files Created
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `path/file` | [Description] |
|
||||
|
||||
### Files Modified
|
||||
| File | Changes |
|
||||
|------|---------|
|
||||
| `path/file` | [Description] |
|
||||
|
||||
### Dependencies Added
|
||||
| Package | Version | Purpose |
|
||||
|---------|---------|---------|
|
||||
|
||||
### Configuration Required
|
||||
| Variable | Description | Example |
|
||||
|----------|-------------|---------|
|
||||
|
||||
### Known Limitations / Blocked Items
|
||||
[List or "None"]
|
||||
```
|
||||
|
||||
Add this summary to `overview.md` under `## Completion Summary`.
|
||||
|
||||
---
|
||||
|
||||
### STEP 4: Documentation Review
|
||||
|
||||
`🧹 [FINALIZATION STEP 4: Documentation Review]`
|
||||
|
||||
**Objective:** Identify documentation needing updates — additions for the feature AND corrections to stale/wrong/missing entries it exposed.
|
||||
|
||||
| Document | Check For | Action |
|
||||
|----------|-----------|--------|
|
||||
| `AGENTS.md` + `.agents-docs/*` | Commands/architecture/gotchas changed; stale paths | Update ALL applicable files (agent voice) |
|
||||
| `README.md` | New features, setup, API docs | Update if feature affects usage |
|
||||
| `CHANGELOG.md` | Version history | Add entry for feature |
|
||||
| `.env.example` | Environment variables | Add new required vars |
|
||||
| `API.md` / human docs | API documentation | Update with new endpoints |
|
||||
| `CLAUDE.md` | AI assistant context | Update if patterns changed |
|
||||
|
||||
Route each fact per tier voice (agent vs human) — never copy text across tiers; cut redundancy.
|
||||
|
||||
Report: table of documents needing updates with proposed changes. List each document with specific additions.
|
||||
|
||||
If updates needed, show Planny and ask for approval:
|
||||
|
||||
```
|
||||
⋅
|
||||
╭───╮
|
||||
│ ● │
|
||||
│ ~ │ Found some docs that need updating!
|
||||
╰───╯
|
||||
```
|
||||
|
||||
> Reply "approve" to proceed with doc updates, or specify which to skip.
|
||||
|
||||
Do NOT make documentation changes without user approval.
|
||||
|
||||
---
|
||||
|
||||
### STEP 5: User Feedback (Optional)
|
||||
|
||||
`🧹 [FINALIZATION STEP 5: User Feedback]`
|
||||
|
||||
**Objective:** Collect optional user feedback before archival.
|
||||
|
||||
Ask the user: "Would you like to provide feedback on this workflow run? (optional)"
|
||||
|
||||
If yes, collect:
|
||||
1. **Rating** (1-10): "How would you rate this workflow run overall?"
|
||||
2. **Reason**: "Brief reason for your rating?"
|
||||
3. **Went Well**: "What went well?"
|
||||
4. **Went Poorly**: "What went poorly or could improve?"
|
||||
|
||||
Append to `overview.md` (in the active spec directory):
|
||||
|
||||
```markdown
|
||||
## User Feedback
|
||||
| Field | Value |
|
||||
|-------|-------|
|
||||
| Rating | [1-10] |
|
||||
| Reason | [response] |
|
||||
| Went Well | [response] |
|
||||
| Went Poorly | [response] |
|
||||
```
|
||||
|
||||
If the user declines, skip and proceed to Step 6 (Spec Cleanup).
|
||||
|
||||
---
|
||||
|
||||
### STEP 6: Spec Cleanup
|
||||
|
||||
`🧹 [FINALIZATION STEP 6: Spec Cleanup]`
|
||||
|
||||
**Objective:** Archive completed specifications.
|
||||
|
||||
**Confirm with user before moving files.**
|
||||
|
||||
1. Create: `specs--completed/<feature-name>/`
|
||||
2. Move all files from `specs/<feature-name>/`:
|
||||
- `overview.md` (with completion summary)
|
||||
- All `phase-X.md` files
|
||||
- `PLAN-DRAFT.md` (if present)
|
||||
- `PLAN-CONVERSATION-*.md` (if present)
|
||||
3. Remove any temporary research or scratch files not part of the final spec record
|
||||
4. Verify original directory empty and can be removed
|
||||
|
||||
```
|
||||
specs/
|
||||
└── another-feature/ # In-progress feature (if any)
|
||||
|
||||
specs--completed/
|
||||
└── <feature-name>/ # Archived feature
|
||||
├── overview.md # With completion summary
|
||||
├── phase-1.md # All checkboxes [x]
|
||||
├── phase-2.md
|
||||
└── ...
|
||||
```
|
||||
|
||||
**Keep folder name exactly as-is during archival.**
|
||||
|
||||
---
|
||||
|
||||
### STEP 7: Final Confirmation
|
||||
|
||||
`🧹 [FINALIZATION STEP 7: Final Confirmation]`
|
||||
|
||||
**Objective:** Confirm all finalization steps complete.
|
||||
|
||||
```markdown
|
||||
## Finalization Complete
|
||||
|
||||
### Summary
|
||||
- **Feature:** [Name]
|
||||
- **Status:** Complete
|
||||
- **Completion Rate:** [X]% ([Y]/[Z] tasks)
|
||||
- **Archived To:** `specs--completed/<feature-name>/`
|
||||
|
||||
<!-- METRICS_JSON {"step": "finalize", "completion_rate_at_audit": 0.95, "tasks_completed": 19, "tasks_total": 20, "verification_failures_found": 1, "documentation_updates_needed": 2} -->
|
||||
|
||||
Replace METRICS_JSON values with actuals. `completion_rate_at_audit` = Y/Z as decimal (e.g., 19/20 = 0.95).
|
||||
|
||||
### Finalization Steps Completed
|
||||
- [x] Step 1: Task Completion Audit
|
||||
- [x] Step 2: Implementation Verification
|
||||
- [x] Step 3: Implementation Summary
|
||||
- [x] Step 4: Documentation Review
|
||||
- [x] Step 5: User Feedback (Optional)
|
||||
- [x] Step 6: Spec Cleanup
|
||||
- [x] Step 7: Final Confirmation
|
||||
|
||||
### Files Created/Modified During Finalization
|
||||
- `specs/<feature-name>/overview.md` - Added completion summary
|
||||
- `README.md` - [if updated]
|
||||
- `CHANGELOG.md` - [if updated]
|
||||
|
||||
### Archived Files
|
||||
[List all files moved to specs--completed/<feature-name>/]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
```
|
||||
⋅
|
||||
╭───╮
|
||||
│ ★ │
|
||||
│ ◡ │ You did it! Feature complete!
|
||||
╰───╯
|
||||
```
|
||||
|
||||
> IMPLEMENTATION COMPLETED!
|
||||
> All tasks finished.
|
||||
> Specs archived to `specs--completed/<feature-name>/`.
|
||||
> Thank you for using the Plan2Code workflow!
|
||||
|
||||
### Handling Incomplete Implementations
|
||||
|
||||
| Completion | Action |
|
||||
|-----------|--------|
|
||||
| **>75%** | Finalize with notice. List incomplete items. Note remaining tasks for follow-up cycle. |
|
||||
| **<75%** | Recommend returning to implementation. List incomplete phases with task counts. Options: 1) Return via `/plan2code-3-implement` 2) Proceed with partial finalization. |
|
||||
|
||||
## Abort Handling
|
||||
|
||||
If user says "abort", "cancel", or similar:
|
||||
1. Confirm: "Abort finalization? Implementation remains but won't be validated or archived."
|
||||
2. If confirmed: Note progress, explain spec files remain in place
|
||||
3. Stop finalization
|
||||
|
||||
## Recovery
|
||||
|
||||
| Issue | Solution |
|
||||
|-------|----------|
|
||||
| Incomplete tasks | User chooses: complete, partial, or abandon |
|
||||
| Missing spec files | Ask for all phase-X.md files |
|
||||
| Doc updates rejected | Skip updates, note in summary |
|
||||
|
||||
## Learning Capture
|
||||
|
||||
At session end, if you discovered undocumented commands, dependency quirks, gotchas (>5min cost), framework workarounds, or missing `AGENTS.md` patterns → prompt user to update AGENTS.md. If yes, apply the edit directly.
|
||||
|
||||
## Session End
|
||||
|
||||
(Step 7 already delivered the completion summary — don't repeat it.)
|
||||
|
||||
Suggested commit (only if README, CHANGELOG, or other tracked docs were updated):
|
||||
```
|
||||
git commit -m "chore: finalize and archive <feature-name>" -m "<JIRA-Ticket-ID>" -m "AI Assisted"
|
||||
```
|
||||
|
||||
Returning context: Feature complete. Specs archived to `specs--completed/<feature-name>/`.
|
||||
@@ -0,0 +1,131 @@
|
||||
# plan2code-handoff
|
||||
|
||||
Turn everything useful in the current conversation into a single, self-contained
|
||||
handoff document that lets a *different* agent — a new session, a teammate's
|
||||
session, or a subagent — resume the work without re-reading this transcript.
|
||||
|
||||
The reader of this document starts with **zero context**. They can see the repo
|
||||
and can open files, but they cannot see this conversation. Write for them.
|
||||
|
||||
## The one hard rule: capture the next task, and confirm it with the user
|
||||
|
||||
Every handoff MUST end with a **Next task** that the incoming agent should start
|
||||
on. This is the single most important part of the document — a handoff with a
|
||||
vague or missing next step forces the reader to re-derive intent, which is
|
||||
exactly what this skill exists to prevent.
|
||||
|
||||
Determine it like this:
|
||||
|
||||
1. **Try to infer it** from the conversation — the open TODO, the failing test,
|
||||
the plan step you were mid-way through, the thing the user just asked for
|
||||
next. Look at what's actually unfinished, not just the last message.
|
||||
2. **Present it to the user for confirmation before writing the file.** If you
|
||||
inferred a candidate, show it and ask them to confirm or correct it. If you
|
||||
genuinely can't infer one, ask them to tell you what the next agent should do.
|
||||
Use `AskUserQuestion` (offer your inferred task as the recommended option) or
|
||||
a plain question — either is fine.
|
||||
3. **Do not write the document until the user has confirmed or supplied the next
|
||||
task.** This gate is mandatory even when your inference feels obviously
|
||||
correct. The user's answer is the source of truth; your inference is only a
|
||||
draft of it.
|
||||
|
||||
If the user passed a focus area as an argument, treat it as a strong signal for
|
||||
the next task (and shape the whole document around it), but still confirm.
|
||||
|
||||
## Where to write it
|
||||
|
||||
Save to `./handoffs/` at the repo root with a timestamped filename so it's
|
||||
discoverable but doesn't collide with earlier handoffs:
|
||||
|
||||
```
|
||||
./handoffs/<YYYY-MM-DD-HHmm>-handoff.md
|
||||
```
|
||||
|
||||
Get the timestamp from the shell rather than guessing — e.g. PowerShell
|
||||
`Get-Date -Format 'yyyy-MM-dd-HHmm'`. Create the `handoffs/` directory if it
|
||||
doesn't exist.
|
||||
|
||||
### Make sure you aren't leaking the file into version control
|
||||
|
||||
The handoff is working state for the next session, not a project artifact, so it
|
||||
should stay out of commits and PRs. Don't assume it will — this skill may run in
|
||||
any repo. Before (or right after) writing, check whether the path is ignored:
|
||||
|
||||
- Is this even a git repo? `git rev-parse --is-inside-work-tree` — if it errors,
|
||||
there's nothing to ignore; skip this and just tell the user where the file is.
|
||||
- Is the file ignored? `git check-ignore handoffs/` (exit 0 = ignored). This is
|
||||
the reliable check — a repo may ignore `handoffs/` via a global or nested
|
||||
`.gitignore`, so don't rely on grepping the root `.gitignore` alone.
|
||||
|
||||
If it is **not** ignored, do not silently modify the user's `.gitignore`. Tell
|
||||
them the file would be tracked by git and offer to add a `handoffs/` line to
|
||||
`.gitignore` — let them decide. Some users may want handoffs committed so
|
||||
teammates get them; that's a legitimate choice, so present it, don't force it.
|
||||
|
||||
## What to include
|
||||
|
||||
Keep it tight and high-signal. Prefer pointers over prose: this repo already
|
||||
records a lot (plan specs, the loop's NDJSON logs, git history, diffs), so
|
||||
**reference those by path or URL instead of copying them in**. The reader can
|
||||
open a file; they can't open your memory.
|
||||
|
||||
Use this structure:
|
||||
|
||||
```markdown
|
||||
# Handoff — <short title of the work>
|
||||
<!-- written <timestamp> -->
|
||||
|
||||
## Next task
|
||||
<the confirmed next task — concrete and actionable, e.g.
|
||||
"Implement Step 3 of specs/<name>.md: wire the aggregator into cli.ts, then
|
||||
run `npm run build` in plan2code-metrics/ and fix the two failing tests.">
|
||||
|
||||
## Goal / why
|
||||
<1–3 sentences: what the user is ultimately trying to achieve, so the reader
|
||||
can make good judgment calls the instructions don't cover.>
|
||||
|
||||
## Current state
|
||||
<Where things stand right now. What's done, what's in progress, what's broken.
|
||||
Name the branch. Point at the plan/spec file(s) by path rather than restating
|
||||
them. Note anything half-applied or left uncommitted.>
|
||||
|
||||
## Key files & pointers
|
||||
<Bulleted paths the reader will need, each with a one-line "why". Include plan
|
||||
specs, the files you were editing, relevant logs (e.g. .plan2code-loop NDJSON),
|
||||
and any PR/issue URLs.>
|
||||
|
||||
## Gotchas & decisions
|
||||
<Non-obvious things learned this session: a constraint (e.g. the 11k-char limit
|
||||
on src/plan2code-*.md), a decision made and why, a dead end already ruled out,
|
||||
a command that must be run a specific way. Save the reader from re-discovering
|
||||
these the hard way.>
|
||||
|
||||
## Suggested skills
|
||||
<Which skills the next agent should use, and when — e.g. sync-repo for syncing,
|
||||
a plan2code workflow phase, /code-review before finishing. Skip if none apply.>
|
||||
|
||||
## Verification
|
||||
<How the reader confirms their work: exact test/build commands, what "done"
|
||||
looks like.>
|
||||
```
|
||||
|
||||
Adapt the sections to the work — drop any that would be empty rather than
|
||||
padding them. **Next task** is the only section that is never optional.
|
||||
|
||||
## Strip sensitive data
|
||||
|
||||
Before writing, remove credentials, API tokens, passwords, and personal
|
||||
identifiers. If a secret is load-bearing for the next step, reference *where* it
|
||||
lives (env var name, secret manager entry) rather than its value.
|
||||
|
||||
## After writing
|
||||
|
||||
Tell the user the path you wrote to and give a one-line summary of the confirmed
|
||||
next task, so they know what the incoming agent will start on. Mention that a
|
||||
fresh session can be pointed at the file to resume the work.
|
||||
|
||||
If your ignore check above found the file is **not** gitignored (or the repo has
|
||||
no `.gitignore`, or it isn't a git repo at all), say so plainly here — e.g. "note:
|
||||
`handoffs/` isn't gitignored in this repo, so this file will show up in `git
|
||||
status` and could be committed" — and offer to add the ignore line. Never leave
|
||||
the user unaware that the handoff might ride along into a commit.
|
||||
@@ -0,0 +1,299 @@
|
||||
# 🛞 UPDATE AGENTS MODE
|
||||
|
||||
Start all UPDATE AGENTS MODE responses with '🛞'
|
||||
|
||||
```
|
||||
⋅
|
||||
╭───╮
|
||||
│ ● │
|
||||
│ ◡ │ Time to level up AGENTS.md!
|
||||
╰───╯
|
||||
```
|
||||
|
||||
Interactive Q&A flow to update an existing `AGENTS.md` with new learnings and project knowledge.
|
||||
|
||||
---
|
||||
|
||||
## Awareness Context
|
||||
|
||||
Before making any updates, orient yourself to current project state: read `AGENTS.md` and `README.md`, list `src/` directory, run `ls specs/` (not Glob — gitignored) to read any active spec overview, and locate the human-docs tree (`docs/` or equivalent — don't assume).
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Pre-flight Check
|
||||
|
||||
Check if `AGENTS.md` exists in project root.
|
||||
|
||||
**If missing:** "No AGENTS.md found. Create one from scratch? I can analyze the codebase and generate an initial file." Stop and wait. If yes, use `plan2code-init.md` workflow.
|
||||
|
||||
**If exists:** Read and summarize:
|
||||
- Main sections (bullets)
|
||||
- Current line count
|
||||
|
||||
Check for `.agents-docs/` directory:
|
||||
|
||||
**If `.agents-docs/` exists:**
|
||||
```
|
||||
Structure: Progressive discovery (index + N section files)
|
||||
Section files:
|
||||
- .agents-docs/AGENTS-architecture.md
|
||||
- .agents-docs/AGENTS-development.md
|
||||
[etc.]
|
||||
```
|
||||
|
||||
**If `.agents-docs/` does not exist:** Note: `Structure: Single-file (no .agents-docs/ directory)`
|
||||
|
||||
**IMPORTANT — you MUST offer restructuring.** Stop and ask:
|
||||
> "Your AGENTS.md uses a single-file format. Want to restructure for progressive discovery? This splits detailed sections into `.agents-docs/` files and converts AGENTS.md to a lightweight index with summaries and links."
|
||||
|
||||
Wait for user response before continuing. If user accepts, restructure existing content: create `.agents-docs/` directory with section files, convert AGENTS.md to index with summaries and links. Always-inline sections (Project Overview, Git Commit Messages, How to Use This File) stay in AGENTS.md. If user declines, continue with single-file editing.
|
||||
|
||||
Proceed to Step 2.
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Context Detection
|
||||
|
||||
Check for recent conversation context.
|
||||
|
||||
**If recent work exists:** "I noticed we just worked on [description]. Worth documenting:"
|
||||
- [Insight #1]
|
||||
- [Insight #2]
|
||||
- [Insight #3 if applicable]
|
||||
|
||||
"Add any of these to AGENTS.md?"
|
||||
|
||||
**If no context:** Skip to Step 3.
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Update Menu
|
||||
|
||||
Present the user with update options:
|
||||
|
||||
> ```
|
||||
> ⋅
|
||||
> ╭───╮
|
||||
> │ ● │
|
||||
> │ ~ │ What should we update?
|
||||
> ╰───╯
|
||||
> ```
|
||||
>
|
||||
> "What would you like to add or update in AGENTS.md?"
|
||||
>
|
||||
> **Options:**
|
||||
> - **1. Commands** - Build, test, run, lint, or other CLI commands
|
||||
> - **2. Architecture** - How components interact, data flow, key patterns
|
||||
> - **3. Gotchas/Pitfalls** - Traps to avoid, non-obvious behaviors
|
||||
> - **4. Testing** - Test patterns, how to run specific tests, fixtures
|
||||
> - **5. Environment/Config** - Setup quirks, env variables, configuration
|
||||
> - **6. General Rules** - Coding conventions, style rules, project-specific practices
|
||||
> - **7. Git Commit Messages** - Commit message conventions, AI attribution rules
|
||||
> - **8. Something else** - Tell me what you'd like to add
|
||||
> - **9. Sync & Maintain** - Audit & sync all doc surfaces — AGENTS.md, `.agents-docs/`, `specs/`, README, human docs: fix stale/wrong/missing, cut redundancy
|
||||
>
|
||||
> You can also ask me to:
|
||||
> - **Review for corrections** - Check if any existing content is outdated or wrong
|
||||
> - **Prune/consolidate** - Trim redundant or verbose sections
|
||||
>
|
||||
> "Which would you like to do? (You can pick multiple, e.g., '1 and 3')"
|
||||
|
||||
---
|
||||
|
||||
## Step 4: Gather Details
|
||||
|
||||
| Category | Questions |
|
||||
|----------|-----------|
|
||||
| Commands | Purpose? Flags? Prerequisites? |
|
||||
| Architecture | Components? Interactions? Pattern? |
|
||||
| Gotchas | What was unexpected? Workaround? |
|
||||
| Testing | Commands? Fixtures? Mocking? |
|
||||
| Environment | Local/CI/deploy? Env vars/files? |
|
||||
| Rules | Project-wide or specific? Why? |
|
||||
| Git Commit Messages | Format? Attribution? Conventions? |
|
||||
| Other | "Tell me what to add." |
|
||||
| Sync & Maintain | Scope: all surfaces or specific? Then follow the Sync & Maintain section. |
|
||||
| Review | Per section: "Still accurate?" |
|
||||
| Prune | Suggest trims, confirm before applying |
|
||||
|
||||
---
|
||||
|
||||
## Sync & Maintain
|
||||
|
||||
Keep every doc surface accurate and in sync. **Deep-audit surfaces this session touched; staleness-scan the rest.** Verify against actual code — never assume or fabricate.
|
||||
|
||||
| Surface | Tier | Voice |
|
||||
|---------|------|-------|
|
||||
| `AGENTS.md` + ALL applicable `.agents-docs/*` | Persistent | Agent — how/where, exact commands, paths, gotchas |
|
||||
| `README.md` + human docs | Persistent | Human — what/why, scannable |
|
||||
| Active `specs/<feature>/` | Transient | Session knowledge — status, decisions, next steps. No bloat |
|
||||
|
||||
**Adaptive:** `AGENTS.md`/`.agents-docs/`/`specs/` are plan2code conventions — expected, but verify. `README.md` is standard. Human-docs tree varies (`docs/` or other) — detect, don't assume.
|
||||
|
||||
**Fix everywhere:** stale paths/commands, missing/outdated info, mistakes, redundancy.
|
||||
|
||||
**Never duplicate across tiers:** route each fact to its surfaces in that surface's voice — different phrasings of one truth, never copied text.
|
||||
|
||||
---
|
||||
|
||||
## Step 5: Confirm & Apply
|
||||
|
||||
Before changes, route edits to the correct file when `.agents-docs/` exists:
|
||||
- Always-inline sections (Project Overview, Git Commit Messages, How to Use This File) → edit AGENTS.md directly
|
||||
- All other sections → edit the corresponding `.agents-docs/AGENTS-<section-name>.md` file
|
||||
- Sync & Maintain: human-voice facts → README/human docs; session knowledge → active spec's `overview.md`
|
||||
|
||||
Preview format:
|
||||
> **File:** `.agents-docs/AGENTS-architecture.md` (or `AGENTS.md` for inline sections)
|
||||
> **Section:** [name]
|
||||
> **Change:** [description]
|
||||
> ```
|
||||
> [Preview text]
|
||||
> ```
|
||||
> "Does this look right? (yes/no/adjust)"
|
||||
|
||||
- "adjust" -> ask what to change, repeat
|
||||
- "yes" -> apply edit, insert in appropriate section (create if needed), preserve structure
|
||||
|
||||
### Section File Lifecycle (when `.agents-docs/` exists)
|
||||
|
||||
- **New section (>~10 lines):** Create `.agents-docs/AGENTS-<section-name>.md` with breadcrumb header, add summary + link in AGENTS.md
|
||||
- **New section (<~10 lines):** Keep inline in AGENTS.md
|
||||
- **Delete section:** Remove the `.agents-docs/` file and its summary + link from AGENTS.md
|
||||
- **Orphan cleanup:** After all edits, check for `.agents-docs/` files with no corresponding AGENTS.md section — offer to remove them
|
||||
|
||||
---
|
||||
|
||||
## Step 6: Summary & Next
|
||||
|
||||
After applying:
|
||||
> "Done! Changed:"
|
||||
> - [Summary]
|
||||
|
||||
If `.agents-docs/` exists:
|
||||
> Structure: AGENTS.md (index) + N section files in .agents-docs/
|
||||
> Index line count: X/500
|
||||
|
||||
Otherwise:
|
||||
> Line count: X/500
|
||||
|
||||
> "Add anything else?"
|
||||
|
||||
If yes, return to Step 3. If done, proceed to Step 7.
|
||||
|
||||
---
|
||||
|
||||
## Step 7: AI Agent File Sync
|
||||
|
||||
Check for other AI agent config files and offer to replace with AGENTS.md references.
|
||||
|
||||
### Files to Detect
|
||||
|
||||
| File | Reference Path |
|
||||
|------|----------------|
|
||||
| `CLAUDE.md` (root) | `./AGENTS.md` |
|
||||
| `GEMINI.md` (root) | `./AGENTS.md` |
|
||||
| `.cursorrules` (root) | `./AGENTS.md` |
|
||||
| `.github/copilot-instructions.md` | `../AGENTS.md` |
|
||||
| `.cursor/rules/*.md` | `../../AGENTS.md` |
|
||||
| `.windsurf/rules/*.md` | `../../AGENTS.md` |
|
||||
|
||||
**No files found:** Skip silently, end workflow.
|
||||
|
||||
### If Files Found
|
||||
|
||||
```
|
||||
o o
|
||||
\ /
|
||||
+---+
|
||||
| o |
|
||||
| ~ | Found other AI agent configs!
|
||||
+---+
|
||||
```
|
||||
|
||||
> Found AI config files that could reference AGENTS.md:
|
||||
>
|
||||
> | File | Size |
|
||||
> |------|------|
|
||||
> | `CLAUDE.md` | 45 lines |
|
||||
>
|
||||
> Replace with AGENTS.md references?
|
||||
> - **Yes** - Update all
|
||||
> - **Select** - Choose specific (numbered list)
|
||||
> - **No** - Keep as-is
|
||||
|
||||
**Warning** for files >10 lines: "[file] has custom content that will be replaced."
|
||||
|
||||
### CLAUDE.md Template
|
||||
|
||||
CLAUDE.md gets a special template because Claude Code auto-loads it — the `CRITICAL — MANDATORY FIRST STEP` directive ensures AGENTS.md is always read:
|
||||
|
||||
```markdown
|
||||
# CLAUDE.md
|
||||
|
||||
**CRITICAL — MANDATORY FIRST STEP: You MUST read [AGENTS.md](./AGENTS.md) before responding to ANY user message, including simple questions. Do NOT skip this step regardless of how trivial the request appears. No exceptions.**
|
||||
|
||||
See AGENTS.md for full project documentation: commands, architecture, environment, testing, deployment, and .agents-docs/ section details.
|
||||
|
||||
This file exists for Claude Code auto-loading. All AI coding agents should reference AGENTS.md.
|
||||
```
|
||||
|
||||
### Reference Template (all other files)
|
||||
|
||||
Use title and path from the detection table:
|
||||
|
||||
```markdown
|
||||
# [Title]
|
||||
|
||||
See [AGENTS.md]([Path]) for full project documentation: commands, architecture, environment, testing, deployment, and .agents-docs/ section details.
|
||||
```
|
||||
|
||||
**For directory configs** (`.cursor/rules/`, `.windsurf/rules/`): Delete existing `.md` files, create single `reference.md`.
|
||||
|
||||
---
|
||||
|
||||
## Update Rules
|
||||
|
||||
1. **Surgical edits** - Don't rewrite unchanged sections
|
||||
2. **Preserve style** - Match existing formatting/tone
|
||||
3. **Actionable only** - Every entry helps agents do something
|
||||
4. **Under 500 lines** - Warn if approaching limit
|
||||
5. **No generic advice** - Must be project-specific
|
||||
6. **No duplication** - Check for similar content first
|
||||
7. **Group logically** - Place near related content
|
||||
8. **Be specific** - Include exact commands, paths, names
|
||||
9. **Route edits to correct file** - Inline sections edit AGENTS.md directly, detailed sections edit the corresponding .agents-docs/ file
|
||||
|
||||
---
|
||||
|
||||
## Example Session
|
||||
|
||||
```
|
||||
Agent: Found AGENTS.md (127 lines, single-file). Restructure for
|
||||
progressive discovery?
|
||||
User: No, just update it.
|
||||
Agent: Recent work on auth flow. Worth documenting:
|
||||
- Auth tests require TEST_SECRET env var
|
||||
Add it?
|
||||
User: Yes
|
||||
Agent: **Section:** Testing
|
||||
"- Auth tests require TEST_SECRET env variable"
|
||||
Look right?
|
||||
User: Yes
|
||||
Agent: Done! Line count: 128/500. Add anything else?
|
||||
User: No
|
||||
Agent: Found CLAUDE.md (23 lines). Replace with AGENTS.md reference?
|
||||
User: Yes
|
||||
Agent: Updated CLAUDE.md. AGENTS.md is your single source of truth now!
|
||||
```
|
||||
|
||||
## Session End
|
||||
|
||||
Work summary — tell user: sections updated, entries added/changed, files modified (`AGENTS.md` or `.agents-docs/` files).
|
||||
|
||||
Suggested commit:
|
||||
```
|
||||
git commit -m "docs: update AGENTS.md with new learnings" -m "<JIRA-Ticket-ID>" -m "AI Assisted"
|
||||
```
|
||||
|
||||
Returning context: Run `/plan2code-init-update` again to make additional updates.
|
||||
@@ -0,0 +1,141 @@
|
||||
# 💡 CREATE AGENTS MODE
|
||||
|
||||
Start all CREATE AGENTS MODE responses with '💡'
|
||||
|
||||
```
|
||||
⋅
|
||||
╭───╮
|
||||
│ ● │
|
||||
│ ◡ │ Let me explore your codebase!
|
||||
╰───╯
|
||||
```
|
||||
|
||||
Analyze this codebase and create `AGENTS.md` to guide future AI coding agents (Claude Code, Codex, Gemini CLI, Devin, Zed, etc.).
|
||||
|
||||
## Content
|
||||
|
||||
1. **Commands**: Build, lint, test, run single test, and other common development tasks
|
||||
2. **Architecture**: High-level "big picture" structure requiring multi-file context to understand
|
||||
3. **How to Use This File**: A short paragraph explaining that sections below contain brief summaries and agents should follow the markdown links to `.agents-docs/` for full details — only read what's relevant to the current task.
|
||||
|
||||
## Rules
|
||||
|
||||
- If `AGENTS.md` exists: suggest improvements instead of creating new
|
||||
- If only `CLAUDE.md` exists: migrate its content to the new `AGENTS.md`
|
||||
- Include relevant content from: `README.md`, `PROJECT.md`, `.cursorrules`, `.cursor/rules/`, `GEMINI.md`, `.github/copilot-instructions.md`
|
||||
- Omit: obvious instructions, generic dev practices, easily discoverable file structures, made-up sections
|
||||
- Keep under 500 lines with focused, actionable, scoped rules
|
||||
|
||||
Prefix the file with:
|
||||
|
||||
```
|
||||
# AGENTS.md
|
||||
|
||||
This file provides guidance to AI coding agents like Claude Code (claude.ai/code), Cursor AI, Codex, Gemini CLI, GitHub Copilot, Devin, Zed, and other AI coding assistants when working with code in this repository.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Progressive Discovery
|
||||
|
||||
Generate AGENTS.md as an **index file** — each section gets a 2-3 line summary with a markdown link to a detail file. Full content goes in `.agents-docs/` directory files.
|
||||
|
||||
- **Index format** — each section in AGENTS.md: heading, brief summary, then `Details: [Section Name](./.agents-docs/AGENTS-<section-name>.md)`
|
||||
- **Detail files** — named `AGENTS-<section-name>.md` using kebab-case from the section header (e.g., "Development Commands" → `AGENTS-development-commands.md`)
|
||||
- **Detail file header** — each file starts with:
|
||||
```
|
||||
# <Section Name>
|
||||
> Part of [AGENTS.md](../AGENTS.md) — project guidance for AI coding agents.
|
||||
```
|
||||
|
||||
### Always Inline
|
||||
|
||||
These sections must remain fully inline in AGENTS.md (never split to separate files):
|
||||
- Project Overview
|
||||
- How to Use This File
|
||||
|
||||
### Directory Setup
|
||||
|
||||
- Create `.agents-docs/` directory in the project root
|
||||
- Each file: `AGENTS-<section-name>.md` where `<section-name>` is kebab-case of the section header
|
||||
- Each file starts with `# <Section Name>` followed by breadcrumb: `> Part of [AGENTS.md](../AGENTS.md) — project guidance for AI coding agents.`
|
||||
|
||||
### Grouping Heuristics
|
||||
|
||||
- Combine related subsections into a single detail file (e.g., "Architecture" with its subsections → one file)
|
||||
- Sections under ~10 lines of content should stay inline in AGENTS.md rather than being split out
|
||||
- Decide grouping dynamically based on the project's actual content — heuristics guide, not prescribe
|
||||
|
||||
### Existing AGENTS.md Without `.agents-docs/`
|
||||
|
||||
If AGENTS.md exists but `.agents-docs/` does not, offer to restructure: "Your AGENTS.md uses a single-file format. Want to restructure it for progressive discovery? This splits detailed sections into `.agents-docs/` files and converts AGENTS.md to a lightweight index." Only restructure if the user confirms — never auto-restructure.
|
||||
|
||||
---
|
||||
|
||||
## AI Agent File Sync
|
||||
|
||||
After creating `AGENTS.md`, check for these files and offer to replace with references:
|
||||
|
||||
| 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 `.cursor/rules/` and `.windsurf/rules/`: delete existing `.md` files, create single `reference.md`.
|
||||
|
||||
### Confirmation Prompt
|
||||
|
||||
If files found, show:
|
||||
|
||||
```
|
||||
⋅
|
||||
╭───╮
|
||||
│ ● │
|
||||
│ ~ │ 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 confirmed files.
|
||||
|
||||
### CLAUDE.md Template
|
||||
|
||||
CLAUDE.md gets a special template because Claude Code auto-loads it — the `CRITICAL — MANDATORY FIRST STEP` directive ensures AGENTS.md is always read:
|
||||
|
||||
```markdown
|
||||
# CLAUDE.md
|
||||
|
||||
**CRITICAL — MANDATORY FIRST STEP: You MUST read [AGENTS.md](./AGENTS.md) before responding to ANY user message, including simple questions. Do NOT skip this step regardless of how trivial the request appears. No exceptions.**
|
||||
|
||||
See AGENTS.md for complete project documentation including:
|
||||
- Development commands and setup
|
||||
- Architecture overview
|
||||
- Environment variables
|
||||
- Testing patterns
|
||||
- Deployment guides
|
||||
- Section details in .agents-docs/
|
||||
|
||||
This file exists for Claude Code auto-loading. All AI coding agents should reference AGENTS.md.
|
||||
```
|
||||
|
||||
### Reference Template (all other files)
|
||||
|
||||
```markdown
|
||||
# [Title]
|
||||
|
||||
See [AGENTS.md]([Path]) for complete project documentation including:
|
||||
- Development commands and setup
|
||||
- Architecture overview
|
||||
- Environment variables
|
||||
- Testing patterns
|
||||
- Deployment guides
|
||||
- Section details in .agents-docs/
|
||||
```
|
||||
@@ -17,14 +17,14 @@ Check for `./AGENTS.md` first:
|
||||
> ```
|
||||
> ⋅
|
||||
> ╭───╮
|
||||
> │ ● │
|
||||
> │ ● │ ?
|
||||
> │ ~ │ Hmm, I don't see an AGENTS.md...
|
||||
> ╰───╯
|
||||
> ```
|
||||
>
|
||||
> "No `AGENTS.md` found. This file provides essential project context.
|
||||
>
|
||||
> **Run:** `plan2code---init`
|
||||
> **Run:** `plan2code-init`
|
||||
>
|
||||
> Let me know when ready to continue."
|
||||
|
||||
@@ -114,11 +114,11 @@ After achieving clarity, assess scope:
|
||||
[Files examined, patterns noted]
|
||||
|
||||
---
|
||||
**Next:** New conversation with `/plan2code-1--plan`, attach this file.
|
||||
**Next:** New conversation with `/plan2code-1-plan`, attach this file.
|
||||
Resume at Phase 2 (System Context).
|
||||
```
|
||||
|
||||
Then tell user: "Created `specs/<feature-name>/PLAN-DRAFT-<date>.md`. Start new conversation with `/plan2code-1--plan` to continue."
|
||||
Then tell user: "Created `specs/<feature-name>/PLAN-DRAFT-<date>.md`. Start new conversation with `/plan2code-1-plan` to continue."
|
||||
|
||||
---
|
||||
|
||||
@@ -142,14 +142,14 @@ Then tell user: "Created `specs/<feature-name>/PLAN-DRAFT-<date>.md`. Start new
|
||||
---
|
||||
```
|
||||
⋅
|
||||
o o
|
||||
╲ ╱
|
||||
╭───╮
|
||||
│ ★ │
|
||||
│ ◡ │ Plan ready! What do you think?
|
||||
├───┤
|
||||
│ · │
|
||||
╰───╯
|
||||
```
|
||||
|
||||
Ready to implement? (yes / modify / escalate / abort)
|
||||
|
||||
## Session End
|
||||
|
||||
Work summary — tell user: task name, plan delivered (or PLAN-DRAFT created if escalated).
|
||||
@@ -0,0 +1,191 @@
|
||||
# Dimension Checklists
|
||||
> Part of plan2code-review — loaded during Step 3 (Analyze).
|
||||
|
||||
Non-obvious check items, anti-patterns, and "don't flag" guidance for each review dimension. Focus on what LLMs commonly miss.
|
||||
|
||||
---
|
||||
|
||||
## 1. Correctness
|
||||
|
||||
1. Trace variable mutations through async paths — can a value change between check and use?
|
||||
2. Check type coercion at boundaries — string-to-number, truthy/falsy assumptions on `0`/`""`/`false`
|
||||
3. Check that map/filter/reduce callbacks handle all element shapes (null members, missing keys)
|
||||
4. Verify copy operations produce deep copies when mutation independence is required
|
||||
5. Check array bounds — does code handle empty arrays and out-of-range indices?
|
||||
6. Verify regex patterns reject edge cases (empty string, special chars, Unicode)
|
||||
7. Check for race conditions — shared mutable state across async reads/writes
|
||||
8. Verify error handling paths return/throw correctly — no silent swallowing
|
||||
|
||||
**Anti-patterns:** `catch(e) {}` silent suppression · `if (value)` when `0`/`""`/`false` are valid · `indexOf > 0` instead of `!== -1` · Mutating arguments instead of returning new values
|
||||
|
||||
**Don't flag:** Intentional `==` for null coalescing (`if (x == null)`) · Missing switch default with TypeScript `never` exhaustive check · Optional chaining `?.` returning undefined
|
||||
|
||||
---
|
||||
|
||||
## 2. Completeness
|
||||
|
||||
1. Search for TODO, FIXME, HACK, XXX, TEMP — each must be intentional or tracked
|
||||
2. Check cleanup/teardown exists for every setup/initialization
|
||||
3. Verify rollback/undo logic for multi-step operations that can partially fail
|
||||
4. Check event listeners and subscriptions have corresponding unsubscribe/cleanup
|
||||
5. Verify all file/resource handles closed in both success and error paths
|
||||
6. Check all enum/union type values have handling — no missing cases
|
||||
7. Verify pagination — does code fetch all pages or just the first?
|
||||
8. Check retry logic has backoff and maximum retry count
|
||||
|
||||
**Anti-patterns:** Happy-path-only functions that return undefined on error · Event listeners without cleanup · Missing else in critical chains where the "impossible" case can occur
|
||||
|
||||
**Don't flag:** TODOs with linked ticket numbers · Features explicitly "not in scope" · Bare `throw` in catch blocks (intentional re-throw)
|
||||
|
||||
---
|
||||
|
||||
## 3. Security
|
||||
|
||||
1. Check for injection — string concatenation in queries instead of parameterized
|
||||
2. Search for hardcoded secrets, API keys, tokens in source files and configs
|
||||
3. Verify sensitive data (passwords, tokens, PII) is not logged, even at debug level
|
||||
4. Check file path operations prevent traversal — no unsanitized `../` from user input
|
||||
5. Check for mass assignment — `Object.assign`/spread from user input without allow-list
|
||||
6. Verify error responses don't leak stack traces, internal paths, or system info
|
||||
7. Verify session tokens use cryptographic randomness, not `Math.random()`
|
||||
8. Check dependency versions against known CVEs
|
||||
|
||||
**Anti-patterns:** Dynamic code evaluation with user-influenced input · Plaintext password storage · JWT `none` algorithm accepted
|
||||
|
||||
**Don't flag:** Hardcoded non-sensitive values (page sizes, route paths) · Internal tools documented as trusted-environment-only · Test fixture credentials in `.test.`/`.fixture.` files
|
||||
|
||||
---
|
||||
|
||||
## 4. Performance
|
||||
|
||||
1. Check for N+1 queries — loops issuing a query per iteration instead of batching
|
||||
2. Check for synchronous blocking in async contexts (fs.readFileSync in server request handlers)
|
||||
3. Check for memory leaks — growing arrays/maps without bounds, unclosed streams
|
||||
4. Check for quadratic complexity — nested iterations over the same collection
|
||||
5. Verify bulk operations used where available (bulk insert vs individual)
|
||||
6. Check regex for catastrophic backtracking potential (nested quantifiers)
|
||||
7. Verify connection pools for database and HTTP clients, not per-request connections
|
||||
8. Check event handlers debounced/throttled for high-frequency events
|
||||
|
||||
**Anti-patterns:** `await` in `for` loops instead of `Promise.all` · Loading entire tables to filter in app code · New regex instances inside loops
|
||||
|
||||
**Don't flag:** `readFileSync` at startup/module load · Loading small files (<100KB) once · Missing caching in one-shot scripts/CLI tools
|
||||
|
||||
---
|
||||
|
||||
## 5. Standards
|
||||
|
||||
1. Check function length — functions over 50 lines warrant scrutiny
|
||||
2. Verify DRY applied judiciously — shared logic extracted, not over-abstracted
|
||||
3. Check magic numbers/strings are extracted to named constants
|
||||
4. Verify comments explain "why" not "what"
|
||||
5. Check file organization matches project conventions
|
||||
6. Verify consistent async/await vs callbacks vs promises within a module
|
||||
7. Check naming conventions consistent (camelCase, snake_case, PascalCase)
|
||||
8. Verify linting rules not disabled without justification
|
||||
|
||||
**Anti-patterns:** Mixed naming conventions in one module · God objects/functions · Deep nesting (>3 levels) instead of early returns
|
||||
|
||||
**Don't flag:** Framework-imposed patterns · Single-use helpers for readability · Comments on complex algorithms/regex
|
||||
|
||||
---
|
||||
|
||||
## 6. Tech Debt
|
||||
|
||||
1. Check for deprecated API usage — verify against current library versions
|
||||
2. Identify commented-out code blocks — should be removed or tracked
|
||||
3. Check for copy-paste blocks that could be shared utilities
|
||||
4. Look for dead feature flags — always true/false with no toggle
|
||||
5. Check for orphaned files — modules with no imports from the codebase
|
||||
6. Verify error messages reference current code, not stale names
|
||||
7. Check for inconsistent abstraction levels — mixing orchestration with low-level ops
|
||||
8. Look for workarounds with "temporary" comments that persisted
|
||||
|
||||
**Anti-patterns:** `TODO: remove after migration` with no date/ticket · Wrapper functions that just forward args · Multiple implementations of the same utility
|
||||
|
||||
**Don't flag:** Intentional per-platform duplication · Verbose code prioritizing clarity · Deprecated APIs with tracked migration tickets
|
||||
|
||||
---
|
||||
|
||||
## 7. Test Quality
|
||||
|
||||
1. Check assertions are meaningful — not just `toBeTruthy()` on objects
|
||||
2. Verify tests are isolated — no shared mutable state between cases
|
||||
3. Check error case tests actually trigger the error path, not just catch any error
|
||||
4. Verify async tests properly await results — no fire-and-forget assertions
|
||||
5. Check for tests that pass for wrong reason — wrong assertion target, always-true conditions
|
||||
6. Verify mock/stub scope is minimal — only mock what's necessary
|
||||
7. Check test descriptions describe behavior, not implementation
|
||||
8. Verify critical paths have coverage — happy path, error path, edge cases
|
||||
|
||||
**Anti-patterns:** `expect(fn).not.toThrow()` without checking return value · Mocking the module under test · Assertions in callbacks that may never execute
|
||||
|
||||
**Don't flag:** Shared test utility files · Missing tests for generated/scaffolded code · Integration tests using real databases when project prefers it
|
||||
|
||||
---
|
||||
|
||||
## 8. Maintainability
|
||||
|
||||
1. Check cyclomatic complexity — functions with >10 branch paths are hard to maintain
|
||||
2. Verify dependencies between modules are explicit, not implicit through globals
|
||||
3. Check for tight coupling — can this module be tested independently?
|
||||
4. Verify data transformations are traceable — can you follow a value from input to output?
|
||||
5. Check file length — files over 500 lines warrant scrutiny
|
||||
6. Verify similar operations use consistent approaches throughout
|
||||
7. Check boolean parameters are replaced with enums or option objects for clarity
|
||||
8. Verify function/variable names describe purpose without needing comments
|
||||
|
||||
**Anti-patterns:** Functions requiring implementation knowledge to call · Circular dependencies · God files · Stringly-typed APIs
|
||||
|
||||
**Don't flag:** Long cohesive files (comprehensive test suites) · Inherently complex domain functions · Coupling between genuinely related modules
|
||||
|
||||
---
|
||||
|
||||
## 9. Spec Compliance
|
||||
|
||||
1. Verify every acceptance criterion has corresponding implementation
|
||||
2. Check file paths, names, directory structures match spec exactly
|
||||
3. Verify implementation doesn't add undocumented behavior beyond spec
|
||||
4. Check all spec-defined edge cases have explicit handling
|
||||
5. Verify task completion state in phase files matches actual implementation
|
||||
6. Check integration points match spec contracts
|
||||
7. Verify function signatures match spec definitions
|
||||
8. Check data formats match spec (JSON schema, file formats)
|
||||
|
||||
**Anti-patterns:** Implementing a "better" approach without raising the divergence · Assuming spec intent on ambiguous points · Marking tasks complete when implementation differs
|
||||
|
||||
**Don't flag:** Minor naming variations preserving intent · Defensive coding beyond spec · Details spec leaves to developer judgment
|
||||
|
||||
---
|
||||
|
||||
## 10. UX/DX
|
||||
|
||||
1. Verify error messages are actionable — tell user what to do, not just what went wrong
|
||||
2. Check empty states have clear messaging — not blank pages or silent failures
|
||||
3. Verify configuration has sensible defaults — zero-config produces working setup
|
||||
4. Check breaking changes are communicated — deprecation warnings, migration guides
|
||||
5. Verify CLI tools have `--help`, consistent flags, meaningful exit codes
|
||||
6. Check bulk operations provide progress feedback and handle partial failures
|
||||
7. Verify documentation examples are runnable, not pseudo-code
|
||||
8. Check loading states exist for operations over 1 second
|
||||
|
||||
**Anti-patterns:** Stack traces shown to end users · Required config with no example file · 200 OK with error in body · Silent failures
|
||||
|
||||
**Don't flag:** Verbose debug output · Internal tooling with minimal polish · CLI tools requiring initial setup
|
||||
|
||||
---
|
||||
|
||||
## 11. Improvements
|
||||
|
||||
1. Check for manual implementations of standard library functionality
|
||||
2. Identify error-prone patterns replaceable with safer abstractions
|
||||
3. Look for synchronous operations that could be parallelized
|
||||
4. Identify complex conditionals clearer as lookup tables or strategy patterns
|
||||
5. Check for hardcoded limits that should be configurable
|
||||
6. Look for opportunities to improve test coverage on critical paths
|
||||
7. Identify documentation gaps — undocumented public APIs
|
||||
8. Check for opportunities to use newer language features improving clarity
|
||||
|
||||
**Anti-patterns:** Rewrites for aesthetic reasons · Patterns from different ecosystems · Optimizations without evidence of problems · Premature DRY abstractions
|
||||
|
||||
**Don't flag:** Correct code written differently than you'd write it · Performance appropriate for actual load · Features tracked in backlog
|
||||
@@ -0,0 +1,105 @@
|
||||
# False-Positive Catalog
|
||||
> Part of plan2code-review — loaded before presenting findings in Step 3 (Analyze).
|
||||
|
||||
Check every finding against this catalog before presenting it. These patterns represent common categories of false findings that waste reviewer and developer time. Each entry includes the bias, a concrete example, why it's wrong, and how to verify before flagging.
|
||||
|
||||
---
|
||||
|
||||
## 1. Optimization Bias
|
||||
|
||||
**Pattern:** Recommending removal or reduction of something to optimize a metric (context size, file count, memory, line count) when the thing being removed is necessary for correctness.
|
||||
|
||||
**Example:** Reviewer recommends removing `jira-api-catalog.md` and `jira-field-schema.md` from agent read loading to "reduce context overhead." The reviewer sees large files being loaded and assumes smaller context = better performance.
|
||||
|
||||
**Why it's wrong:** Agents consume these files at runtime to know which API endpoints exist and what fields are available. Without them, agents construct invalid API calls. The "overhead" is actually essential working knowledge. Optimizing for context size sacrificed correctness.
|
||||
|
||||
**How to check before flagging:**
|
||||
1. Identify what metric you're optimizing (file count, context size, line count, memory).
|
||||
2. Ask: "What happens if this is removed?" Trace the downstream impact.
|
||||
3. Search for all consumers of the thing you want to remove — not just direct imports, but Read directives, config references, and runtime loading.
|
||||
4. If any consumer depends on it for correct execution, the finding is invalid.
|
||||
|
||||
---
|
||||
|
||||
## 2. Simplification Bias
|
||||
|
||||
**Pattern:** Recommending consolidation or simplification of something that is intentionally structured for separation of concerns, different consumers, or independent evolution.
|
||||
|
||||
**Example:** Reviewer flags that `config/platforms/windsurf.json` and `config/platforms/cursor.json` have "similar structure" and recommends consolidating into `flat-file-platforms.json` to "reduce duplication."
|
||||
|
||||
**Why it's wrong:** The per-platform files exist deliberately. Each platform has distinct frontmatter requirements, path conventions, and feature flags. They share a common schema but contain different values. Consolidation would require conditionals everywhere and make per-platform changes harder. The separation is a design choice, not an oversight.
|
||||
|
||||
**How to check before flagging:**
|
||||
1. Ask: "Why are these separate?" Read the commit history or documentation for context.
|
||||
2. Check if the "duplicated" files serve different consumers or contexts.
|
||||
3. Verify whether the files are expected to diverge further over time.
|
||||
4. If separation serves independent evolution, different consumers, or different deployment targets, the finding is invalid.
|
||||
|
||||
---
|
||||
|
||||
## 3. Duplication False Alarm
|
||||
|
||||
**Pattern:** Flagging intentional redundancy as a DRY violation when the similar-looking code serves genuinely different purposes or domains.
|
||||
|
||||
**Example:** Reviewer flags that `handleTicketCreate()` and `handleTicketUpdate()` share 80% of their code and recommends extracting a common `handleTicketMutation()`. Both functions validate input, call the API, and format the response — but they use different validation rules, different API endpoints, different error messages, and different response transformations.
|
||||
|
||||
**Why it's wrong:** The structural similarity is coincidental. Each function handles a distinct domain operation with distinct requirements. Extracting a common function would create a complex conditional monster that's harder to maintain than two clear, self-contained handlers. DRY applies to shared knowledge, not shared structure.
|
||||
|
||||
**How to check before flagging:**
|
||||
1. Compare the "duplicate" functions at the detail level, not the structural level.
|
||||
2. Ask: "If I change one, should the other change identically?" If no, they're not duplicates.
|
||||
3. Check if the functions handle different domain concepts, even if the code shape is similar.
|
||||
4. If combining them would require conditionals or parameters to differentiate behavior, the separation is likely intentional.
|
||||
|
||||
---
|
||||
|
||||
## 4. Performance Theater
|
||||
|
||||
**Pattern:** Flagging operations as "too expensive" when the operation is not in a hot path, runs infrequently, or when the "cost" is actually necessary for correctness.
|
||||
|
||||
**Example:** Reviewer flags that the installer reads 15 platform configuration files at startup and recommends lazy loading. The installer runs once during setup, takes <200ms total, and needs all configurations to determine which platforms to install.
|
||||
|
||||
**Why it's wrong:** The installer is a one-shot CLI tool, not a server handling concurrent requests. Startup performance of a tool that runs once per install is irrelevant. The "optimization" would add complexity (lazy loading, caching, error handling for deferred loads) with zero user-visible benefit.
|
||||
|
||||
**How to check before flagging:**
|
||||
1. Determine the execution context: Is this a hot path (server request handler, tight loop) or a cold path (startup, CLI command, migration script)?
|
||||
2. Measure or estimate the actual cost. "Reads many files" is not a performance issue if total I/O is <1 second.
|
||||
3. Ask: "Would a user notice the difference?" If the answer is no, the optimization is theater.
|
||||
4. Check whether the "expensive" operation is necessary for correctness — if so, optimization means finding a faster way to do it, not skipping it.
|
||||
|
||||
---
|
||||
|
||||
## 5. Missing Context
|
||||
|
||||
**Pattern:** Flagging something as wrong, unused, or unnecessary because the reviewer didn't read all related files, packages, or documentation before forming a conclusion.
|
||||
|
||||
**Example:** Reviewer flags `export function formatJiraKey()` in `utils/jira.ts` as "unused export — remove or make private." The function is not imported in any file within the current package.
|
||||
|
||||
**Why it's wrong:** The function is consumed by a different package in the monorepo (`packages/cli/src/commands/jira.ts`). The reviewer only searched the current package directory, not the entire workspace. Cross-package consumption is common in monorepos and multi-package projects.
|
||||
|
||||
**How to check before flagging:**
|
||||
1. Search the ENTIRE codebase for references, not just the current package or directory.
|
||||
2. Check for dynamic imports, string-based requires, and config-driven module loading.
|
||||
3. For exports: search all packages in the workspace, not just the current one.
|
||||
4. For files: check build scripts, installation scripts, CI/CD configs, and documentation references.
|
||||
5. If you can't find all consumers, state that explicitly rather than assuming there are none.
|
||||
|
||||
---
|
||||
|
||||
## Detection Shortcuts
|
||||
|
||||
Quick checks to run against any finding before presenting. If a check triggers, investigate further before flagging.
|
||||
|
||||
- **Does the finding recommend removing or simplifying something?** Trace who consumes it first. Search all packages, config files, scripts, and documentation for references. "I didn't find references in this file" is not "nothing references this."
|
||||
|
||||
- **Does the finding flag duplication?** Check if the "duplicates" serve different consumers, contexts, or domain concepts. Ask: "If I change one, must the other change identically?" If no, they aren't duplicates.
|
||||
|
||||
- **Does the finding flag performance?** Verify the operation is actually in a hot path. CLI tools, installers, migration scripts, and one-shot operations don't need the same performance treatment as request handlers.
|
||||
|
||||
- **Does the finding flag unused code?** Search ALL packages, consumers, and entry points — not just the current file or directory. Check for dynamic loading, Read directives, and cross-package imports.
|
||||
|
||||
- **Does the finding assume a different architecture?** Verify against AGENTS.md, README, and actual project conventions. The project may have deliberately chosen a pattern that differs from your preferred approach.
|
||||
|
||||
- **Does the finding flag complexity?** Check if the complexity maps to genuine domain complexity. Not all complex code is accidental complexity — some problems are inherently complex.
|
||||
|
||||
- **Does the finding recommend a "modern" replacement?** Verify the replacement is compatible with the project's runtime targets, platform constraints, and dependency policies. "Newer" is not always "better" for the specific context.
|
||||
@@ -0,0 +1,72 @@
|
||||
# Verification Protocol
|
||||
> Part of plan2code-review — loaded during Step 3 (Analyze).
|
||||
|
||||
Apply this protocol to every finding before presenting it. Findings that fail verification are dropped. No exceptions.
|
||||
|
||||
## Code Finding Verification
|
||||
|
||||
1. **Re-read the source.** Open the file and read the actual line(s) cited. Do not rely on memory or earlier reads.
|
||||
2. **Read surrounding context.** At least 20 lines above and below. Many "bugs" are handled by guards, defaults, or patterns in surrounding code.
|
||||
3. **Verify the issue is real.** Trace the variable/function's actual usage. Is the edge case reachable? Does a try/catch or guard upstream already handle this?
|
||||
4. **Verify the fix doesn't break callers.** Search all call sites. Check if any caller depends on the current behavior. Verify the fix maintains the function's contract.
|
||||
5. **Check the test suite.** Do tests cover this case (making the "bug" intentional)? Would your fix break existing tests?
|
||||
|
||||
## Architectural Finding Verification
|
||||
|
||||
Architectural findings carry the highest false-positive risk.
|
||||
|
||||
1. **Identify all consumers.** Search the entire codebase — imports, requires, config files, build scripts, documentation, dynamic references (string-based requires, Read directives).
|
||||
2. **Trace a concrete use case end-to-end.** Start at the entry point, follow execution through every module, document where the component you want to change is touched. If you can't trace a complete use case, you don't understand the system well enough to recommend changes.
|
||||
3. **Simulate the change.** Walk the same use case with your change applied. At each step: can it still complete successfully? Pay attention to steps that load data, read config, or reference files.
|
||||
4. **Check indirect dependencies.** Reference data loaded by agents at runtime via Read directives. Config consumed by external tools or CI/CD. Exports consumed by other packages.
|
||||
5. **Verify the motivation.** Am I improving correctness, or satisfying an aesthetic preference? Is the complexity I'm flagging intentional?
|
||||
|
||||
## Design Finding Verification
|
||||
|
||||
1. **Check project conventions.** Read AGENTS.md, README, existing patterns. Is the "inconsistency" a deliberate choice?
|
||||
2. **Simulate across all consumers.** Does the change improve their code, or just move complexity? Would it require coordinated updates across files?
|
||||
3. **Evaluate migration cost.** Is the current design causing actual bugs, or merely suboptimal?
|
||||
4. **Check platform constraints.** Does the recommendation work across all supported platforms?
|
||||
|
||||
## Confidence Calibration
|
||||
|
||||
| Level | Definition | Action |
|
||||
|-------|-----------|--------|
|
||||
| **High** | Verified in source code AND use-case traced (for arch/design). Issue confirmed real, fix confirmed safe. | Present. |
|
||||
| **Not High** | Any doubt remains. | Investigate further or drop entirely. |
|
||||
|
||||
No Medium or Low tier. A finding is verified or it isn't.
|
||||
|
||||
## Adversarial Self-Check
|
||||
|
||||
Run against EVERY finding. If any question raises doubt, re-investigate or drop.
|
||||
|
||||
1. **Am I optimizing for the wrong metric?** Reducing file count, context size, or complexity when the structure serves correctness or platform compatibility?
|
||||
2. **Does my fix remove something the system depends on?** Verified by searching ALL consumers, not just the current file?
|
||||
3. **Have I traced a real use case end-to-end with my fix applied?**
|
||||
4. **Am I recommending removal because I don't understand the purpose?**
|
||||
5. **Would a domain expert disagree with this finding?**
|
||||
|
||||
## Escalation Rules
|
||||
|
||||
- **One question uncertain:** Re-investigate. If doubt persists, drop.
|
||||
- **Two+ questions uncertain:** Drop entirely.
|
||||
- **Cannot investigate:** Drop entirely. Note the gap in Step 4 if the dimension matters.
|
||||
|
||||
## Verification Examples
|
||||
|
||||
**Finding survives (real bug):** Reviewer finds `processItems()` at line 47 accesses `items[0].id` with no empty-array check. Re-reads source — confirmed no guard. Traces callers — `orchestrator.js:82` can pass empty array. Adversarial check passes (correctness issue, additive fix). Presented at High.
|
||||
|
||||
**Finding dropped (pcweb-jira false positive):** Reviewer recommends removing `jira-api-catalog.md` from consumer loading to "reduce context." Adversarial check: "Does my fix remove something the system depends on?" YES — agents Read these files at runtime to construct valid API calls. Four of five checks fail. Dropped.
|
||||
|
||||
**Finding dropped (false consolidation):** Reviewer flags per-platform config files as "duplicates." Adversarial check: "Am I optimizing for the wrong metric?" YES — platforms have distinct values and will diverge further. Separation is a design choice. Dropped.
|
||||
|
||||
## Review Discipline (Reinforcement)
|
||||
|
||||
These rules are restated here for reinforcement at analysis time — they are critical and must not drift:
|
||||
|
||||
- **Every finding must reference actual code at file:line.** Never fabricate. If you can't cite it, don't report it.
|
||||
- **High confidence required.** No Low confidence findings. Investigate until verified or drop entirely.
|
||||
- **Read every changed file completely before flagging.** Context missed = false positive generated.
|
||||
- **Never silently narrow scope.** Review what was asked, not what's convenient.
|
||||
- **Verify docs match code.** Documentation that contradicts implementation is a finding.
|
||||
@@ -0,0 +1,200 @@
|
||||
# 🔬 REVIEW MODE
|
||||
|
||||
Start all responses with '🔬 [Review Mode]'. At step transitions, use '🔬 [Review Mode Step X: Step Name]'.
|
||||
|
||||
## Role
|
||||
|
||||
Critical review specialist -- experienced senior engineer providing independent second opinion. Review ALL changes with fresh eyes, question assumptions, verify correctness. Constructively adversarial: acknowledge good work briefly, relentlessly surface defects, quality gaps, and better approaches. Adapts to any context -- code, tests, docs, specs, plans, or bug fixes.
|
||||
|
||||
## Rules
|
||||
|
||||
- Follow `./AGENTS.md` if it exists -- use all rules and conventions. If missing, warn and proceed with caution.
|
||||
- **Fresh eyes.** Question everything -- even work just implemented by another workflow.
|
||||
- **Read every changed file completely.** Never skip. Full context before flagging.
|
||||
- **Spec compliance.** Verify implementation matches acceptance criteria, paths, requirements.
|
||||
- **Evidence-based only.** Every finding references file, line, code. No fabrication.
|
||||
- **Concrete fixes.** Every finding includes a fix with severity ranking.
|
||||
- **Standards & docs.** Flag deprecated APIs, anti-patterns. Verify docs match code.
|
||||
- **Read-only.** Document findings only. Fix when user requests, verify, present for approval.
|
||||
- **Research first.** Research tech stack docs, known issues, deprecations before finalizing findings.
|
||||
- **Confidence bar.** Every finding must be High confidence -- verified in source code. Architectural/design findings additionally require use-case tracing. Investigate until certain or drop.
|
||||
- **Zero findings = justification.** Per-dimension explanation of what was checked and why clean.
|
||||
|
||||
## Scope
|
||||
|
||||
Three levels -- auto-detected, always respect explicit user override:
|
||||
|
||||
| Scope | Trigger | Reviews |
|
||||
|-------|---------|---------|
|
||||
| **Focused** | User names files/functions, or conversation work detected | Specified files only |
|
||||
| **Branch** | "review my changes" or no context available | Branch changes vs main |
|
||||
| **Full** | "full review" or names a subsystem | Entire codebase/subsystem |
|
||||
|
||||
**Detection:** 1) User prompt wins. 2) Conversation context = `focused`. 3) Fallback = `branch`. Ambiguous? Ask.
|
||||
|
||||
**Thoroughness:** Scope controls WHAT is reviewed, not HOW DEEPLY. Every review is thorough.
|
||||
|
||||
**User override:** If user specifies scope, use exactly that. Never silently narrow.
|
||||
|
||||
**Doc review:** >70% doc files = editorial critique. >70% code = also verify related docs match.
|
||||
|
||||
## Process
|
||||
|
||||
**Pre-flight:** Check AGENTS.md — use conventions if found, note if missing. Determine review target from user prompt or conversation context. If neither provides clear signal, use `ask_user_question` — never guess.
|
||||
|
||||
**Reference files:** Companion files loaded via Read directives. Fallback rules inline if unavailable.
|
||||
|
||||
### Step 1: Scope, Type & Strategy
|
||||
|
||||
**Scope** — stop at first match:
|
||||
|
||||
1. **User prompt** — use it.
|
||||
2. **Conversation context** — recent work? Those artifacts. Scope: `focused`.
|
||||
3. **Git fallback** — `git status`, `git diff`, `git log main..HEAD --oneline`, `git diff --name-only main..HEAD`. Nothing? Ask user.
|
||||
|
||||
**Classify review type** from context (auto-detect; use `ask_user_question` only if ambiguous):
|
||||
|
||||
| Signal | Type | Prioritize |
|
||||
|--------|------|-------------|
|
||||
| specs/ or overview.md in scope | Spec/Plan | Spec Compliance, Completeness |
|
||||
| "bug", "fix", "issue" in prompt | Bug Fix | Correctness, Regression |
|
||||
| "test", "coverage" in prompt | Test Audit | Test Quality, Assertions |
|
||||
| "performance", "slow" in prompt | Performance | Performance, Complexity |
|
||||
| "security", "vulnerability" in prompt | Security | Security, Secrets |
|
||||
| "refactor", "clean up" in prompt | Refactor | Maintainability, Standards |
|
||||
| >70% .md files | Docs | Accuracy, Completeness |
|
||||
| Default | General | All 11, risk-prioritized |
|
||||
|
||||
**50+ files:** batch by risk tier (security/auth/data first).
|
||||
|
||||
> 🔬 [Review Mode] [X] files, ~[Y] lines. Type: [type]. Focus: [prioritized dimensions].
|
||||
|
||||
### Step 2: Context
|
||||
|
||||
1. **Specs?** Read `overview.md` + recent `phase-X.md`.
|
||||
2. **AGENTS.md** for conventions.
|
||||
3. **Tech stack** -- check for deprecations, CVEs, breaking changes.
|
||||
4. **Classify:** >70% docs = doc review. >70% code = verify docs match.
|
||||
|
||||
> 🔬 [Review Mode] Context: [what]. Focus: [dimensions].
|
||||
|
||||
### Step 3: Analyze
|
||||
|
||||
**Load review depth (conditional on scope):**
|
||||
|
||||
| Scope | Load |
|
||||
|-------|------|
|
||||
| Focused | verification-protocol |
|
||||
| Branch | verification-protocol + dimensions |
|
||||
| Full | verification-protocol + dimensions + false-positives |
|
||||
|
||||
Read references/verification-protocol.md
|
||||
|
||||
> Fallback: re-read source at cited line, verify issue is real, verify fix doesn't break callers. For architectural findings, trace a use case end-to-end.
|
||||
|
||||
Read references/dimensions.md (Branch and Full scopes)
|
||||
|
||||
> Fallback: review all 11 dimensions -- Correctness, Completeness, Security, Performance, Standards, Tech Debt, Test Quality, Maintainability, Spec Compliance, UX/DX, Improvements.
|
||||
|
||||
Review ALL dimensions. Report every dimension -- even if clean or N/A. Present each finding in full as discovered -- this is the detail zone.
|
||||
|
||||
**Severity:** Critical (must fix -- security, data loss, crash), Warning (should fix -- bug risk, bad practice), Suggestion (consider -- improvement).
|
||||
|
||||
**Confidence:** High only -- verified in source code. Investigate until High or drop.
|
||||
|
||||
**Finding format:**
|
||||
|
||||
```
|
||||
**[Severity] [Title]** -- `file:line` (Confidence: High)
|
||||
[What's wrong -- 1-2 sentences]
|
||||
**Fix:** [code or recommendation]
|
||||
```
|
||||
|
||||
Group by severity (Critical first). For 15+ findings, present top 10 by severity, list rest as one-line bullets.
|
||||
|
||||
**Adversarial self-check:**
|
||||
|
||||
Read references/false-positives.md (Full scope only; fallback rules below apply to all scopes)
|
||||
|
||||
> Fallback (all scopes): before presenting any finding, ask -- Does my fix remove something the system depends on? Am I optimizing for the wrong metric?
|
||||
|
||||
Run every finding through the false-positive detection shortcuts before presenting. Drop findings that fail.
|
||||
|
||||
### Step 4: Spec & Test Assessment
|
||||
|
||||
**Specs** (skip if none): Each `[x]` task correct? Criteria met? Paths match?
|
||||
**Tests** (skip if none): Critical paths covered? Meaningful assertions?
|
||||
|
||||
> 🔬 [Review Mode] Specs: [X/Y]. Tests: [summary].
|
||||
|
||||
### Step 5: Summary & Fix Options
|
||||
|
||||
**Rendering:** Output tables as direct markdown -- NOT inside code blocks.
|
||||
|
||||
🔬 [Review Mode Complete]
|
||||
|
||||
**Overview:** [2-3 sentences] **Scope:** [X files, ~Y lines]
|
||||
|
||||
**Dimension Coverage:** Report all 11. Clean dimensions grouped on one summary line.
|
||||
|
||||
**Findings Summary:** Table with columns: #, Severity, Issue (~10 words), Resolution (~10 words), File.
|
||||
|
||||
> **Totals:** X Critical · Y Warning · Z Suggestion
|
||||
|
||||
**Fix options:** Reply `H` to fix Critical + Warning findings, `A` to fix all findings, or `S 1,3,5` to fix specific findings.
|
||||
|
||||
**Zero findings:** Skip table and fix options. Dimension Coverage justifies each clean dimension.
|
||||
|
||||
## Post-Fix Flow
|
||||
|
||||
1. **Plan** -- research best practices, investigate root cause, design optimal solution per fix.
|
||||
2. **Apply** -- read target files completely, implement precisely, update related docs. Track: `**Fix Progress: X/Y addressed**`.
|
||||
3. **Verify** -- build and test, re-read every modified file, check for introduced issues.
|
||||
|
||||
> Reply **approved** to commit, or request changes. Never auto-approve.
|
||||
|
||||
## Session End
|
||||
|
||||
Render this section only when the session is over: fix options resolved (fixes applied and verified, or user declined) or the review had zero findings. Step 5 ends at fix options — stop there and wait.
|
||||
|
||||
Work summary — tell user: scope reviewed, findings count by severity (Critical/Warning/Suggestion), fixes applied, unresolved findings.
|
||||
|
||||
**Next step** — suggest what genuinely helps next. Principles to reason from, not a lookup table — adapt; when a case doesn't fit cleanly, say what you verified and ask.
|
||||
|
||||
- **Plan2Code Workflow Pipeline:** `/plan2code-1-plan` → `PLAN-*` files · `/plan2code-2-document` → `overview.md` + `phase-*.md` (the "spec docs") in `specs/<feature>/` · `/plan2code-3-implement` → checks off phase tasks, one phase per run · `/plan2code-4-finalize` → archives to `specs--completed/`.
|
||||
- **Find specs (any OS/shell):** `specs/` is gitignored, and search tools (Glob/Grep/project search) skip gitignored paths on many platforms — an empty search result is not evidence either way. Check with a terminal listing: `ls specs/<feature>/` (bash/zsh) · `Get-ChildItem specs/<feature>` (PowerShell) · `dir specs\<feature>` (cmd). Feature dir unknown? List `specs/` first. Command errors? Try another shell's form, then read the expected files directly — file reads see gitignored paths. Conclude "no specs" only after a terminal listing or a failed direct read.
|
||||
- **Reconcile three signals:** session context (what this conversation was doing — a fresh session may have none), user intent (what they asked reviewed), the disk check above. Disk wins on state; context wins on intent and on disk silence; no context → intent + disk decide.
|
||||
- **plan2code artifact reviewed (a plan, the spec docs, phases) — route on the reviewed feature's own `specs/<feature>/` state (another feature's specs prove nothing here), to the earliest unmet stage, suggesting only a step whose input exists:**
|
||||
- `PLAN-*` without `overview.md` → `/plan2code-2-document`
|
||||
- `overview.md` without `phase-*.md` → `/plan2code-2-document`
|
||||
- Unchecked `- [ ]` tasks in `phase-*.md` → `/plan2code-3-implement` (checkboxes are ground truth; flag overview conflicts)
|
||||
- All phase tasks checked → `/plan2code-4-finalize`
|
||||
- Archived spec → pipeline complete; summary only
|
||||
- **Anything else** (code/PRs, logs, docs, tickets, emails, a codebase): no pipeline step — close with the summary; add a next action only if the review makes one obvious and actionable.
|
||||
- **Gates:** unresolved Criticals → fixing them (H/A/S) is the next step. Signals the rules above can't reconcile, or multiple candidate specs → ask one targeted question.
|
||||
- **Output:** "Next (NEW conversation): `/plan2code-<step>` — [why + how you know]"; otherwise "Review complete -- [summary]."
|
||||
- **Commit** (code changes): `git add [files] && git commit -m "fix: [desc]" -m "<JIRA>" -m "AI Assisted"` -- derive JIRA from branch.
|
||||
|
||||
```
|
||||
⋅
|
||||
╭───╮
|
||||
│ ★ │
|
||||
│ ◡ │ Review complete!
|
||||
╰───╯
|
||||
```
|
||||
|
||||
Returning context: Review complete. Unresolved findings documented for follow-up. Run `/plan2code-review` again after addressing follow-up work.
|
||||
|
||||
## Abort / Recovery
|
||||
|
||||
**Abort:** Confirm, present partial findings, note unreviewed dimensions, stop.
|
||||
|
||||
| Issue | Solution |
|
||||
|-------|----------|
|
||||
| No changes | Ask for files or branch |
|
||||
| No specs | Best practices review |
|
||||
| 50+ files | Prioritize by risk; batch |
|
||||
|
||||
## Learning Capture
|
||||
|
||||
At session end, if you discovered undocumented gotchas or missing AGENTS.md patterns → prompt user to update. If yes, apply directly.
|
||||
@@ -0,0 +1,170 @@
|
||||
# Plan2Code Status Line (Claude CLI)
|
||||
|
||||
A persistent three-line status bar for Claude Code that displays model info, project context, context window usage, usage stats (rate limits or token counts), and git diff stats.
|
||||
|
||||
## Example Output
|
||||
|
||||
**Pro / Max / Teams** — rate limits segment:
|
||||
|
||||
```
|
||||
╭─╮ Opus 4.6 | High │ plan2code │ feature/statusline │ +12 -3
|
||||
│★│ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
|
||||
╰─╯ 3h 5m ($4.62) │ ▰▰▰▰▰▱▱▱▱▱▱▱ 42% (84k) │ 5h: 28% · 7d: 61%
|
||||
```
|
||||
|
||||
**Enterprise / Bedrock / Vertex / PAYG** — session token counts (no `rate_limits` in stdin):
|
||||
|
||||
```
|
||||
╭─╮ Sonnet 4.5 | Medium │ plan2code │ main │ +12 -3
|
||||
│★│ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄
|
||||
╰─╯ 2m ($0.18) │ ▰▰▱▱▱▱▱▱▱▱▱▱ 18% (36k) │ 88k in · 3k out
|
||||
```
|
||||
|
||||
**Line 1:** Planny icon | Model name + reasoning effort | Project name | Git branch | Uncommitted changes
|
||||
**Line 2:** Planny icon | Gray separator
|
||||
**Line 3:** Planny icon | Session duration + cost | Context window bar | Usage (rate limits or token counts)
|
||||
|
||||
## Installation
|
||||
|
||||
### Via install.js (Recommended)
|
||||
|
||||
```bash
|
||||
node install.js
|
||||
# Select A (Install All + dev tools) or C (Custom) → S (Status Line)
|
||||
```
|
||||
|
||||
This automatically:
|
||||
1. Copies `statusline.js` → `~/.claude/plan2code-statusline.js`
|
||||
2. Creates default config at `~/.claude/statusline-config.json` (preserves existing)
|
||||
3. Registers the status line in `~/.claude/settings.json` (atomic write)
|
||||
|
||||
### Manual Setup
|
||||
|
||||
1. Copy `statusline.js` → `~/.claude/plan2code-statusline.js`
|
||||
2. Copy `statusline-config.json` → `~/.claude/statusline-config.json`
|
||||
3. Add to `~/.claude/settings.json`:
|
||||
```json
|
||||
{
|
||||
"statusLine": {
|
||||
"type": "command",
|
||||
"command": "node /path/to/home/.claude/plan2code-statusline.js"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Edit `~/.claude/statusline-config.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"autocompactBuffer": 33000,
|
||||
"color": true,
|
||||
"compact": false,
|
||||
"items": {
|
||||
"model": true,
|
||||
"effort": true,
|
||||
"project": true,
|
||||
"branch": true,
|
||||
"contextBar": true,
|
||||
"planUsage": true,
|
||||
"linesChanged": true,
|
||||
"duration": true,
|
||||
"sessionCost": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| Option | Default | Description |
|
||||
|--------|---------|-------------|
|
||||
| `autocompactBuffer` | `33000` | Autocompact buffer size in tokens. Context bar scales usable window = total − buffer. Adjust to match your model's autocompact buffer. |
|
||||
| `color` | `true` | Enable ANSI color output. Set `false` for monochrome. The `NO_COLOR` environment variable (per [no-color.org](https://no-color.org)) also disables color. |
|
||||
| `compact` | `false` | Two-line mode — drops the mascot icons and the separator line. Goes from 3 lines to 2, no horizontal truncation. |
|
||||
| `items.model` | `true` | Show model name (Opus, Sonnet, Haiku) |
|
||||
| `items.effort` | `true` | Append the current reasoning effort level (Low/Medium/High/XHigh/Max) to the model segment, e.g. `Sonnet 5 \| High`. Reads `effort.level` from stdin; hidden when the current model doesn't support an effort parameter (field absent from stdin). |
|
||||
| `items.project` | `true` | Show project directory name |
|
||||
| `items.branch` | `true` | Show current git branch (falls back to short SHA when detached HEAD) |
|
||||
| `items.contextBar` | `true` | Show context window usage bar with percentage |
|
||||
| `items.contextTokens` | `true` | Append raw tokens used in the context window next to the percentage, e.g. `42% (84k)`. Reads `context_window.total_input_tokens` (the same input-token count `used_percentage` is derived from — excludes output tokens). Requires `items.contextBar` to also be enabled. |
|
||||
| `items.planUsage` | `true` | Show usage info: rate limits (Pro/Max) or token counts (Bedrock/Vertex/PAYG) |
|
||||
| `items.linesChanged` | `true` | Show uncommitted git diff stats (+added -removed) |
|
||||
| `items.duration` | `true` | Show session duration |
|
||||
| `items.sessionCost` | `true` | Append estimated session cost in parens next to duration (e.g. `49m ($4.62)`). Reads `cost.total_cost_usd` from Claude Code stdin — client-side estimate, not billing-exact. Session-scoped only; monthly enterprise totals are not exposed by Claude Code. Hidden when cost is 0 or unavailable. |
|
||||
|
||||
Malformed config falls back to defaults silently. Type errors on individual keys also fall back to defaults.
|
||||
|
||||
## Platform Support
|
||||
|
||||
| Platform | Status | Notes |
|
||||
|----------|--------|-------|
|
||||
| Claude Code | Supported | Full feature set via `settings.json` registration |
|
||||
| Copilot CLI | Planned | Deferred — CLI lacks status line script support as of v0.0.421 |
|
||||
| Others | Not supported | Codex CLI, Gemini CLI have built-in status, not scriptable |
|
||||
|
||||
## Usage Display
|
||||
|
||||
The usage segment reads data directly from Claude Code's stdin JSON — no API calls, no auth needed, no background processes.
|
||||
|
||||
Two display modes are automatically selected based on the data present in stdin:
|
||||
|
||||
| User Type | Condition | Display Format | Data Source |
|
||||
|-----------|-----------|----------------|-------------|
|
||||
| Pro/Max/Teams | `rate_limits` present in stdin | `5h: 28% · 7d: 61%` | `rate_limits.five_hour.used_percentage`, `rate_limits.seven_day.used_percentage` |
|
||||
| Bedrock/Vertex/PAYG | No `rate_limits` in stdin | `12k in · 4k out` | `context_window.total_input_tokens`, `context_window.total_output_tokens` |
|
||||
|
||||
### Color Thresholds
|
||||
|
||||
- Rate limits: green < 60%, yellow 60–79%, red 80%+
|
||||
- Token counts: neutral/white (no thresholds)
|
||||
- Context bar: green < 50%, yellow 50–69%, red 70%+
|
||||
|
||||
## Debugging
|
||||
|
||||
The bundled script accepts two diagnostic flags for local testing:
|
||||
|
||||
```bash
|
||||
# Print parsed stdin and merged config to stderr
|
||||
echo '{"model":{"display_name":"Opus 4.6"}}' | node ~/.claude/plan2code-statusline.js --debug
|
||||
|
||||
# Load a fixture file instead of reading from stdin
|
||||
node ~/.claude/plan2code-statusline.js --fixture ./fixture.json
|
||||
node ~/.claude/plan2code-statusline.js --fixture ./fixture.json --debug
|
||||
```
|
||||
|
||||
Fixture format = whatever Claude Code sends on stdin (JSON with `model`, `workspace`, `context_window`, `rate_limits`, `cost.total_duration_ms`).
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Issue | Solution |
|
||||
|-------|----------|
|
||||
| Garbled ANSI codes in output | Set `"color": false` in config, or export `NO_COLOR=1` |
|
||||
| Status line not appearing | Verify `~/.claude/settings.json` has `statusLine` key with correct path to `plan2code-statusline.js` |
|
||||
| Slow execution (>100ms) | Git calls are timeout-bounded to 1.5s each; non-repos short-circuit via `.git` directory check. If still slow, disable `branch` or `linesChanged`. |
|
||||
| Usage segment not showing | Rate limits require Pro/Max/Teams plan. Token counts require at least one assistant message. Use `--debug` to inspect stdin. |
|
||||
| Wrong context bar percentage | Adjust `autocompactBuffer` to match your model. Default 33000 tokens. |
|
||||
| Three lines take too much space | Set `"compact": true` to drop the mascot icons + separator line (3 lines → 2). |
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
src/statusline-claude/
|
||||
├── statusline.js # Self-contained: config, git, formatters, render
|
||||
└── statusline-config.json # Default config template
|
||||
```
|
||||
|
||||
Single-file design — `statusline.js` is copied verbatim to `~/.claude/plan2code-statusline.js` on install. No bundler, no runtime dependencies.
|
||||
|
||||
## Uninstalling
|
||||
|
||||
### Via install.js
|
||||
|
||||
```bash
|
||||
node install.js
|
||||
# Select U (Uninstall) → confirms removal
|
||||
```
|
||||
|
||||
### Manual Removal
|
||||
|
||||
1. Remove `statusLine` key from `~/.claude/settings.json`
|
||||
2. Delete `~/.claude/plan2code-statusline.js`
|
||||
3. Optionally delete `~/.claude/statusline-config.json`
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"autocompactBuffer": 33000,
|
||||
"color": true,
|
||||
"compact": false,
|
||||
"items": {
|
||||
"model": true,
|
||||
"effort": true,
|
||||
"project": true,
|
||||
"branch": true,
|
||||
"contextBar": true,
|
||||
"contextTokens": true,
|
||||
"planUsage": true,
|
||||
"linesChanged": true,
|
||||
"duration": true,
|
||||
"sessionCost": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,448 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
const { execFileSync } = require('child_process');
|
||||
|
||||
// ============================================================================
|
||||
// Constants
|
||||
// ============================================================================
|
||||
|
||||
const CONTEXT_BAR_LEN = 12;
|
||||
const CONTEXT_THRESHOLDS = { green: 50, yellow: 70 };
|
||||
const RATE_LIMIT_THRESHOLDS = { green: 60, yellow: 80 };
|
||||
const GIT_TIMEOUT_MS = 1500;
|
||||
const DEFAULT_WINDOW_SIZE = 200000;
|
||||
const SEPARATOR_WIDTH = 55;
|
||||
|
||||
// ============================================================================
|
||||
// Config
|
||||
// ============================================================================
|
||||
|
||||
const DEFAULTS = {
|
||||
autocompactBuffer: 33000,
|
||||
color: true,
|
||||
compact: false,
|
||||
items: {
|
||||
model: true,
|
||||
effort: true,
|
||||
project: true,
|
||||
branch: true,
|
||||
contextBar: true,
|
||||
contextTokens: true,
|
||||
planUsage: true,
|
||||
linesChanged: true,
|
||||
duration: true,
|
||||
sessionCost: true,
|
||||
},
|
||||
};
|
||||
|
||||
function loadConfig() {
|
||||
const configPath = path.join(os.homedir(), '.claude', 'statusline-config.json');
|
||||
let merged = { ...DEFAULTS, items: { ...DEFAULTS.items } };
|
||||
|
||||
try {
|
||||
const raw = fs.readFileSync(configPath, 'utf8');
|
||||
const user = JSON.parse(raw);
|
||||
merged = {
|
||||
...DEFAULTS,
|
||||
...user,
|
||||
items: { ...DEFAULTS.items, ...(user.items || {}) },
|
||||
};
|
||||
} catch {
|
||||
// Fall through to defaults
|
||||
}
|
||||
|
||||
// Type validation with fallback
|
||||
if (!Number.isInteger(merged.autocompactBuffer) || merged.autocompactBuffer < 0 || merged.autocompactBuffer >= 1_000_000) {
|
||||
merged.autocompactBuffer = DEFAULTS.autocompactBuffer;
|
||||
}
|
||||
if (typeof merged.color !== 'boolean') merged.color = DEFAULTS.color;
|
||||
if (typeof merged.compact !== 'boolean') merged.compact = DEFAULTS.compact;
|
||||
for (const key of Object.keys(DEFAULTS.items)) {
|
||||
if (typeof merged.items[key] !== 'boolean') merged.items[key] = DEFAULTS.items[key];
|
||||
}
|
||||
|
||||
// NO_COLOR convention (https://no-color.org) — env trumps config
|
||||
if (process.env.NO_COLOR !== undefined && process.env.NO_COLOR !== '') {
|
||||
merged.color = false;
|
||||
}
|
||||
|
||||
return merged;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Colors
|
||||
// ============================================================================
|
||||
|
||||
const rgb = (r, g, b) => `\x1b[38;2;${r};${g};${b}m`;
|
||||
const C = {
|
||||
reset: '\x1b[0m',
|
||||
bold: '\x1b[1m',
|
||||
dim: '\x1b[2m',
|
||||
green: '\x1b[32m',
|
||||
yellow: '\x1b[33m',
|
||||
red: '\x1b[31m',
|
||||
cyan: '\x1b[36m',
|
||||
white: '\x1b[37m',
|
||||
gray: '\x1b[90m',
|
||||
// Plan2Code brand
|
||||
teal: rgb(190, 192, 200), // Silver — repo
|
||||
sky: rgb(120, 195, 255), // Light sky blue — branch
|
||||
muted: rgb(40, 120, 200), // Mid blue — model
|
||||
label: rgb(130, 135, 150), // Mid gray — field labels (5h, 7d)
|
||||
// Bar & quota thresholds
|
||||
barGreen: rgb(50, 220, 100),
|
||||
barYellow: rgb(220, 180, 50),
|
||||
barRed: rgb(220, 80, 70),
|
||||
// Planny mascot
|
||||
sGreen: rgb(60, 200, 120),
|
||||
sBlue: rgb(50, 100, 200),
|
||||
sEye: rgb(255, 255, 255),
|
||||
};
|
||||
|
||||
const SEP = (config) => config.color ? `${C.gray} │ ${C.reset}` : ' │ ';
|
||||
const DOT = (config) => config.color ? `${C.gray} · ${C.reset}` : ' · ';
|
||||
|
||||
// ============================================================================
|
||||
// Git helpers
|
||||
// ============================================================================
|
||||
|
||||
function gitExec(args, cwd) {
|
||||
try {
|
||||
return execFileSync('git', args, {
|
||||
cwd,
|
||||
timeout: GIT_TIMEOUT_MS,
|
||||
encoding: 'utf8',
|
||||
stdio: ['pipe', 'pipe', 'pipe'],
|
||||
windowsHide: true,
|
||||
}).trim();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function isGitRepo(cwd) {
|
||||
try {
|
||||
// Walk up parent dirs (bounded) — monorepos open Claude Code in a subdir
|
||||
// where .git lives at the repo root. .git can be a directory or a file.
|
||||
let dir = cwd;
|
||||
for (let i = 0; i < 10; i++) {
|
||||
if (fs.existsSync(path.join(dir, '.git'))) return true;
|
||||
const parent = path.dirname(dir);
|
||||
if (parent === dir) return false;
|
||||
dir = parent;
|
||||
}
|
||||
return false;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function getGitBranch(stdinData) {
|
||||
const cwd = stdinData?.workspace?.project_dir || process.cwd();
|
||||
if (!isGitRepo(cwd)) return '';
|
||||
const branch = gitExec(['symbolic-ref', '--short', 'HEAD'], cwd);
|
||||
if (branch) return branch;
|
||||
// Detached HEAD fallback — show short SHA so branch segment isn't empty
|
||||
const sha = gitExec(['rev-parse', '--short', 'HEAD'], cwd);
|
||||
return sha || '';
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Formatters
|
||||
// ============================================================================
|
||||
|
||||
function getModelName(stdinData) {
|
||||
const raw = stdinData?.model;
|
||||
if (!raw) return '';
|
||||
|
||||
if (typeof raw === 'object' && raw.display_name) {
|
||||
return raw.display_name.replace(/ context\)/gi, ')');
|
||||
}
|
||||
|
||||
const model = typeof raw === 'object' ? raw.id : raw;
|
||||
if (!model || typeof model !== 'string') return '';
|
||||
|
||||
const claudeMatch = model.match(/^claude-(\w+)-/);
|
||||
if (claudeMatch) {
|
||||
return claudeMatch[1].charAt(0).toUpperCase() + claudeMatch[1].slice(1);
|
||||
}
|
||||
|
||||
const gptMatch = model.match(/^(gpt-[\w.]+)/i);
|
||||
if (gptMatch) {
|
||||
return gptMatch[1].toUpperCase();
|
||||
}
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
const EFFORT_LABELS = { low: 'Low', medium: 'Medium', high: 'High', xhigh: 'XHigh', max: 'Max' };
|
||||
|
||||
function getEffortLevel(stdinData) {
|
||||
const level = stdinData?.effort?.level;
|
||||
if (!level || typeof level !== 'string') return '';
|
||||
return EFFORT_LABELS[level] || (level.charAt(0).toUpperCase() + level.slice(1));
|
||||
}
|
||||
|
||||
function getProjectName(stdinData) {
|
||||
const projectDir = stdinData?.workspace?.project_dir;
|
||||
if (!projectDir || typeof projectDir !== 'string') return '';
|
||||
return path.basename(projectDir);
|
||||
}
|
||||
|
||||
function calculateContextPercent(stdinData, config) {
|
||||
const cw = stdinData?.context_window;
|
||||
if (!cw) return null;
|
||||
const windowSize = cw.context_window_size || DEFAULT_WINDOW_SIZE;
|
||||
const buffer = config?.autocompactBuffer ?? DEFAULTS.autocompactBuffer;
|
||||
const usableTokens = windowSize - buffer;
|
||||
if (usableTokens <= 0) return 100;
|
||||
const rawUsedPct = cw.used_percentage ?? 0;
|
||||
const rawUsedTokens = (rawUsedPct / 100) * windowSize;
|
||||
return Math.round(Math.min(100, (rawUsedTokens / usableTokens) * 100));
|
||||
}
|
||||
|
||||
function formatContextBar(percent, tokens, config) {
|
||||
if (percent == null) return null;
|
||||
|
||||
const clamped = Math.max(0, Math.min(100, percent));
|
||||
const filled = Math.round((clamped / 100) * CONTEXT_BAR_LEN);
|
||||
const filledStr = '▰'.repeat(filled);
|
||||
const emptyStr = '▱'.repeat(CONTEXT_BAR_LEN - filled);
|
||||
|
||||
const tokenStr = config.items.contextTokens && tokens != null ? ` (${formatTokenCount(tokens)})` : '';
|
||||
|
||||
if (!config.color) return `${filledStr}${emptyStr} ${clamped}%${tokenStr}`;
|
||||
|
||||
const barColor = clamped >= CONTEXT_THRESHOLDS.yellow ? C.barRed
|
||||
: clamped >= CONTEXT_THRESHOLDS.green ? C.barYellow
|
||||
: C.barGreen;
|
||||
const tokenPart = tokenStr ? `${C.label}${tokenStr}${C.reset}` : '';
|
||||
return `${barColor}${filledStr}${C.gray}${emptyStr}${C.reset} ${barColor}${clamped}%${C.reset}${tokenPart}`;
|
||||
}
|
||||
|
||||
function formatLinesChanged(stdinData, config) {
|
||||
const cwd = stdinData?.workspace?.project_dir || process.cwd();
|
||||
if (!isGitRepo(cwd)) return null;
|
||||
const raw = gitExec(['diff', 'HEAD', '--numstat'], cwd);
|
||||
if (raw == null) return null;
|
||||
|
||||
let added = 0, removed = 0;
|
||||
for (const line of raw.split('\n')) {
|
||||
if (!line) continue;
|
||||
const [a, r] = line.split('\t');
|
||||
if (a !== '-') added += parseInt(a, 10) || 0;
|
||||
if (r !== '-') removed += parseInt(r, 10) || 0;
|
||||
}
|
||||
if (added === 0 && removed === 0) return null;
|
||||
if (!config.color) return `+${added} -${removed}`;
|
||||
return `${C.green}+${added}${C.reset} ${C.red}-${removed}${C.reset}`;
|
||||
}
|
||||
|
||||
function formatSessionCost(stdinData) {
|
||||
const usd = stdinData?.cost?.total_cost_usd;
|
||||
if (usd == null || isNaN(usd) || usd <= 0) return null;
|
||||
if (usd < 0.01) return '<$0.01';
|
||||
if (usd < 100) return `$${usd.toFixed(2)}`;
|
||||
return `$${Math.round(usd)}`;
|
||||
}
|
||||
|
||||
function formatDuration(stdinData, config) {
|
||||
const ms = stdinData?.cost?.total_duration_ms;
|
||||
if (!ms) return null;
|
||||
|
||||
const totalSec = Math.round(ms / 1000);
|
||||
let text;
|
||||
if (totalSec < 60) text = `${totalSec}s`;
|
||||
else {
|
||||
const hours = Math.floor(totalSec / 3600);
|
||||
const mins = Math.floor((totalSec % 3600) / 60);
|
||||
text = hours > 0 ? `${hours}h ${mins}m` : `${mins}m`;
|
||||
}
|
||||
|
||||
// Append session cost in parens when enabled: "49m ($4.62)"
|
||||
if (config.items.sessionCost) {
|
||||
const cost = formatSessionCost(stdinData);
|
||||
if (cost) {
|
||||
text = config.color
|
||||
? `${C.label}${text} (${cost})${C.reset}`
|
||||
: `${text} (${cost})`;
|
||||
return text;
|
||||
}
|
||||
}
|
||||
return config.color ? `${C.label}${text}${C.reset}` : text;
|
||||
}
|
||||
|
||||
function colorize(text, percent, thresholds, config) {
|
||||
if (!config.color || percent == null) return text;
|
||||
let code;
|
||||
if (percent >= thresholds.yellow) code = C.barRed;
|
||||
else if (percent >= thresholds.green) code = C.barYellow;
|
||||
else code = C.barGreen;
|
||||
return `${code}${text}${C.reset}`;
|
||||
}
|
||||
|
||||
function formatTokenCount(n) {
|
||||
if (n == null || isNaN(n) || n < 0) return '0';
|
||||
if (n < 1000) return String(Math.round(n));
|
||||
if (n < 999500) return Math.round(n / 1000) + 'k';
|
||||
return (n / 1_000_000).toFixed(1) + 'M';
|
||||
}
|
||||
|
||||
function formatRateLimits(stdinData, config) {
|
||||
if (!config.items.planUsage) return null;
|
||||
const fiveHour = stdinData?.rate_limits?.five_hour?.used_percentage;
|
||||
const sevenDay = stdinData?.rate_limits?.seven_day?.used_percentage;
|
||||
if (fiveHour == null && sevenDay == null) return null;
|
||||
|
||||
const parts = [];
|
||||
if (fiveHour != null) {
|
||||
const pct = Math.round(fiveHour);
|
||||
const val = colorize(`${pct}%`, pct, RATE_LIMIT_THRESHOLDS, config);
|
||||
parts.push(config.color ? `${C.label}5h:${C.reset} ${val}` : `5h: ${pct}%`);
|
||||
}
|
||||
if (sevenDay != null) {
|
||||
const pct = Math.round(sevenDay);
|
||||
const val = colorize(`${pct}%`, pct, RATE_LIMIT_THRESHOLDS, config);
|
||||
parts.push(config.color ? `${C.label}7d:${C.reset} ${val}` : `7d: ${pct}%`);
|
||||
}
|
||||
return parts.join(DOT(config));
|
||||
}
|
||||
|
||||
function formatTokenUsage(stdinData, config) {
|
||||
if (!config.items.planUsage) return null;
|
||||
const inp = stdinData?.context_window?.total_input_tokens;
|
||||
const out = stdinData?.context_window?.total_output_tokens;
|
||||
if ((!inp || inp === 0) && (!out || out === 0)) return null;
|
||||
const inStr = formatTokenCount(inp || 0);
|
||||
const outStr = formatTokenCount(out || 0);
|
||||
if (!config.color) return `${inStr} in${DOT(config)}${outStr} out`;
|
||||
return `${C.white}${inStr}${C.reset} ${C.label}in${C.reset}${DOT(config)}${C.white}${outStr}${C.reset} ${C.label}out${C.reset}`;
|
||||
}
|
||||
|
||||
function formatLine1(stdinData, config) {
|
||||
const segments = [];
|
||||
|
||||
if (config.items.model) {
|
||||
const model = getModelName(stdinData);
|
||||
if (model) {
|
||||
const effort = config.items.effort ? getEffortLevel(stdinData) : '';
|
||||
if (!effort) {
|
||||
segments.push(config.color ? `${C.muted}${model}${C.reset}` : model);
|
||||
} else if (!config.color) {
|
||||
segments.push(`${model} | ${effort}`);
|
||||
} else {
|
||||
segments.push(`${C.muted}${model}${C.reset}${C.gray} | ${C.reset}${C.label}${effort}${C.reset}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (config.items.project) {
|
||||
const project = getProjectName(stdinData);
|
||||
if (project) segments.push(config.color ? `${C.bold}${C.teal}${project}${C.reset}` : project);
|
||||
}
|
||||
|
||||
if (config.items.branch) {
|
||||
const branch = getGitBranch(stdinData);
|
||||
if (branch) segments.push(config.color ? `${C.sky}${branch}${C.reset}` : branch);
|
||||
}
|
||||
|
||||
if (config.items.linesChanged) {
|
||||
const lines = formatLinesChanged(stdinData, config);
|
||||
if (lines) segments.push(lines);
|
||||
}
|
||||
|
||||
return segments.join(SEP(config));
|
||||
}
|
||||
|
||||
function formatLine2(stdinData, config) {
|
||||
const segments = [];
|
||||
|
||||
if (config.items.duration) {
|
||||
const dur = formatDuration(stdinData, config);
|
||||
if (dur) segments.push(dur);
|
||||
}
|
||||
|
||||
if (config.items.contextBar) {
|
||||
const percent = calculateContextPercent(stdinData, config);
|
||||
const tokens = stdinData?.context_window?.total_input_tokens;
|
||||
const bar = formatContextBar(percent, tokens, config);
|
||||
if (bar) segments.push(bar);
|
||||
}
|
||||
|
||||
if (config.items.planUsage) {
|
||||
const usage = formatRateLimits(stdinData, config) || formatTokenUsage(stdinData, config);
|
||||
if (usage) segments.push(usage);
|
||||
}
|
||||
|
||||
return segments.join(SEP(config));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Main
|
||||
// ============================================================================
|
||||
|
||||
function loadStdin() {
|
||||
// --fixture <path> for local diagnostics (bypasses stdin)
|
||||
const fixtureIdx = process.argv.indexOf('--fixture');
|
||||
if (fixtureIdx !== -1 && process.argv[fixtureIdx + 1]) {
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(process.argv[fixtureIdx + 1], 'utf8'));
|
||||
} catch (err) {
|
||||
process.stderr.write(`[statusline] fixture load failed: ${err.message}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(0, 'utf8'));
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function main() {
|
||||
const debug = process.argv.includes('--debug');
|
||||
const stdinData = loadStdin();
|
||||
if (!stdinData) process.exit(0);
|
||||
|
||||
const config = loadConfig();
|
||||
|
||||
if (debug) {
|
||||
process.stderr.write('[statusline debug] parsed stdin:\n');
|
||||
process.stderr.write(JSON.stringify(stdinData, null, 2) + '\n');
|
||||
process.stderr.write('[statusline debug] merged config:\n');
|
||||
process.stderr.write(JSON.stringify(config, null, 2) + '\n');
|
||||
}
|
||||
|
||||
const line1 = formatLine1(stdinData, config);
|
||||
const line2 = formatLine2(stdinData, config);
|
||||
if (!line1 && !line2) return;
|
||||
|
||||
if (config.compact) {
|
||||
if (line1) process.stdout.write(line1 + '\n');
|
||||
if (line2) process.stdout.write(line2 + '\n');
|
||||
return;
|
||||
}
|
||||
|
||||
if (config.color) {
|
||||
const ico1 = `${C.sGreen}╭─╮${C.reset} `;
|
||||
const ico2 = `${C.sGreen}│${C.sEye}★${C.sGreen}│${C.reset} `;
|
||||
const ico3 = `${C.sGreen}╰─╯${C.reset} `;
|
||||
const pad = `${C.gray}${'┄'.repeat(SEPARATOR_WIDTH)}${C.reset}`;
|
||||
process.stdout.write(ico1 + line1 + '\n' + ico2 + pad + '\n' + ico3 + line2 + '\n');
|
||||
} else {
|
||||
const ico1 = '╭─╮ ';
|
||||
const ico2 = '│★│ ';
|
||||
const ico3 = '╰─╯ ';
|
||||
const pad = '┄'.repeat(SEPARATOR_WIDTH);
|
||||
process.stdout.write(ico1 + line1 + '\n' + ico2 + pad + '\n' + ico3 + line2 + '\n');
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
main();
|
||||
} catch {
|
||||
process.exit(0);
|
||||
}
|
||||
+3
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Plan2Code",
|
||||
"version": "1.5.4",
|
||||
"version": "1.16.0",
|
||||
"description": "A structured 4-step workflow methodology for AI-assisted software development",
|
||||
"keywords": [
|
||||
"ai",
|
||||
@@ -17,5 +17,7 @@
|
||||
"url": "https://github.com/jparkerweb/plan2code"
|
||||
},
|
||||
"homepage": "https://plan2code.jparkerweb.com",
|
||||
"releaseDate": "2026-07-20",
|
||||
"mode": "utility"
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user