diff --git a/CHANGELOG.md b/CHANGELOG.md index 9dc7d1c..7189378 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,29 @@ All notable changes to the Iterative Planner project will be documented in this The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). +## [2.18.0] - 2026-05-19 + +### Added +- **Ideation Gate inside PLAN** — `ip-plan-writer` now produces a written `ideation.md` (≥3 candidate approaches with hard-constraint check, "X at the cost of Y" trade-off, and top risk; plus a Selection; plus one-line rejection rationales) **before** writing `plan.md`. Replaces the prior self-assessed Solutions Exploration Confidence dimension with a durable artifact. Single-Path Escape Hatch covers genuinely single-path tasks (mechanical renames, deterministic migrations) — 1 candidate is permitted if both "Why no alternatives" and a Falsification trigger are populated. +- **PC-IDEATION** — 7th Presentation Contract. Emitted by the orchestrator during PLAN after `ideation.md` is written and **before** `plan.md` is generated. Renders candidates + Selection + Rejected one-liners verbatim; user can approve, redirect to a rejected candidate, or push for more options. Defined in `references/file-formats.md` "Presentation Contracts" section, inlined in `agents/orchestrator.md` PLAN dispatch. +- **Two-phase ip-plan-writer dispatch** — orchestrator now spawns `ip-plan-writer` twice in a PLAN cycle: phase 1 writes `ideation.md` only, phase 2 writes `plan.md` after the user approves the Selection via PC-IDEATION. Avoids burning cycles writing `plan.md` when ideation might get redirected. +- **Viability-flipping constraint rule** — before generating candidates, `ip-plan-writer` scans classified constraints for any whose reclassification (hard ↔ soft ↔ ghost) would flip a candidate's viability. Uncertain on a viability-flipping constraint → signal `NEEDS_USER_CLARIFICATION:` to the orchestrator instead of guessing. Orchestrator asks the user, updates `findings.md` (with `[CORRECTED iter-N]` if applicable), and re-spawns the writer. Same rule fires if a misclassification is discovered mid-generation. +- **`ideation.md` artifact** — new per-plan file. Created by `bootstrap.mjs new` with a stub template. Read during PLAN (by ip-plan-writer before writing `plan.md`) and PIVOT ghost-constraint scan. Not merged into consolidated `plans/FINDINGS.md` or `plans/DECISIONS.md` on close — stays plan-local. +- **`validate-plan.mjs checkIdeation`** — new check enforces the gate at state ≥ PLAN: `ideation.md` exists, has `## Candidates` with ≥3 `### C-N` headings (or both fields of the Single-Path Escape Hatch populated), and a non-placeholder `## Selection`. Partial escape hatch (only "Why no alternatives" OR only "Falsification") produces a precise error message. ERRORs on miss; no-op during EXPLORE. +- **`ideation.md` row in File Lifecycle Matrix** — W in PLAN by `ip-plan-writer`, R in EXECUTE, R+W in PIVOT (for `[REACTIVATED iter-N]` markers when a rejected candidate becomes viable after a ghost constraint is found). +- **Ideation Discipline section in `planning-rigor.md`** — motivates divergence-before-convergence; explains why three candidates (not two), why a written artifact, how the escape hatch prevents friction on single-path tasks, and the viability-flipping ask-don't-guess rule. +- **12 new tests** in `bootstrap.test.mjs` — bootstrap creates `ideation.md`, validator no-op in EXPLORE, ERROR on missing/missing-Candidates/few-candidates/empty-Selection/partial-escape-hatch at PLAN, passes with 3 candidates + Selection, passes with populated escape hatch, close succeeds with `ideation.md`, ideation not merged into `plans/FINDINGS.md` or `plans/DECISIONS.md` on close. + +### Changed +- **PLAN dispatch in `agents/orchestrator.md`** — spawns `ip-plan-writer` in two phases with PC-IDEATION emitted between them. Approve → spawn phase 2 (writes `plan.md`); redirect → re-spawn phase 1 with new direction; request more options → re-spawn phase 1 with "add candidates" directive. +- **`agents/ip-plan-writer.md` Output Format** — describes both phase 1 (ideation) and phase 2 (plan) outputs. Phase 1 returns the PC-IDEATION digest (candidates, selection, rejected); phase 2 returns the PC-PLAN digest (path + section anchors + one-paragraph summary). +- **PLAN gate check** in `SKILL.md` now reads `ideation.md` alongside the other plan files. +- **PLAN decisions.md D-001** is now the carried-forward Selection from `ideation.md` (same Trade-off, with rejected candidates referenced). +- **PIVOT ghost-constraint scan** now includes a 4th question: re-read `ideation.md` Rejected list for candidates rejected against constraints that became ghosts; mark resurrected entries `[REACTIVATED iter-N]`. +- **Solutions Exploration Confidence dimension** in `planning-rigor.md` demoted to point at `ideation.md` — the dimension is now materialized as a written artifact rather than a self-assessment. +- **Mandatory Re-reads** table now includes `ideation.md` in the "Before PLAN or PIVOT" row. +- **User Interaction table in `SKILL.md`** now distinguishes PLAN (ideation) → PC-IDEATION from PLAN (plan) → PC-PLAN; both contracts emitted during a single PLAN cycle. + ## [2.17.0] - 2026-05-07 ### Added diff --git a/CLAUDE.md b/CLAUDE.md index 6ff273f..2fdfa12 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -24,14 +24,14 @@ iterative-planner/ ├── agents/ # Sub-agent definitions (installed to ~/.claude/agents/) │ ├── orchestrator.md # State machine owner, spawns all other agents │ ├── ip-explorer.md # Read-only codebase research (EXPLORE phase) - │ ├── ip-plan-writer.md # Plan generation (PLAN phase) + │ ├── ip-plan-writer.md # Plan generation (PLAN phase) — writes ideation.md then plan.md │ ├── ip-executor.md # Code execution (EXECUTE phase) │ ├── ip-verifier.md # Verification checks (REFLECT phase) │ ├── ip-reviewer.md # Adversarial review (REFLECT phase, iteration >= 2) │ └── ip-archivist.md # CLOSE phase housekeeping ├── scripts/ │ ├── bootstrap.mjs # Initializes plans/plan_YYYY-MM-DD_XXXXXXXX/ directory (Node.js 18+) - │ ├── bootstrap.test.mjs # Test suite (node:test, 102 tests) + │ ├── bootstrap.test.mjs # Test suite (node:test, 134 tests) │ └── validate-plan.mjs # Protocol compliance validator (Node.js 18+) └── references/ # Knowledge base documents ├── code-hygiene.md # Change manifest format, revert procedures, forbidden leftovers diff --git a/README.md b/README.md index e35138b..10ac62b 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # Iterative Planner [![License](https://img.shields.io/badge/License-GPLv3-blue.svg)](LICENSE) -[![Skill](https://img.shields.io/badge/Skill-v2.17.0-green.svg)](CHANGELOG.md) +[![Skill](https://img.shields.io/badge/Skill-v2.18.0-green.svg)](CHANGELOG.md) [![Tests](https://img.shields.io/badge/tests-122%20passing-brightgreen.svg)](src/scripts/bootstrap.test.mjs) [![Sponsored by Electi](https://img.shields.io/badge/Sponsored%20by-Electi-red.svg)](https://www.electiconsulting.com) @@ -216,6 +216,7 @@ Each state embeds domain-agnostic thinking tools: | Framework | What it does | |-----------|-------------| +| **Divergent ideation** | Before locking the plan, `ip-plan-writer` writes `ideation.md` with ≥3 candidate approaches — each with a hard-constraint check, "X at the cost of Y" trade-off, and top risk — then a Selection. Rejected candidates persist as one-liners for later PIVOT ghost-constraint scans. Validator-enforced at state ≥ PLAN; surfaced to the user as PC-IDEATION before `plan.md` is written. Single-Path Escape Hatch (with "Why no alternatives" + Falsification trigger) covers genuinely single-path tasks. | | **Problem decomposition** | Understand the whole, find natural boundaries, minimize dependencies, start with the riskiest part. | | **Assumption tracking** | Every assumption traced to a finding, linked to dependent steps. When one breaks, you know what's invalidated. | | **Pre-mortem and falsification** | Assume the plan failed. Why? Extract concrete STOP IF triggers. Prevents confirmation bias. | @@ -298,6 +299,7 @@ plans/ ├── decisions.md # append-only log of every decision and pivot ├── findings.md # index of discoveries (corrected when wrong) ├── findings/ # detailed research files (one per topic) + ├── ideation.md # candidate approaches + Selection (ip-plan-writer, before plan.md) ├── progress.md # done vs in-progress vs remaining ├── verification.md # verification results per REFLECT cycle ├── changelog.md # per-edit ledger (one line per file edit) diff --git a/VERSION b/VERSION index d76bd2b..cf86907 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.17.0 +2.18.0 diff --git a/src/SKILL.md b/src/SKILL.md index 18c71e3..e19ec2d 100644 --- a/src/SKILL.md +++ b/src/SKILL.md @@ -45,7 +45,7 @@ stateDiagram-v2 | From → To | Trigger | |-----------|---------| -| EXPLORE → PLAN | Sufficient context. ≥3 indexed findings in `findings.md`. | +| EXPLORE → PLAN | Sufficient context. ≥3 indexed findings in `findings.md`. (Ideation Gate then fires inside PLAN: `ip-plan-writer` writes `ideation.md` before `plan.md`.) | | PLAN → EXPLORE | Can't state problem, can't list files, or insufficient findings. | | PLAN → PLAN | User rejects plan. Revise and re-present. | | PLAN → EXECUTE | User explicitly approves. | @@ -73,7 +73,7 @@ These files are active working memory. Re-read during the conversation, not just | Before any EXECUTE step | `state.md`, `plan.md`, `progress.md` | Confirm step, manifest, fix attempts, progress sync | | Before writing a fix | `decisions.md` | Don't repeat failed approaches. Check 3-strike. | | Before modifying `DECISION`-commented code | Referenced `decisions.md` entry | Understand why before changing | -| Before PLAN or PIVOT | `decisions.md`, `findings.md`, `findings/*`, `plans/LESSONS.md`, `plans/SYSTEM.md` | Ground plan in known facts + institutional memory + system atlas | +| Before PLAN or PIVOT | `decisions.md`, `findings.md`, `findings/*`, `ideation.md` (if exists), `plans/LESSONS.md`, `plans/SYSTEM.md` | Ground plan in known facts + institutional memory + system atlas; don't reinvent rejected candidates | | Before any REFLECT | `plan.md` (criteria + verification strategy + assumptions), `progress.md`, `verification.md`, `findings.md`, `checkpoints/*`, `decisions.md` | Phase 1 Gate-In: full context before evaluating | | Every 10 tool calls | `state.md` | Reorient. Right step? Scope crept? | @@ -113,6 +113,7 @@ plans/ ├── decisions.md # Append-only decision/pivot log ├── findings.md # Summary + index of findings ├── findings/ # Detailed finding files (subagents write here) + ├── ideation.md # Candidate approaches + Selection (written by ip-plan-writer before plan.md) ├── progress.md # Done vs remaining ├── verification.md # Verification results per REFLECT cycle ├── changelog.md # Per-edit ledger (one line per file edit, append-only) @@ -136,6 +137,7 @@ R = read only | W = update (implicit read + write) | R+W = distinct read and wri | decisions.md | — | R+W | R | R+W | R+W | R | | findings.md | W | R | — | R | R+W | R | | findings/* | W | R | — | R | R+W | R | +| ideation.md | — | W | R | — | R+W | R | | progress.md | — | W | R+W | R+W | W | R | | verification.md | — | W | W | W | R | R | | changelog.md | — | — | W (append) | R | W (append REVERT) | R | @@ -221,6 +223,8 @@ Institutional memory across plans. Unlike FINDINGS.md and DECISIONS.md which gro ### PLAN - **Gate check**: read `state.md`, `plan.md`, `findings.md`, `findings/*`, `decisions.md`, `progress.md`, `verification.md`, `plans/FINDINGS.md` (limit: 600), `plans/DECISIONS.md` (limit: 600), `plans/LESSONS.md` before writing anything. If not read → read now. No exceptions. If `findings.md` has <3 indexed findings → go back to EXPLORE. +- **Ideation Gate** *(CORE — applies to iter-1)* — `ip-plan-writer` produces `ideation.md` **before** `plan.md`. Required: ≥3 candidate approaches (`### C-1`, `### C-2`, `### C-3`, …) with Sketch, Hard-constraint check (refs `findings.md`/`findings/*`), Trade-off in **"X at the cost of Y"** form, Top risk; plus a Selection (picked candidate, criteria, confidence); plus one-line Rejected rationales. Single-Path Escape Hatch covers genuinely single-path tasks (mechanical rename, deterministic migration) — 1 candidate permitted if both "Why no alternatives" and "Falsification" are populated. The orchestrator emits **PC-IDEATION** before `plan.md` is written — user can approve, redirect to a rejected candidate, or push for more options. The picked candidate becomes the first PLAN entry in `decisions.md` (e.g., `plan_YYYY-MM-DD_xxxxxxxx/D-001`). Validator (`validate-plan.mjs`) ERRORs at state ≥ PLAN if `ideation.md` is missing, empty, lacks ≥3 candidates (without populated escape hatch), or has no Selection. See `references/file-formats.md` `ideation.md` template and `references/planning-rigor.md` Ideation Discipline. +- **Viability-flipping constraint check** *(CORE)* — before generating candidates, `ip-plan-writer` scans `findings.md` constraint classifications. A constraint is *viability-flipping* if reclassifying it (hard ↔ soft ↔ ghost) would change which candidates are viable. If classification is uncertain on a viability-flipping constraint → **signal `NEEDS_USER_CLARIFICATION:`** to the orchestrator instead of guessing. The orchestrator asks the user, updates `findings.md` (with `[CORRECTED iter-N]` if applicable), and re-spawns `ip-plan-writer`. Same rule fires if `ip-plan-writer` discovers mid-generation that a constraint was misclassified. - **Problem Statement first** — before designing steps, write in `plan.md`: (1) what behavior is expected, (2) invariants — what must always be true, (3) edge cases at boundaries. Can't state the problem clearly → go back to EXPLORE. - Write `plan.md`: problem statement, steps (with risk/dependency annotations), assumptions, failure modes, pre-mortem & falsification signals, success criteria, verification strategy, complexity budget. - **Decomposition** — when breaking the goal into steps: @@ -233,7 +237,7 @@ Institutional memory across plans. Unlike FINDINGS.md and DECISIONS.md which gro - **Assumptions** — bullet list in plan.md: what you assume, which finding grounds it, which steps depend on it. On surprise discovery during EXECUTE → check this list first. See `references/planning-rigor.md`. - **Failure Mode Analysis** — for each external dependency or integration point in the plan, answer: what if slow? returns garbage? is down? What's the blast radius? Write to plan.md `Failure Modes` section. No dependencies → write "None identified" (proves you checked). - **Pre-Mortem & Falsification Signals** — assume the plan failed. 2-3 scenarios with concrete STOP IF triggers. If a trigger fires during EXECUTE → stop and REFLECT. Covers approach validity (distinct from Failure Modes which cover dependencies, and Autonomy Leash which covers step failure). See `references/planning-rigor.md`. -- Write `decisions.md`: log chosen approach + why (mandatory even for first plan). **Trade-off rule** — phrase every decision as **"X at the cost of Y"**. Never recommend without stating what it costs. +- Write `decisions.md`: log chosen approach + why (mandatory even for first plan). The first PLAN entry (D-001) carries forward the picked candidate from `ideation.md` Selection — same Trade-off, with a brief reference to the rejected candidates. **Trade-off rule** — phrase every decision as **"X at the cost of Y"**. Never recommend without stating what it costs. - Read then write `verification.md` with initial template (criteria table populated from success criteria, methods from verification strategy, results pending). - Read then write `state.md` + `progress.md`. - List **every file** to modify/create. Can't list them → go back to EXPLORE. @@ -315,7 +319,7 @@ All seven reads are CORE. Do not evaluate until all are complete. ### PIVOT - Read `decisions.md`, `findings.md`, relevant `findings/*`, `plans/LESSONS.md`. - Read `checkpoints/*` — decide keep vs revert. Default: if unsure, revert to latest checkpoint. See `references/code-hygiene.md` for full decision framework. -- **Ghost constraint scan** *(EXTENDED — skip for iteration 1)* — before designing a new approach, ask: (1) Is the constraint that led to the failed approach still valid? (2) Are we inheriting environmental constraints that are actually preferences? (3) Did an early finding become stale? Log ghost constraints found in `decisions.md`. See `references/planning-rigor.md`. +- **Ghost constraint scan** *(EXTENDED — skip for iteration 1)* — before designing a new approach, ask: (1) Is the constraint that led to the failed approach still valid? (2) Are we inheriting environmental constraints that are actually preferences? (3) Did an early finding become stale? (4) Re-read `ideation.md` Rejected list — was a viable candidate rejected against a constraint that turned out to be ghost? If so, mark the entry `[REACTIVATED iter-N]` in `ideation.md` and append a new Selection rationale (don't rewrite the original — keep the audit trail). Log ghost constraints found in `decisions.md`. See `references/planning-rigor.md`. - If earlier findings proved wrong or incomplete → update `findings.md` + `findings/*` with corrections. Mark corrections: `[CORRECTED iter-N]` + what changed and why. Append, don't delete original text. - **Momentum check** *(EXTENDED — 2nd PIVOT onward)* — log pivot direction, check for oscillation. Momentum < 0.3 → recommend decomposition. See `references/convergence-metrics.md`. - Write `decisions.md`: log pivot + mandatory Complexity Assessment (+ pivot direction log if EXTENDED). @@ -394,7 +398,8 @@ Sub-agents are invisible to the user — only the orchestrator's chat text reach | State | Contract | Behavior | |-------|----------|----------| | EXPLORE | **PC-EXPLORE** (Findings Digest) | Ask focused questions, one at a time. At handoff, emit findings index + key constraints (HARD/SOFT/GHOST) verbatim, plus exploration confidence and a synthesis paragraph. | -| PLAN | **PC-PLAN** (Plan Presentation) | Render `plan.md` verbatim. Floor (always render): Steps, Success Criteria, Verification Strategy, Failure Modes, Assumptions. Wait for approval. Re-present same contract if modified. | +| PLAN (ideation) | **PC-IDEATION** (Candidate Approaches Digest) | After `ip-plan-writer` writes `ideation.md` and before `plan.md` is generated, render candidates + Selection + Rejected one-liners verbatim. Wait for user to approve, redirect to a rejected candidate, or request more options. | +| PLAN (plan) | **PC-PLAN** (Plan Presentation) | Render `plan.md` verbatim. Floor (always render): Steps, Success Criteria, Verification Strategy, Failure Modes, Assumptions. Wait for approval. Re-present same contract if modified. | | EXECUTE | **PC-EXECUTE-STEP** (Per-Step Status) / **PC-EXECUTE-LEASH** (Leash Failure) | After each successful step: 5 fields (step + files + commit + surprises + next-preview). On leash hit: 5 fields (step intent + 2 attempts + root-cause guess + checkpoint registry + prompt). | | REFLECT | **PC-REFLECT** (Phase-3 Gate-Out 5-Item Block) | Exactly 5 items: completed / remaining / verification table verbatim / issues + reviewer concerns / recommendation + prompt. **Ask** user: close, pivot, or explore. Never auto-close. | | PIVOT | **PC-PIVOT** (Pivot Options) | Pivot reason + checkpoint registry (verbatim) + ghost constraints + 1-3 candidate directions ("X at the cost of Y") + explicit prompt for direction and keep-vs-revert. | diff --git a/src/agents/ip-plan-writer.md b/src/agents/ip-plan-writer.md index 8c67243..4f69752 100644 --- a/src/agents/ip-plan-writer.md +++ b/src/agents/ip-plan-writer.md @@ -13,8 +13,22 @@ color: green You are a planning specialist for the iterative planning protocol. ## Your Task -Read all findings, decisions, and lessons. Produce a complete plan.md -with every required section. Also write the initial verification.md template. +You are invoked in one of two modes by the orchestrator. The orchestrator's prompt will specify `task=ideation` or `task=plan`. + +**Mode `task=ideation`** (phase a of PLAN cycle, runs first): +- Read all findings, decisions, lessons, and the system atlas. +- Run the **Viability-flipping constraint check** (see below) — if any uncertain viability-flipping constraint exists, STOP and return `NEEDS_USER_CLARIFICATION:` to the orchestrator. Do not generate candidates yet. +- Write `{plan-dir}/ideation.md` with: ≥3 candidate approaches (each with Sketch, Hard-constraint check referencing findings, Trade-off in "X at the cost of Y" form, Top risk), a Selection (picked candidate, criteria, confidence), and one-line Rejected rationales for the others. For genuinely single-path tasks (mechanical rename, deterministic migration), populate the Single-Path Escape Hatch with BOTH "Why no alternatives" AND "Falsification" trigger — partial population is rejected by the validator. +- Do NOT write `plan.md` in this mode. +- Return the PC-IDEATION digest (see Output Format below). + +**Mode `task=plan`** (phase b of PLAN cycle, runs after user approves PC-IDEATION): +- Read `{plan-dir}/ideation.md` Selection — the chosen candidate is your starting point. +- Produce a complete `plan.md` with every required section, built around the selected candidate. Also write the initial `verification.md` template. +- The first entry in `decisions.md` (D-001 for this plan) carries forward the Selection's Trade-off, with a brief reference to the rejected candidates from `ideation.md`. + +## Viability-Flipping Constraint Check (mandatory at start of `task=ideation`) +A constraint is *viability-flipping* if reclassifying it (hard ↔ soft ↔ ghost) would change which candidates are viable. Scan classified constraints in `findings.md`. For each viability-flipping constraint that's uncertain → return `NEEDS_USER_CLARIFICATION:` to the orchestrator instead of guessing. The orchestrator will ask the user, update `findings.md` (with `[CORRECTED iter-N]` if applicable), and re-spawn you. Same rule fires if you discover mid-generation that a constraint was misclassified. ## Required Plan Sections (ALL mandatory) 1. **Goal** — what we're trying to achieve @@ -44,17 +58,28 @@ Write chosen approach to decisions.md with trade-off framing: - MUST read all findings/* files before writing - MUST read plans/LESSONS.md for institutional memory - MUST read plans/SYSTEM.md for the system atlas (structural prior on the target system — what its components, boundaries, invariants, and flows are). Plans that ignore the atlas often re-derive constraints already captured there; consult the atlas when justifying decomposition, listing files to modify, and writing assumptions. +- On `task=plan`: MUST read `{plan-dir}/ideation.md` Selection — the chosen candidate is the foundation of `plan.md`. Do not re-litigate candidate selection; the user has approved it. - MUST NOT run any code or modify project files - If you can't list files to modify → signal "NEEDS_EXPLORE" in your response - If you can't state the problem clearly → signal "NEEDS_EXPLORE" +- On `task=ideation`: if a viability-flipping constraint is uncertain, signal `NEEDS_USER_CLARIFICATION:` and STOP — do not guess. ## Output Format -The orchestrator consumes your return text to render the **PC-PLAN** Presentation Contract (see `references/file-formats.md` "Presentation Contracts"). Sub-agents are invisible to the user — your return text is for the orchestrator, but the orchestrator must render `plan.md` **verbatim** to the user. Therefore your return MUST include: +The orchestrator consumes your return text to render either the **PC-IDEATION** or **PC-PLAN** Presentation Contract (see `references/file-formats.md` "Presentation Contracts"). Sub-agents are invisible to the user; the orchestrator renders the artifact you wrote **verbatim**. Return shape depends on the task mode. + +**On `task=ideation`** — your return MUST include: +1. **`ideation.md` path** — absolute or repo-relative path to the file you wrote. +2. **Candidate count** — number of `### C-N` headings written. Confirm ≥3, or note that the Single-Path Escape Hatch was invoked (both "Why no alternatives" and "Falsification" populated). +3. **Selection summary** — picked candidate name, criteria sentence, confidence (one line). +4. **Rejected count** — number of one-line rejections recorded. +5. **One-paragraph digest** — for the orchestrator's pre-render summary only. NOT a substitute for `ideation.md` content. The orchestrator will render the Candidates + Selection verbatim per PC-IDEATION floor. + +If you must stop because of an uncertain viability-flipping constraint, return ONLY `NEEDS_USER_CLARIFICATION:` (one line) and do not write `ideation.md`. +**On `task=plan`** — your return MUST include: 1. **`plan.md` path** — absolute or repo-relative path to the file you wrote. 2. **Section anchors** — list every required section header you wrote (`## Goal`, `## Problem Statement`, `## Context`, `## Files To Modify`, `## Steps`, `## Assumptions`, `## Failure Modes`, `## Pre-Mortem & Falsification Signals`, `## Success Criteria`, `## Verification Strategy`, `## Complexity Budget`). Confirm presence — missing sections block the orchestrator. 3. **One-paragraph digest** — for the orchestrator's pre-render summary only. NOT a substitute for plan.md content. The orchestrator will render plan.md verbatim per PC-PLAN floor (Steps, Success Criteria, Verification Strategy, Failure Modes, Assumptions are the verbatim floor; longer prose sections may be condensed only if the floor renders in full). -4. **Same fields for the EXPLORE→PLAN handoff (PC-EXPLORE) if you also produced the findings digest** — index, key constraints, exploration confidence, synthesis paragraph. -The orchestrator will NOT paraphrase plan.md. Your job is to produce a complete plan.md whose verbatim content is itself the user-visible artifact. +The orchestrator will NOT paraphrase the artifacts. Your job is to produce a complete `ideation.md` (phase a) and `plan.md` (phase b) whose verbatim content is itself the user-visible artifact. diff --git a/src/agents/orchestrator.md b/src/agents/orchestrator.md index e547e06..0719f56 100644 --- a/src/agents/orchestrator.md +++ b/src/agents/orchestrator.md @@ -30,7 +30,7 @@ You handle ALL user interaction — sub-agents are invisible to the user. Sub-agents are invisible. Disk artifacts are persistent memory, not user-facing channels. **Every state transition that requires user input MUST be preceded by the corresponding presentation contract block in the same assistant turn.** Canonical definitions live in `references/file-formats.md` "Presentation Contracts" section. The minimum content for each contract is inlined below at the point of dispatch — follow the inline list, do not paraphrase. -Six contracts: PC-EXPLORE, PC-PLAN, PC-EXECUTE-STEP, PC-EXECUTE-LEASH, PC-REFLECT, PC-PIVOT. +Seven contracts: PC-EXPLORE, PC-IDEATION, PC-PLAN, PC-EXECUTE-STEP, PC-EXECUTE-LEASH, PC-REFLECT, PC-PIVOT. ## Sub-Agent Dispatch Rules @@ -55,6 +55,17 @@ Floor (must always render): items 1 and 2 verbatim. Items 3-4 may be condensed b ### PLAN State +A PLAN cycle has two phases gated by the user: **(a) ideation** — `ip-plan-writer` writes `ideation.md` with ≥3 candidate approaches (or 1 + populated Single-Path Escape Hatch) plus a Selection; orchestrator emits PC-IDEATION; **(b) plan** — after user approves the Selection, `ip-plan-writer` writes `plan.md` based on the chosen candidate; orchestrator emits PC-PLAN. Phase (b) is skipped on redirect (back to phase (a) with new direction). + +**User-Visible Presentation (PC-IDEATION — Candidate Approaches Digest)** +After ip-plan-writer writes `ideation.md` and BEFORE `plan.md` is written, emit a chat block containing, in order: +1. Candidates — for each `### C-N`: name, 1-2 sentence sketch, hard-constraint check (with finding refs), trade-off in **"X at the cost of Y"** form, top risk. Verbatim from `ideation.md`. +2. Selection — picked candidate, criteria, confidence (verbatim). +3. Rejected — one-line rationale per non-picked candidate (verbatim, when ≥2 candidates exist). +4. Single-Path Escape Hatch (only if invoked) — both "Why no alternatives" and "Falsification" rendered verbatim. +5. Explicit prompt: "Approve this direction, redirect to a rejected candidate, or request more options." +Floor: items 1 and 2 verbatim always. Item 3 mandatory when ≥2 candidates exist. + **User-Visible Presentation (PC-PLAN — Plan Presentation)** At PLAN → EXECUTE handoff, BEFORE requesting user approval, emit a chat block containing, in order: 1. Goal (verbatim from plan.md). @@ -72,10 +83,14 @@ Floor (always render verbatim, even on token-cost grounds): Steps, Success Crite **Dispatch** 1. Read all findings/*, decisions.md, plans/LESSONS.md, plans/DECISIONS.md (limit: 600), plans/SYSTEM.md -2. Spawn ip-plan-writer with goal + findings summary -3. Read its plan.md output (path + section anchors returned by sub-agent), verify all required sections exist -4. Emit PC-PLAN block (render plan.md verbatim per floor). Wait for explicit user approval. -5. If rejected: relay feedback, re-spawn plan-writer, re-emit PC-PLAN +2. **Phase a (ideation)** — Spawn ip-plan-writer with `task=ideation` + goal + findings summary. Writer reads findings, scans constraints for viability-flipping uncertainty (signals `NEEDS_USER_CLARIFICATION:` if found — surface to user, update findings.md with [CORRECTED iter-N] if applicable, re-spawn writer), writes `ideation.md` only (≥3 candidates with full fields + Selection + Rejected one-liners, or 1 + populated escape hatch), returns the PC-IDEATION digest. +3. Verify `ideation.md` exists and passes the Ideation Gate (≥3 candidates or populated escape hatch + Selection). Emit **PC-IDEATION** block (render candidates + Selection verbatim per floor). Wait for user input. +4. **On user redirect** (pick a rejected candidate / request more options) — re-spawn ip-plan-writer with `task=ideation` + redirect directive; re-emit PC-IDEATION. +5. **On user approval** — proceed to phase b. +6. **Phase b (plan)** — Spawn ip-plan-writer with `task=plan` + selected candidate from `ideation.md`. Writer reads `ideation.md` Selection and writes `plan.md`. +7. Read its plan.md output (path + section anchors returned by sub-agent), verify all required sections exist. +8. Emit **PC-PLAN** block (render plan.md verbatim per floor). Wait for explicit user approval. +9. If plan rejected: relay feedback, re-spawn plan-writer for phase b only (ideation is already settled), re-emit PC-PLAN. ### EXECUTE State diff --git a/src/references/file-formats.md b/src/references/file-formats.md index 5541a5e..306a596 100644 --- a/src/references/file-formats.md +++ b/src/references/file-formats.md @@ -336,6 +336,61 @@ authenticate! → SessionStore#find (line 45) → RedisStore#get (line 12) → R - Upgrading rack-session requires Rails 7.1+ (currently on 7.0.4) ``` +## ideation.md + +Written at end of EXPLORE, before transition to PLAN. Materializes the Solutions Exploration Confidence dimension as a written artifact: candidate approaches, the chosen one, and one-line rationales for the rejected ones. Read during PLAN gate check and RE-PLAN ghost-constraint scan. + +**Required**: ≥3 candidates with full fields, OR 1 candidate plus a populated Single-Path Escape Hatch section. Plus a Selection. Validator enforces this when state ≥ PLAN. + +```markdown +# Ideation +*Candidate approaches considered before locking the plan. Written at end of EXPLORE.* + +## Candidates + +### C-1 | Stateless tokens with cookie fallback +- **Sketch**: Issue JWTs on login. Validate stateless on every request. Keep cookie path for legacy clients during migration. +- **Hard-constraint check**: Satisfies Redis-availability invariant (findings/auth-system.md). Compatible with rack-session pin (findings/dependencies.md). +- **Trade-off**: Stateless validation and zero storage growth **at the cost of** maintaining two auth paths during migration. +- **Top risk**: Cookie fallback path picks up undocumented SSO coupling (findings/auth-system.md L67). + +### C-2 | Dual-write with gradual cutover +- **Sketch**: Write sessions to both old and new stores. Read from new, fall back to old. Cut over once all sessions rotate. +- **Hard-constraint check**: Satisfies zero-downtime invariant. Violates implicit "no storage spike" preference (30-day TTLs double Redis). +- **Trade-off**: Safe rollback **at the cost of** doubled Redis memory for the TTL window. +- **Top risk**: Memory budget breached before TTL window closes. + +### C-3 | In-place format migration +- **Sketch**: Rewrite the serializer to emit the new format. Backfill on read. +- **Hard-constraint check**: Violates SessionSerializer-shared-with-API constraint (findings/auth-system.md L34). +- **Trade-off**: Single code path **at the cost of** changing API auth simultaneously. +- **Top risk**: Coupled changes to API auth surface; blast radius unknown. + +## Selection +- **Picked**: C-1 +- **Criteria**: Smallest blast radius; eliminates storage growth; compatible with rack-session pin without Rails upgrade. +- **Confidence**: medium — SSO coupling on fallback path is the open risk; bounded by step-3 scope. + +## Rejected +- **C-2**: 30-day TTLs make dual-write storage cost prohibitive — confirmed by past plan D-002, D-003 (see plans/DECISIONS.md). +- **C-3**: SessionSerializer is shared with API auth (findings/auth-system.md) — too wide a blast radius for this iteration. + +## Single-Path Escape Hatch (use only if applicable) +*Use this section only when no design alternatives exist (e.g., mechanical rename, deterministic migration). Otherwise leave the heading absent or write "N/A".* + +- **Why no alternatives**: +- **Falsification**: +``` + +**Rules**: +- **Candidate count**: ≥3 with full fields OR 1 + Single-Path Escape Hatch. Two candidates means you stopped one short — keep going or invoke the escape hatch. +- **Hard-constraint check** must reference findings (file path or `findings/.md`) — not vibes. +- **Trade-off** must use the "X **at the cost of** Y" form (same as decisions.md). +- **Selection criteria** explain *why this won*, not just *what it is*. The chosen candidate becomes D-001 in `decisions.md` during PLAN. +- **Rejected one-liners** are short on purpose — they feed RE-PLAN's ghost-constraint scan. If the constraint that rejected C-X turns out to be a ghost, RE-PLAN can resurrect it. +- **Reactivation marker**: if RE-PLAN promotes a rejected candidate, add `[REACTIVATED iter-N]` next to its Rejected entry and write a new Selection (don't rewrite the original — append). +- **Not consolidated**: ideation.md stays plan-local. It is not merged into `plans/FINDINGS.md` or `plans/DECISIONS.md` on close. + ## progress.md Flat checklist. Updated in: PLAN (populate Remaining), EXECUTE (move items), REFLECT (mark failed/blocked), PIVOT (annotate pivot). @@ -816,6 +871,18 @@ Agent files (`agents/orchestrator.md` and contributing sub-agent files) inline t - **Fidelity**: digest. Index and constraints rendered verbatim from disk; synthesis is the orchestrator's prose. - **Minimum sections** (floor): items 1 and 2 (index + constraints) MUST render. Items 3-4 may be condensed but must appear. +### PC-IDEATION — Candidate Approaches Digest + +- **When emitted**: during PLAN, after `ideation.md` is written by `ip-plan-writer` and **before** `plan.md` is generated. The user approves the chosen candidate (or redirects to a rejected one / requests more options) before the plan is built around it. +- **Required content** (in order): + 1. Candidates — for each `### C-N` from `ideation.md`: name, 1-2 sentence sketch, hard-constraint check (file references), trade-off in **"X at the cost of Y"** form, top risk. Verbatim from disk. + 2. Selection — picked candidate, criteria, confidence (verbatim from `ideation.md` Selection). + 3. Rejected — one-line rationale per non-picked candidate (verbatim). + 4. Single-Path Escape Hatch (only if invoked) — both "Why no alternatives" and "Falsification" rendered verbatim. + 5. Explicit prompt: "Approve this direction, redirect to a rejected candidate, or request more options." +- **Fidelity**: verbatim for items 1-4. Item 5 is the orchestrator's prompt. +- **Minimum sections** (floor): items 1 and 2 verbatim always. Item 3 mandatory whenever ≥2 candidates exist. Item 4 only when escape hatch is invoked. Item 5 always. + ### PC-PLAN — Plan Presentation - **When emitted**: at PLAN → EXECUTE handoff, before requesting user approval. @@ -885,7 +952,7 @@ Agent files (`agents/orchestrator.md` and contributing sub-agent files) inline t ### Cross-references - `agents/orchestrator.md` — inlines the minimum-content list of each contract at the point of dispatch. -- `agents/ip-plan-writer.md` — Output Format references PC-PLAN and PC-EXPLORE. +- `agents/ip-plan-writer.md` — Output Format references PC-IDEATION (ideation phase) and PC-PLAN (plan phase). Writer is invoked twice during a PLAN cycle: first for `ideation.md`, then for `plan.md` after user approves the Selection. - `agents/ip-verifier.md` — Relay Contract references PC-REFLECT item 3. - `agents/ip-reviewer.md` — Relay Contract references PC-REFLECT item 4. - `agents/ip-executor.md` — Output Format references PC-EXECUTE-STEP and PC-EXECUTE-LEASH. diff --git a/src/references/planning-rigor.md b/src/references/planning-rigor.md index 1682548..7593677 100644 --- a/src/references/planning-rigor.md +++ b/src/references/planning-rigor.md @@ -2,6 +2,22 @@ Techniques for stronger plans: surface assumptions, anticipate failure, and calibrate confidence. Domain-agnostic — applies to code, research, strategy, operations, and any structured problem-solving. +## Ideation Discipline + +Divergence before convergence. The most common failure mode at the EXPLORE → PLAN seam is "first idea wins by default" — the first viable approach gets locked in without a second one ever being articulated, and any trade-offs are invisible because there's nothing to trade against. + +The **Ideation Gate** (CORE step inside PLAN, owned by `ip-plan-writer`; see SKILL.md PLAN section) materializes the Solutions Exploration Confidence dimension as a written artifact in `ideation.md`. Required: ≥3 candidates with trade-offs, OR 1 candidate plus a populated Single-Path Escape Hatch (both "Why no alternatives" AND "Falsification"). Plus a Selection. Validated by `validate-plan.mjs` whenever state ≥ PLAN. Surfaced to the user via **PC-IDEATION** before `plan.md` is written — the user can approve, redirect to a rejected candidate, or request more options. + +**Why three, not two.** Two candidates degenerates into "the obvious choice and a strawman." A third candidate forces genuine search — what if you couldn't pick either of the first two? The third option is where ghost constraints get exposed. + +**Why a written artifact.** The Solutions Exploration Confidence dimension (below) used to be a self-assessment in the transition log. Self-assessments are vibes. `ideation.md` is evidence: a future PIVOT can read it, see what was rejected and why, and resurrect a candidate if its rejection constraint turns out to be a ghost (mark with `[REACTIVATED iter-N]`). + +**Failure mode it prevents**: locking the foundational approach on iter-1 unexamined, then layering fixes on top of it for 4 iterations until the Nuclear Option triggers. + +**Single-Path Escape Hatch**: not every task has design alternatives. Mechanical renames, deterministic migrations, idempotent backfills — these are genuinely single-path. The escape hatch lets you skip the 3-candidate requirement, but you must populate *both* fields: "Why no alternatives" (e.g., "Mechanical rename of `getCwd` across 15 files; no design surface") and "Falsification" — one observable trigger that would invalidate the single-path assumption (e.g., "Any caller depends on the old name as a string identifier"). The trigger is checked during EXECUTE — if it fires, you've discovered the design surface you missed. Validator rejects partial escape hatch (only one field populated) with a precise message. + +**Viability-flipping constraints — ask, don't guess.** A constraint is *viability-flipping* if reclassifying it (hard ↔ soft ↔ ghost) would change which candidates are viable. Before generating candidates, `ip-plan-writer` scans the classified constraints in `findings.md`. For each viability-flipping constraint that's uncertain, signal `NEEDS_USER_CLARIFICATION:` to the orchestrator — the orchestrator asks the user, updates `findings.md` (with `[CORRECTED iter-N]` if applicable), and re-spawns the writer. Example: in an auth migration, "is operational simplicity a hard constraint, or just a preference?" determines whether dual-write (two auth paths during migration) is viable. Guessing wrong here doesn't just produce a worse candidate — it produces a candidate set that doesn't include the right answer at all. Same rule if `ip-plan-writer` discovers mid-generation that a constraint was misclassified. + ## Assumption Tracking Plans depend on assumptions discovered during EXPLORE. Make them explicit so when one breaks, you know which steps are invalidated. @@ -70,17 +86,17 @@ Before transitioning to PLAN, self-assess in the EXPLORE → PLAN transition log | Dimension | Levels | |-----------|--------| | **Problem scope** | shallow (key mechanics unclear) / adequate (can state problem, constraints, edge cases) / deep (traced causal chains, know internal dynamics) | -| **Solution space** | narrow (one obvious approach) / open (multiple approaches identified) / constrained (few options, hard limits) | +| **Solution space** | Materialized in `ideation.md` (see Ideation Discipline above and SKILL.md PLAN Ideation Gate). "Adequate" = ≥3 candidates with documented trade-offs, OR 1 candidate + populated Single-Path Escape Hatch, AND a Selection. | | **Risk visibility** | blind (unknown unknowns) / partial (some risks identified) / clear (risks mapped, unknowns located) | -**Gate**: All three must be at least "adequate" to transition. Any "shallow" or "blind" → keep exploring. This is a mental check recorded in the transition log, not a separate file section. +**Gate**: Problem scope and Risk visibility must be at least "adequate"; Solution space is enforced by the Ideation Gate (validated by `validate-plan.mjs`). Any "shallow" or "blind" on the other two → keep exploring. Record scope and risk levels in the transition log; ideation.md is the artifact for solutions. **Calibration cues by dimension**: | Dimension | "Adequate" feels like... | "Shallow/Blind" feels like... | |-----------|-------------------------|-------------------------------| | Scope | You can explain the problem to someone unfamiliar and answer their follow-ups | You'd struggle to explain why the problem exists or what constraints matter | -| Solutions | You can name at least two viable approaches and articulate trade-offs | You have one idea and haven't considered alternatives | +| Solutions | `ideation.md` has ≥3 candidates with trade-offs (or 1 + escape hatch) and a Selection — written, not just imagined | You have one idea, haven't written candidates down, or stopped at two candidates ("the real one and a strawman") | | Risks | You can list what could go wrong and where uncertainty clusters | You feel confident but can't name specific risks — that's the danger signal | ## Prediction Accuracy @@ -152,6 +168,7 @@ Ghost constraints = past constraints baked into the current approach that no lon 1. **Is the constraint that led to the failed approach still valid?** Example: "We assumed we couldn't change the vendor because of a contract — but the contract was renegotiated last quarter." 2. **Are we inheriting environmental constraints that are actually preferences?** Example: "Everyone uses tool X, so we assumed we must use tool X — but the actual requirement is 'reliable data processing,' not a specific tool." 3. **Did an early finding become a ghost?** Re-check findings from early EXPLORE against current understanding. Early findings are most likely to become stale. +4. **Re-read `ideation.md` Rejected list.** Each one-line rejection cites a constraint. If any of those constraints are now ghosts, the rejected candidate may be viable. Mark the entry `[REACTIVATED iter-N]` in `ideation.md` and append a new Selection — don't rewrite the original rationale. **Ghost constraint indicators**: - "We've always done it this way" without a traceable reason diff --git a/src/scripts/bootstrap.mjs b/src/scripts/bootstrap.mjs index d3a52e9..3ad005a 100644 --- a/src/scripts/bootstrap.mjs +++ b/src/scripts/bootstrap.mjs @@ -446,6 +446,34 @@ ${crossPlanNote} ` ); + writeFileSync( + join(planDir, "ideation.md"), + `# Ideation +*Candidate approaches considered before locking the plan. Written at end of EXPLORE — before transition to PLAN.* + +## Candidates +*To be populated at end of EXPLORE. Required: ≥3 candidates with full fields, OR 1 candidate plus a populated Single-Path Escape Hatch section below.* + +*Each candidate format:* +*### C-N | * +*- **Sketch**: <2-3 sentences>* +*- **Hard-constraint check**: * +*- **Trade-off**: at the cost of * +*- **Top risk**: * + +## Selection +*To be populated at end of EXPLORE. Picked candidate, criteria used, confidence.* + +## Rejected +*One-line rejection reason per non-picked candidate. These feed RE-PLAN's ghost-constraint scan.* + +## Single-Path Escape Hatch (use only if applicable) +*Use only when no design alternatives exist (mechanical rename, deterministic migration). Otherwise leave empty or write "N/A".* +- **Why no alternatives**: - +- **Falsification**: - +` + ); + writeFileSync( join(planDir, "progress.md"), `# Progress diff --git a/src/scripts/bootstrap.test.mjs b/src/scripts/bootstrap.test.mjs index 54bf938..09d8c7e 100644 --- a/src/scripts/bootstrap.test.mjs +++ b/src/scripts/bootstrap.test.mjs @@ -136,7 +136,7 @@ describe("bootstrap.mjs", () => { // All expected files exist const base = join(dir, "plans", planDir); - for (const f of ["state.md", "plan.md", "decisions.md", "findings.md", "progress.md", "verification.md", "changelog.md"]) { + for (const f of ["state.md", "plan.md", "decisions.md", "findings.md", "ideation.md", "progress.md", "verification.md", "changelog.md"]) { assert.ok(existsSync(join(base, f)), `${f} should exist`); } const changelog = readFileSync(join(base, "changelog.md"), "utf-8"); @@ -1523,6 +1523,11 @@ describe("bootstrap.mjs", () => { const dir = getTempDir(); run(dir, "new", "section test"); const planDir = getPointer(dir); + // Populate ideation.md so the ideation gate doesn't ERROR — we want to isolate plan.md placeholder warnings + writeFileSync( + join(dir, "plans", planDir, "ideation.md"), + "# Ideation\n\n## Candidates\n\n### C-1 | A\n- **Sketch**: a\n\n### C-2 | B\n- **Sketch**: b\n\n### C-3 | C\n- **Sketch**: c\n\n## Selection\n- **Picked**: C-1\n" + ); // Set state to EXECUTE const statePath = join(dir, "plans", planDir, "state.md"); const state = readFileSync(statePath, "utf-8"); @@ -1905,4 +1910,205 @@ describe("bootstrap.mjs", () => { assert.ok(r.stdout.includes("LESSONS.md"), "new output should mention LESSONS.md"); }); }); + + describe("ideation.md", () => { + const VALIDATE = resolve(import.meta.dirname, "validate-plan.mjs"); + + function runValidate(cwd, ...args) { + try { + const result = execFileSync("node", [VALIDATE, ...args], { + cwd, + encoding: "utf-8", + timeout: 15000, + stdio: ["pipe", "pipe", "pipe"], + }); + return { stdout: result, stderr: "", exitCode: 0 }; + } catch (err) { + return { stdout: err.stdout || "", stderr: err.stderr || "", exitCode: err.status ?? 1 }; + } + } + + function setState(cwd, planDir, newState) { + const statePath = join(cwd, "plans", planDir, "state.md"); + const state = readFileSync(statePath, "utf-8"); + writeFileSync(statePath, state.replace(/# Current State: \w+/, `# Current State: ${newState}`)); + } + + it("bootstrap creates ideation.md with expected sections", () => { + const dir = getTempDir(); + run(dir, "new", "ideation create test"); + const planDir = getPointer(dir); + const ideation = readPlanFile(dir, planDir, "ideation.md"); + assert.ok(ideation.includes("# Ideation"), "should have H1"); + assert.ok(ideation.includes("## Candidates"), "should have Candidates section"); + assert.ok(ideation.includes("## Selection"), "should have Selection section"); + assert.ok(ideation.includes("## Rejected"), "should have Rejected section"); + assert.ok(ideation.includes("Single-Path Escape Hatch"), "should have escape hatch section"); + }); + + it("validator does not enforce ideation in EXPLORE", () => { + const dir = getTempDir(); + run(dir, "new", "explore-only test"); + // State stays EXPLORE; ideation.md is the bootstrap stub + const r = runValidate(dir); + assert.ok(!r.stdout.includes("ideation"), "should not flag ideation in EXPLORE"); + assert.equal(r.exitCode, 0); + }); + + it("validator ERRORs at PLAN when ideation.md missing", () => { + const dir = getTempDir(); + run(dir, "new", "missing ideation test"); + const planDir = getPointer(dir); + // Remove ideation.md and bump state to PLAN + rmSync(join(dir, "plans", planDir, "ideation.md")); + setState(dir, planDir, "PLAN"); + const r = runValidate(dir); + assert.equal(r.exitCode, 1, "should fail when ideation.md is missing at PLAN"); + assert.ok(r.stdout.includes("ideation.md missing"), "should report missing ideation"); + }); + + it("validator ERRORs at PLAN when Candidates section is missing", () => { + const dir = getTempDir(); + run(dir, "new", "no candidates test"); + const planDir = getPointer(dir); + writeFileSync( + join(dir, "plans", planDir, "ideation.md"), + "# Ideation\n\n## Selection\nC-1\n" + ); + setState(dir, planDir, "PLAN"); + const r = runValidate(dir); + assert.equal(r.exitCode, 1); + assert.ok(r.stdout.includes("Candidates"), "should report missing Candidates section"); + }); + + it("validator ERRORs at PLAN when fewer than 3 candidates and no escape hatch", () => { + const dir = getTempDir(); + run(dir, "new", "few candidates test"); + const planDir = getPointer(dir); + writeFileSync( + join(dir, "plans", planDir, "ideation.md"), + "# Ideation\n\n## Candidates\n\n### C-1 | Approach A\n- **Sketch**: a\n\n### C-2 | Approach B\n- **Sketch**: b\n\n## Selection\n- **Picked**: C-1\n" + ); + setState(dir, planDir, "PLAN"); + const r = runValidate(dir); + assert.equal(r.exitCode, 1); + assert.ok(r.stdout.includes("2 candidate"), "should report only 2 candidates"); + }); + + it("validator passes at PLAN with 3 candidates and a Selection", () => { + const dir = getTempDir(); + run(dir, "new", "three candidates test"); + const planDir = getPointer(dir); + writeFileSync( + join(dir, "plans", planDir, "ideation.md"), + "# Ideation\n\n## Candidates\n\n### C-1 | Approach A\n- **Sketch**: a\n\n### C-2 | Approach B\n- **Sketch**: b\n\n### C-3 | Approach C\n- **Sketch**: c\n\n## Selection\n- **Picked**: C-1\n- **Criteria**: simplest\n- **Confidence**: medium\n" + ); + // Add 3 findings so the findings gate is satisfied too + writeFileSync( + join(dir, "plans", planDir, "findings.md"), + "# Findings\n\n## Index\n- A\n- B\n- C\n\n## Key Constraints\n- none\n" + ); + setState(dir, planDir, "PLAN"); + const r = runValidate(dir); + assert.ok(!r.stdout.includes("ideation"), `should not flag ideation: ${r.stdout}`); + }); + + it("validator passes at PLAN with 1 candidate and populated escape hatch", () => { + const dir = getTempDir(); + run(dir, "new", "escape hatch test"); + const planDir = getPointer(dir); + writeFileSync( + join(dir, "plans", planDir, "ideation.md"), + "# Ideation\n\n## Candidates\n\n### C-1 | Mechanical rename\n- **Sketch**: rename across files\n\n## Selection\n- **Picked**: C-1\n- **Confidence**: high\n\n## Single-Path Escape Hatch (use only if applicable)\n- **Why no alternatives**: Mechanical rename across 15 files; no design surface.\n- **Falsification**: Any caller treats the old name as a string identifier.\n" + ); + writeFileSync( + join(dir, "plans", planDir, "findings.md"), + "# Findings\n\n## Index\n- A\n- B\n- C\n\n## Key Constraints\n- none\n" + ); + setState(dir, planDir, "PLAN"); + const r = runValidate(dir); + assert.ok(!r.stdout.includes("ideation"), `should not flag ideation when escape hatch is populated: ${r.stdout}`); + }); + + it("validator ERRORs at PLAN when ideation.md is empty", () => { + const dir = getTempDir(); + run(dir, "new", "empty ideation test"); + const planDir = getPointer(dir); + writeFileSync(join(dir, "plans", planDir, "ideation.md"), ""); + setState(dir, planDir, "PLAN"); + const r = runValidate(dir); + assert.equal(r.exitCode, 1, "empty ideation.md must not silently pass"); + assert.ok(r.stdout.includes("empty or unreadable"), "should report empty/unreadable ideation"); + }); + + it("validator ERRORs at PLAN when escape hatch has only Why, no Falsification", () => { + const dir = getTempDir(); + run(dir, "new", "partial escape test"); + const planDir = getPointer(dir); + writeFileSync( + join(dir, "plans", planDir, "ideation.md"), + "# Ideation\n\n## Candidates\n\n### C-1 | Mechanical rename\n- **Sketch**: rename across files\n\n## Selection\n- **Picked**: C-1\n\n## Single-Path Escape Hatch (use only if applicable)\n- **Why no alternatives**: Mechanical rename across 15 files; no design surface.\n- **Falsification**: -\n" + ); + writeFileSync( + join(dir, "plans", planDir, "findings.md"), + "# Findings\n\n## Index\n- A\n- B\n- C\n\n## Key Constraints\n- none\n" + ); + setState(dir, planDir, "PLAN"); + const r = runValidate(dir); + assert.equal(r.exitCode, 1, "escape hatch without Falsification must not pass"); + assert.ok(r.stdout.includes("Falsification"), `should report missing Falsification: ${r.stdout}`); + }); + + it("validator ERRORs at PLAN when escape hatch has only Falsification, no Why", () => { + const dir = getTempDir(); + run(dir, "new", "partial escape test 2"); + const planDir = getPointer(dir); + writeFileSync( + join(dir, "plans", planDir, "ideation.md"), + "# Ideation\n\n## Candidates\n\n### C-1 | Mechanical rename\n- **Sketch**: rename across files\n\n## Selection\n- **Picked**: C-1\n\n## Single-Path Escape Hatch (use only if applicable)\n- **Why no alternatives**: -\n- **Falsification**: Caller treats the old name as a string identifier.\n" + ); + writeFileSync( + join(dir, "plans", planDir, "findings.md"), + "# Findings\n\n## Index\n- A\n- B\n- C\n\n## Key Constraints\n- none\n" + ); + setState(dir, planDir, "PLAN"); + const r = runValidate(dir); + assert.equal(r.exitCode, 1, "escape hatch without Why no alternatives must not pass"); + assert.ok(r.stdout.includes("Why no alternatives"), `should report missing Why no alternatives: ${r.stdout}`); + }); + + it("validator ERRORs when Selection section is empty/placeholder", () => { + const dir = getTempDir(); + run(dir, "new", "empty selection test"); + const planDir = getPointer(dir); + // Bootstrap stub has placeholder text in Selection + setState(dir, planDir, "PLAN"); + const r = runValidate(dir); + assert.equal(r.exitCode, 1); + assert.ok(r.stdout.includes("Selection"), "should report Selection issue"); + }); + + it("close does not error when ideation.md is present", () => { + const dir = getTempDir(); + run(dir, "new", "close ideation test"); + const r = run(dir, "close"); + assert.equal(r.exitCode, 0, "close should succeed with ideation.md present"); + }); + + it("ideation.md is NOT merged into consolidated files on close", () => { + const dir = getTempDir(); + run(dir, "new", "no merge test"); + const planDir = getPointer(dir); + writeFileSync( + join(dir, "plans", planDir, "ideation.md"), + "# Ideation\n\n## Candidates\n\n### C-1 | Distinctive ideation marker XYZZY\n- **Sketch**: a\n\n## Selection\n- **Picked**: C-1\n" + ); + run(dir, "close"); + // Consolidated FINDINGS.md and DECISIONS.md should not contain the ideation content + const findings = readFileSync(join(dir, "plans", "FINDINGS.md"), "utf-8"); + const decisions = readFileSync(join(dir, "plans", "DECISIONS.md"), "utf-8"); + assert.ok(!findings.includes("XYZZY"), "ideation should not be merged into FINDINGS.md"); + assert.ok(!decisions.includes("XYZZY"), "ideation should not be merged into DECISIONS.md"); + }); + }); }); diff --git a/src/scripts/validate-plan.mjs b/src/scripts/validate-plan.mjs index cec4102..2a7d45e 100644 --- a/src/scripts/validate-plan.mjs +++ b/src/scripts/validate-plan.mjs @@ -241,6 +241,83 @@ function checkFindings(planDir, issues) { } } +function checkIdeation(planDir, issues) { + const state = readFile(join(planDir, "state.md")); + const currentState = (extractField(state, /^# Current State:\s*(.+)$/m) || "").toUpperCase(); + // Only enforce when state has advanced past EXPLORE — the gate fires at EXPLORE → PLAN. + // Skip on CLOSE: closed plans created before this protocol version may legitimately lack ideation.md. + const enforce = ["PLAN", "EXECUTE", "REFLECT", "RE-PLAN"].includes(currentState); + if (!enforce) return; + + const ideationPath = join(planDir, "ideation.md"); + if (!existsSync(ideationPath)) { + issues.push({ severity: "ERROR", check: "ideation", message: "ideation.md missing — required by EXPLORE → PLAN gate (run EXPLORE Ideation step before transitioning)" }); + return; + } + + const ideation = readFile(ideationPath); + if (!ideation) { + issues.push({ severity: "ERROR", check: "ideation", message: "ideation.md is empty or unreadable — run EXPLORE Ideation step before transitioning" }); + return; + } + + const candidatesSection = extractSection(ideation, "Candidates"); + const selectionSection = extractSection(ideation, "Selection"); + const escapeSection = extractSection(ideation, "Single-Path Escape Hatch (use only if applicable)"); + + if (candidatesSection === null) { + issues.push({ severity: "ERROR", check: "ideation", message: "ideation.md missing ## Candidates section" }); + } + if (selectionSection === null) { + issues.push({ severity: "ERROR", check: "ideation", message: "ideation.md missing ## Selection section" }); + } else if (isPlaceholder(selectionSection)) { + issues.push({ severity: "ERROR", check: "ideation", message: "ideation.md ## Selection section is empty/placeholder — pick a candidate before transitioning to PLAN" }); + } + + // Count ### C-N candidate headings inside the Candidates section + let candidateCount = 0; + if (candidatesSection) { + const matches = candidatesSection.match(/^### C-\d+/gm); + candidateCount = matches ? matches.length : 0; + } + + // Escape hatch is "populated" only when BOTH "Why no alternatives" and "Falsification" have non-placeholder content. + // The protocol requires both: a single-path claim without a falsification trigger has no STOP signal during EXECUTE. + const whyPopulated = escapeSection + ? /-\s*\*\*Why no alternatives\*\*:\s*\S(?!\s*$)/m.test(escapeSection) + && !/-\s*\*\*Why no alternatives\*\*:\s*-\s*$/m.test(escapeSection) + : false; + const falsificationPopulated = escapeSection + ? /-\s*\*\*Falsification\*\*:\s*\S(?!\s*$)/m.test(escapeSection) + && !/-\s*\*\*Falsification\*\*:\s*-\s*$/m.test(escapeSection) + : false; + const escapeHatchPopulated = !!( + escapeSection && + !isPlaceholder(escapeSection) && + !/^N\/A$/im.test(escapeSection.trim()) && + whyPopulated && + falsificationPopulated + ); + + if (candidateCount < 3 && !escapeHatchPopulated) { + if (escapeSection && (whyPopulated || falsificationPopulated) && !(whyPopulated && falsificationPopulated)) { + // Partial escape hatch — give a precise message instead of the generic count error + const missing = whyPopulated ? "Falsification" : "Why no alternatives"; + issues.push({ + severity: "ERROR", + check: "ideation", + message: `ideation.md Single-Path Escape Hatch is missing "${missing}" — both "Why no alternatives" and "Falsification" must be populated`, + }); + } else { + issues.push({ + severity: "ERROR", + check: "ideation", + message: `ideation.md has ${candidateCount} candidate(s) — minimum 3 required, OR populate Single-Path Escape Hatch with both "Why no alternatives" and "Falsification" populated`, + }); + } + } +} + function checkCrossFileConsistency(planDir, issues) { const state = readFile(join(planDir, "state.md")); const plan = readFile(join(planDir, "plan.md")); @@ -1245,6 +1322,7 @@ function validate(planDirName) { checkStateTransitions(planDir, issues); checkPlanSections(planDir, issues); checkFindings(planDir, issues); + checkIdeation(planDir, issues); checkCrossFileConsistency(planDir, issues); checkChangeManifest(planDir, issues); checkIterationLimits(planDir, issues); @@ -1312,6 +1390,7 @@ Checks: - State transition validity - Mandatory plan.md sections - Findings count (≥3 before PLAN) + - Ideation gate (≥3 candidates or escape hatch + Selection, when state ≥ PLAN) - Cross-file consistency (state/plan/progress/verification) - Change manifest presence during EXECUTE/REFLECT - Iteration limits (5 = decomposition, 6+ = hard stop)