From 20c9ed6b1805a81f657f9b3a3c00d140677f6efd Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 8 May 2026 20:57:33 +0000 Subject: [PATCH 1/3] =?UTF-8?q?Release=20v2.10.0:=20Add=20Ideation=20Gate?= =?UTF-8?q?=20at=20EXPLORE=20=E2=86=92=20PLAN?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Materializes the Solutions Exploration Confidence dimension as a written artifact (ideation.md) instead of a self-assessment. Forces ≥3 candidate approaches with trade-offs (or 1 + Single-Path Escape Hatch) and an explicit Selection before transitioning to PLAN. Rejected candidates feed RE-PLAN's ghost-constraint scan and can be reactivated when their rejection constraint turns out to be ghost. Validator enforces the gate when state ≥ PLAN. 12 new tests, 109 total. https://claude.ai/code/session_01WmjKGk8F6QgjpPDK49aBAn --- CHANGELOG.md | 18 ++++ CLAUDE.md | 6 +- README.md | 6 +- VERSION | 2 +- src/SKILL.md | 22 +++-- src/references/file-formats.md | 55 +++++++++++ src/references/planning-rigor.md | 21 +++- src/scripts/bootstrap.mjs | 28 ++++++ src/scripts/bootstrap.test.mjs | 161 ++++++++++++++++++++++++++++++- src/scripts/validate-plan.mjs | 56 +++++++++++ 10 files changed, 360 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 32ec995..165a4e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,24 @@ 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.10.0] - 2026-05-08 + +### Added +- **Ideation Gate at EXPLORE → PLAN** — new CORE sub-step at the end of EXPLORE materializes the Solutions Exploration Confidence dimension as a written artifact (`ideation.md`) instead of a self-assessment. Required: ≥3 candidate approaches with hard-constraint check, "X at the cost of Y" trade-off, and top risk; plus a Selection (picked candidate, criteria, confidence); plus one-line rejection rationales. Single-Path Escape Hatch covers genuinely single-path tasks (mechanical renames, deterministic migrations) — 1 candidate is permitted if "why no alternatives" + a falsification trigger are populated. +- **`ideation.md` artifact** — new per-plan file. Created by `bootstrap.mjs new` with a stub template. Read during PLAN gate check and RE-PLAN ghost-constraint scan. Not merged into consolidated `plans/FINDINGS.md` or `plans/DECISIONS.md` on close — stays plan-local. +- **`validate-plan.mjs` ideation check** — new check enforces the gate when state ≥ PLAN: `ideation.md` exists, has a `## Candidates` section with ≥3 `### C-N` headings (or a populated Single-Path Escape Hatch), and a non-placeholder `## Selection`. ERRORs on miss; no-op during EXPLORE. +- **`ideation.md` row in File Lifecycle Matrix** — W in EXPLORE, R in PLAN, R+W in RE-PLAN (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, and how the escape hatch prevents friction on single-path tasks. +- **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 at PLAN, passes with 3 candidates, passes with escape hatch, close succeeds, ideation not merged on close. 109 tests total (was 97). + +### Changed +- **EXPLORE → PLAN transition rule** in SKILL.md now requires the Ideation Gate in addition to the ≥3 findings minimum. +- **PLAN gate check** now reads `ideation.md` alongside the other plan files. Insufficient candidates or missing Selection → back to EXPLORE. +- **PLAN decisions.md D-001** is now the carried-forward Selection from `ideation.md` (same Trade-off, with rejected candidates referenced). +- **RE-PLAN ghost-constraint scan** now includes a 4th question: re-read `ideation.md` Rejected list for candidates rejected against constraints that became ghosts. +- **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 RE-PLAN" row. + ## [2.9.0] - 2026-03-06 ### Fixed diff --git a/CLAUDE.md b/CLAUDE.md index 6db81cb..6c9eb7c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -23,7 +23,7 @@ iterative-planner/ ├── SKILL.md # Core protocol (state machine, rules) - the main instruction set ├── scripts/ │ ├── bootstrap.mjs # Initializes plans/plan_YYYY-MM-DD_XXXXXXXX/ directory (Node.js 18+) - │ ├── bootstrap.test.mjs # Test suite (node:test, 97 tests) + │ ├── bootstrap.test.mjs # Test suite (node:test, 109 tests) │ └── validate-plan.mjs # Protocol compliance validator (Node.js 18+) └── references/ # Knowledge base documents ├── complexity-control.md # Anti-complexity protocol (revert-first, 3-strike, nuclear option) @@ -65,7 +65,8 @@ Complete spec in **src/SKILL.md**. Key sections: - **Complexity Control**: src/SKILL.md "Complexity Control" section + `src/references/complexity-control.md` (6 Simplification Checks including essential vs accidental complexity) - **Code Hygiene**: src/SKILL.md "Code Hygiene" section + `src/references/code-hygiene.md` - **Decision Anchoring**: src/SKILL.md "Decision Anchoring" section + `src/references/decision-anchoring.md` -- **Planning Rigor**: src/SKILL.md PLAN/EXPLORE/REFLECT/RE-PLAN sections + `src/references/planning-rigor.md` (assumptions, pre-mortem, falsification signals, exploration confidence, prediction accuracy, ghost constraints, decomposition) +- **Planning Rigor**: src/SKILL.md PLAN/EXPLORE/REFLECT/RE-PLAN sections + `src/references/planning-rigor.md` (ideation discipline, assumptions, pre-mortem, falsification signals, exploration confidence, prediction accuracy, ghost constraints, decomposition) +- **Ideation Gate**: src/SKILL.md EXPLORE Ideation Gate sub-step + `src/references/file-formats.md` (`ideation.md` template) + `src/references/planning-rigor.md` Ideation Discipline section - **Git Integration**: src/SKILL.md "Git Integration" section Do not duplicate protocol content here. Read src/SKILL.md directly. @@ -138,6 +139,7 @@ make help # Show available targets - [ ] `plans/INDEX.md` created by bootstrap and updated on close - [ ] `lessons_snapshot.md` created in plan directory on close - [ ] `src/scripts/validate-plan.mjs` passes syntax check +- [ ] `ideation.md` referenced in SKILL.md (EXPLORE Ideation Gate, PLAN gate check, RE-PLAN ghost-constraint scan, File Lifecycle Matrix, Filesystem Structure tree, Mandatory Re-reads), templated in `src/references/file-formats.md`, created by `bootstrap.mjs`, and enforced by `validate-plan.mjs` at state ≥ PLAN ## Updating Local Skill diff --git a/README.md b/README.md index a1acfa3..7337f4a 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.9.0-green.svg)](CHANGELOG.md) +[![Skill](https://img.shields.io/badge/Skill-v2.10.0-green.svg)](CHANGELOG.md) [![Sponsored by Electi](https://img.shields.io/badge/Sponsored%20by-Electi-red.svg)](https://www.electiconsulting.com) **Complex tasks break AI agents. This skill fixes that.** @@ -85,6 +85,7 @@ plans/ ├── decisions.md # Append-only log of every decision and pivot ├── findings.md # Index of discoveries (corrected when wrong) ├── findings/ # Detailed research files + ├── ideation.md # Candidate approaches + selection (EXPLORE → PLAN gate) ├── progress.md # Done vs remaining ├── verification.md # Verification results per REFLECT cycle ├── checkpoints/ # Snapshots before risky changes @@ -139,7 +140,8 @@ Each state embeds domain-agnostic thinking tools: | Framework | State | What it does | |-----------|-------|-------------| | **Constraint classification** | EXPLORE | Tag every constraint as *hard*, *soft*, or *ghost* (no longer applies). Ghost constraints unlock options nobody thought existed. | -| **Exploration confidence** | EXPLORE → PLAN | Self-assess scope, solution space, risk visibility. "Shallow" on any = keep exploring. | +| **Divergent ideation** | EXPLORE → PLAN gate | Generate ≥3 candidate approaches with trade-offs in `ideation.md` before converging. Rejected candidates feed RE-PLAN's ghost-constraint scan and can be reactivated when their rejection constraint turns out to be ghost. | +| **Exploration confidence** | EXPLORE → PLAN | Self-assess scope and risk visibility; solutions are materialized in `ideation.md`. "Shallow"/"blind" = keep exploring. | | **Problem decomposition** | PLAN | Understand the whole, find natural boundaries, minimize dependencies, start with the riskiest part. | | **Assumption tracking** | PLAN | Every assumption traced to a finding, linked to dependent steps. When one breaks, you know what's invalidated. | | **Pre-mortem & falsification** | PLAN | Assume the plan failed -- why? Extract concrete STOP IF triggers. Prevents confirmation bias. | diff --git a/VERSION b/VERSION index c8e38b6..10c2c0c 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.9.0 +2.10.0 diff --git a/src/SKILL.md b/src/SKILL.md index c7715c5..4c99d57 100644 --- a/src/SKILL.md +++ b/src/SKILL.md @@ -46,7 +46,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**: `ideation.md` has ≥3 candidates with trade-offs (or 1 + Single-Path Escape Hatch) AND a Selection. | | 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. | @@ -74,7 +74,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 RE-PLAN | `decisions.md`, `findings.md`, `findings/*`, `plans/LESSONS.md` | Ground plan in known facts + institutional memory | +| Before PLAN or RE-PLAN | `decisions.md`, `findings.md`, `findings/*`, `ideation.md`, `plans/LESSONS.md` | Ground plan in known facts + institutional memory; don't reinvent rejected candidates | | Before any REFLECT | `plan.md` (criteria), `progress.md`, `verification.md` | Compare against written criteria, not vibes | | 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 end of EXPLORE) ├── progress.md # Done vs remaining ├── verification.md # Verification results per REFLECT cycle ├── checkpoints/ # Snapshots before risky changes @@ -135,6 +136,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 | | checkpoints/* | — | — | W | R | R | — | @@ -211,12 +213,20 @@ Institutional memory across plans. Unlike FINDINGS.md and DECISIONS.md which gro - **Soft constraint**: preferences, conventions, team familiarity — negotiable if trade-off is explicit. - **Ghost constraint**: past constraints baked into current approach that **no longer apply**. Finding and removing ghost constraints unlocks options nobody thought were available. Separate constraints from preferences — be honest about which is which. Can't distinguish them → keep exploring. +- **Ideation Gate** *(CORE — applies to iter-1)* — last action of EXPLORE before the transition log. Materializes the Solutions Exploration Confidence dimension as a written artifact. Read then write `ideation.md` per `references/file-formats.md`: + - **≥3 candidate approaches** (`### C-1`, `### C-2`, `### C-3`, …), each with: Sketch (2-3 sentences), Hard-constraint check (refs `findings.md`/`findings/*`), Trade-off in **"X at the cost of Y"** form, Top risk. + - **Selection** — picked candidate, criteria used, confidence (high/medium/low + one-line reason). + - **Rejected** — one-line rationale per non-picked candidate. These feed RE-PLAN's ghost-constraint scan; if a rejection constraint turns out to be ghost, the rejected candidate can be reactivated. + - **Single-Path Escape Hatch** — if the task genuinely has no design alternatives (mechanical rename, deterministic migration), list 1 candidate and populate the escape hatch with: why no alternatives, one falsification trigger that would invalidate the single-path assumption. Two candidates is not enough — either generate a third or invoke the escape hatch. + - **Surface to user**: present the candidate set + selection before the EXPLORE → PLAN transition. The user can redirect to a rejected candidate or push for more options. + - Picked candidate becomes the seed for **D-001** in `decisions.md` during PLAN (the chosen approach + Trade-off, with rejected candidates referenced). + - Validator (`validate-plan.mjs`) ERRORs at PLAN/EXECUTE/REFLECT/RE-PLAN if ideation.md is missing, lacks ≥3 candidates (without escape hatch), or has no Selection. - Use **Task subagents** to parallelize research. All subagent output → `{plan-dir}/findings/` files. Never rely on context-only results. **Main agent** updates `findings.md` index after subagents write — subagents don't touch the index. **Naming**: `findings/{topic-slug}.md` (kebab-case, descriptive — e.g. `auth-system.md`, `test-coverage.md`). - Use "think hard" / "ultrathink" for complex analysis. - REFLECT → EXPLORE loops: append to existing findings, don't overwrite. Mark corrections with `[CORRECTED iter-N]`. ### 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. +- **Gate check**: read `state.md`, `plan.md`, `findings.md`, `findings/*`, `ideation.md`, `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. If `ideation.md` lacks a Selection or has <3 candidates (and no Single-Path Escape Hatch) → go back to EXPLORE Ideation Gate. - **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: @@ -229,7 +239,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. @@ -284,9 +294,9 @@ On **failed step**: skip gate. Follow Autonomy Leash (revert-first, 2 attempts m | Unknowns need investigation, or findings contradicted | → EXPLORE (update findings first) | ### RE-PLAN -- Read `decisions.md`, `findings.md`, relevant `findings/*`, `plans/LESSONS.md`. +- Read `decisions.md`, `findings.md`, relevant `findings/*`, `ideation.md`, `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). 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. - Write `decisions.md`: log pivot + mandatory Complexity Assessment. - Write `state.md` + `progress.md` (mark failed items, note pivot). diff --git a/src/references/file-formats.md b/src/references/file-formats.md index f5965b3..35e990a 100644 --- a/src/references/file-formats.md +++ b/src/references/file-formats.md @@ -239,6 +239,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), RE-PLAN (annotate pivot). diff --git a/src/references/planning-rigor.md b/src/references/planning-rigor.md index 26c199c..d13fa27 100644 --- a/src/references/planning-rigor.md +++ b/src/references/planning-rigor.md @@ -2,6 +2,20 @@ 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 in EXPLORE → PLAN 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** (last action of EXPLORE; see SKILL.md EXPLORE section) materializes the Solutions dimension as a written artifact in `ideation.md`. Required: ≥3 candidates with trade-offs, OR 1 candidate plus a populated Single-Path Escape Hatch. Plus a Selection. Validated by `validate-plan.mjs` whenever state ≥ PLAN. + +**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 RE-PLAN can read it, see what was rejected and why, and resurrect a candidate if its rejection constraint turns out to be a ghost. + +**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 state *why* there are no alternatives and *one falsification 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. + ## Assumption Tracking Plans depend on assumptions discovered during EXPLORE. Make them explicit so when one breaks, you know which steps are invalidated. @@ -70,17 +84,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 EXPLORE 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 @@ -123,6 +137,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 faa55b7..2d042c9 100644 --- a/src/scripts/bootstrap.mjs +++ b/src/scripts/bootstrap.mjs @@ -389,6 +389,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 7fea6b2..1bd5c09 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"]) { + for (const f of ["state.md", "plan.md", "decisions.md", "findings.md", "ideation.md", "progress.md", "verification.md"]) { assert.ok(existsSync(join(base, f)), `${f} should exist`); } // Subdirectories @@ -1393,6 +1393,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"); @@ -1563,4 +1568,158 @@ 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 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 9dc1d3a..857c473 100644 --- a/src/scripts/validate-plan.mjs +++ b/src/scripts/validate-plan.mjs @@ -186,6 +186,60 @@ 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) 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; + } + + // Determine if escape hatch is genuinely populated (not the bootstrap placeholder) + const escapeHatchPopulated = !!( + escapeSection && + !isPlaceholder(escapeSection) && + /-\s*\*\*Why no alternatives\*\*:\s*[^\s-]/.test(escapeSection) && + !/^N\/A$/im.test(escapeSection.trim()) + ); + + if (candidateCount < 3 && !escapeHatchPopulated) { + issues.push({ + severity: "ERROR", + check: "ideation", + message: `ideation.md has ${candidateCount} candidate(s) — minimum 3 required, OR populate Single-Path Escape Hatch with a "Why no alternatives" rationale`, + }); + } +} + function checkCrossFileConsistency(planDir, issues) { const state = readFile(join(planDir, "state.md")); const plan = readFile(join(planDir, "plan.md")); @@ -256,6 +310,7 @@ function validate(planDirName) { checkStateTransitions(planDir, issues); checkPlanSections(planDir, issues); checkFindings(planDir, issues); + checkIdeation(planDir, issues); checkCrossFileConsistency(planDir, issues); checkConsolidatedFiles(issues); @@ -300,6 +355,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) - Consolidated files existence From 708dde4773072980bcb50513bd1e6ec2a7d8a190 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 8 May 2026 21:06:43 +0000 Subject: [PATCH 2/3] Ideation Gate: ask user when constraint classification flips viability MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Before generating candidates, the agent must scan classified constraints for any whose reclassification (hard ↔ soft ↔ ghost) would change which candidates are viable. If classification is uncertain on a viability- flipping constraint, the agent ASKs a focused question instead of guessing. If discovered mid-generation, the agent stops, re-classifies in findings.md, and re-runs the gate (state stays EXPLORE — the gate is an EXPLORE sub-step). This closes the gap where autonomous candidate generation produces a candidate set that doesn't include the right answer because a soft constraint was treated as hard (or vice versa). Process rule, no artifact change — validate-plan.mjs is unchanged. https://claude.ai/code/session_01WmjKGk8F6QgjpPDK49aBAn --- CHANGELOG.md | 1 + README.md | 2 +- src/SKILL.md | 1 + src/references/planning-rigor.md | 2 ++ 4 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 165a4e9..584b1d1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Added - **Ideation Gate at EXPLORE → PLAN** — new CORE sub-step at the end of EXPLORE materializes the Solutions Exploration Confidence dimension as a written artifact (`ideation.md`) instead of a self-assessment. Required: ≥3 candidate approaches with hard-constraint check, "X at the cost of Y" trade-off, and top risk; plus a Selection (picked candidate, criteria, confidence); plus one-line rejection rationales. Single-Path Escape Hatch covers genuinely single-path tasks (mechanical renames, deterministic migrations) — 1 candidate is permitted if "why no alternatives" + a falsification trigger are populated. +- **Viability-flipping constraint rule** — before generating candidates, the agent must scan classified constraints for any whose reclassification (hard ↔ soft ↔ ghost) would flip a candidate's viability. If classification is uncertain → ASK the user a focused question; don't guess. If discovered mid-generation, the agent stops, re-classifies (with `[CORRECTED iter-N]` if applicable), and re-runs the gate. State remains EXPLORE — the gate is an EXPLORE sub-step, not a separate state. - **`ideation.md` artifact** — new per-plan file. Created by `bootstrap.mjs new` with a stub template. Read during PLAN gate check and RE-PLAN ghost-constraint scan. Not merged into consolidated `plans/FINDINGS.md` or `plans/DECISIONS.md` on close — stays plan-local. - **`validate-plan.mjs` ideation check** — new check enforces the gate when state ≥ PLAN: `ideation.md` exists, has a `## Candidates` section with ≥3 `### C-N` headings (or a populated Single-Path Escape Hatch), and a non-placeholder `## Selection`. ERRORs on miss; no-op during EXPLORE. - **`ideation.md` row in File Lifecycle Matrix** — W in EXPLORE, R in PLAN, R+W in RE-PLAN (for `[REACTIVATED iter-N]` markers when a rejected candidate becomes viable after a ghost constraint is found). diff --git a/README.md b/README.md index 7337f4a..c2f1020 100644 --- a/README.md +++ b/README.md @@ -140,7 +140,7 @@ Each state embeds domain-agnostic thinking tools: | Framework | State | What it does | |-----------|-------|-------------| | **Constraint classification** | EXPLORE | Tag every constraint as *hard*, *soft*, or *ghost* (no longer applies). Ghost constraints unlock options nobody thought existed. | -| **Divergent ideation** | EXPLORE → PLAN gate | Generate ≥3 candidate approaches with trade-offs in `ideation.md` before converging. Rejected candidates feed RE-PLAN's ghost-constraint scan and can be reactivated when their rejection constraint turns out to be ghost. | +| **Divergent ideation** | EXPLORE → PLAN gate | Generate ≥3 candidate approaches with trade-offs in `ideation.md` before converging. When a constraint's classification (hard/soft/ghost) would flip a candidate's viability and is uncertain, **ask the user before generating** — don't guess. Rejected candidates feed RE-PLAN's ghost-constraint scan and can be reactivated when their rejection constraint turns out to be ghost. | | **Exploration confidence** | EXPLORE → PLAN | Self-assess scope and risk visibility; solutions are materialized in `ideation.md`. "Shallow"/"blind" = keep exploring. | | **Problem decomposition** | PLAN | Understand the whole, find natural boundaries, minimize dependencies, start with the riskiest part. | | **Assumption tracking** | PLAN | Every assumption traced to a finding, linked to dependent steps. When one breaks, you know what's invalidated. | diff --git a/src/SKILL.md b/src/SKILL.md index 4c99d57..6ad7aa6 100644 --- a/src/SKILL.md +++ b/src/SKILL.md @@ -218,6 +218,7 @@ Institutional memory across plans. Unlike FINDINGS.md and DECISIONS.md which gro - **Selection** — picked candidate, criteria used, confidence (high/medium/low + one-line reason). - **Rejected** — one-line rationale per non-picked candidate. These feed RE-PLAN's ghost-constraint scan; if a rejection constraint turns out to be ghost, the rejected candidate can be reactivated. - **Single-Path Escape Hatch** — if the task genuinely has no design alternatives (mechanical rename, deterministic migration), list 1 candidate and populate the escape hatch with: why no alternatives, one falsification trigger that would invalidate the single-path assumption. Two candidates is not enough — either generate a third or invoke the escape hatch. + - **Viability-flipping constraint check** — before generating candidates, scan the constraint classification in `findings.md`. A constraint is **viability-flipping** if reclassifying it (hard ↔ soft, hard ↔ ghost, soft ↔ ghost) would change which candidates are viable. For each viability-flipping constraint where classification is uncertain → **ASK the user a focused question** (one at a time, per the EXPLORE question rule) before generating. Don't guess. If during generation a constraint turns out to be misclassified and that flips a candidate's viability → STOP, log the discovery in `state.md` and `findings.md` (with `[CORRECTED iter-N]` if applicable), re-classify constraints, then re-run the gate. State remains EXPLORE throughout — the gate is the last EXPLORE sub-step, so this is "back up one sub-step," not a state transition. - **Surface to user**: present the candidate set + selection before the EXPLORE → PLAN transition. The user can redirect to a rejected candidate or push for more options. - Picked candidate becomes the seed for **D-001** in `decisions.md` during PLAN (the chosen approach + Trade-off, with rejected candidates referenced). - Validator (`validate-plan.mjs`) ERRORs at PLAN/EXECUTE/REFLECT/RE-PLAN if ideation.md is missing, lacks ≥3 candidates (without escape hatch), or has no Selection. diff --git a/src/references/planning-rigor.md b/src/references/planning-rigor.md index d13fa27..9821252 100644 --- a/src/references/planning-rigor.md +++ b/src/references/planning-rigor.md @@ -16,6 +16,8 @@ The **Ideation Gate** (last action of EXPLORE; see SKILL.md EXPLORE section) mat **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 state *why* there are no alternatives and *one falsification 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. +**Viability-flipping constraints — ask, don't guess.** A constraint is *viability-flipping* if reclassifying it (hard ↔ soft, hard ↔ ghost) would change which candidates are viable. Before generating candidates, scan the classified constraints. For each viability-flipping constraint that's uncertain, **ask the user a focused question** — don't guess. 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. If you discover mid-generation that a constraint was misclassified and that flips a candidate's viability, stop, re-classify in `findings.md` (with `[CORRECTED iter-N]` if you're correcting an earlier finding), and re-run the gate. The state stays EXPLORE — the gate is an EXPLORE sub-step, so this is "back up one step," not a transition. + ## Assumption Tracking Plans depend on assumptions discovered during EXPLORE. Make them explicit so when one breaks, you know which steps are invalidated. From 1aa648e064f176dd1885f442256fcff164432943 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 10 May 2026 10:03:44 +0000 Subject: [PATCH 3/3] Tighten ideation gate validator: empty-file and Falsification checks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses two Codex review comments on PR #1: P1 — Empty/unreadable ideation.md silently passed validation. The early return on falsy readFile() result bypassed all ideation checks, letting a plan reach PLAN/EXECUTE without candidates or Selection. Now ERRORs with "ideation.md is empty or unreadable". P2 — Single-Path Escape Hatch was treated as populated when only "Why no alternatives" had content; "Falsification" being placeholder ("-") still passed. Both fields are protocol-required: a single-path claim without a falsification trigger has no STOP signal during EXECUTE. Now checks both, and emits a precise message when only one is populated. 3 new tests covering: empty ideation.md, escape hatch missing Falsification, escape hatch missing Why no alternatives. 112 tests, 0 failures (was 109). https://claude.ai/code/session_01WmjKGk8F6QgjpPDK49aBAn --- src/scripts/bootstrap.test.mjs | 47 ++++++++++++++++++++++++++++++++++ src/scripts/validate-plan.mjs | 41 ++++++++++++++++++++++------- 2 files changed, 79 insertions(+), 9 deletions(-) diff --git a/src/scripts/bootstrap.test.mjs b/src/scripts/bootstrap.test.mjs index 1bd5c09..89f66a7 100644 --- a/src/scripts/bootstrap.test.mjs +++ b/src/scripts/bootstrap.test.mjs @@ -1688,6 +1688,53 @@ describe("bootstrap.mjs", () => { 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"); diff --git a/src/scripts/validate-plan.mjs b/src/scripts/validate-plan.mjs index 857c473..907ac3b 100644 --- a/src/scripts/validate-plan.mjs +++ b/src/scripts/validate-plan.mjs @@ -201,7 +201,10 @@ function checkIdeation(planDir, issues) { } const ideation = readFile(ideationPath); - if (!ideation) return; + 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"); @@ -223,20 +226,40 @@ function checkIdeation(planDir, issues) { candidateCount = matches ? matches.length : 0; } - // Determine if escape hatch is genuinely populated (not the bootstrap placeholder) + // 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) && - /-\s*\*\*Why no alternatives\*\*:\s*[^\s-]/.test(escapeSection) && - !/^N\/A$/im.test(escapeSection.trim()) + !/^N\/A$/im.test(escapeSection.trim()) && + whyPopulated && + falsificationPopulated ); if (candidateCount < 3 && !escapeHatchPopulated) { - issues.push({ - severity: "ERROR", - check: "ideation", - message: `ideation.md has ${candidateCount} candidate(s) — minimum 3 required, OR populate Single-Path Escape Hatch with a "Why no alternatives" rationale`, - }); + 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`, + }); + } } }