From 62f477484dbc957a43f7d14e474592a770484bc7 Mon Sep 17 00:00:00 2001 From: Baptiste LAFOURCADE Date: Fri, 23 Oct 2026 07:31:46 +0200 Subject: [PATCH 1/5] refactor(aidd-dev): lean the plan skill for small models Four actions become three (capture, explore, conclude) behind a mermaid router. The plan folder is checked by a bundled validator built from the templates, then by one independent read against plan-checks. Phases gain a Depends on column. Co-Authored-By: Claude Sonnet 5.5 --- plugins/aidd-dev/CATALOG.md | 10 +- plugins/aidd-dev/skills/01-plan/SKILL.md | 50 +++--- .../skills/01-plan/actions/01-capture.md | 17 ++ .../skills/01-plan/actions/01-gather.md | 21 --- .../skills/01-plan/actions/02-explore.md | 25 +-- .../skills/01-plan/actions/03-conclude.md | 27 +++ .../skills/01-plan/actions/03-wireframe.md | 20 --- .../skills/01-plan/actions/04-plan.md | 28 --- .../skills/01-plan/assets/phase-template.md | 54 ++---- .../skills/01-plan/assets/plan-template.md | 51 ++++-- .../skills/01-plan/references/findings.md | 11 ++ .../skills/01-plan/references/plan-checks.md | 9 + .../skills/01-plan/references/plan-status.md | 23 --- .../skills/01-plan/scripts/validate-plan.cjs | 158 ++++++++++++++++ scripts/__tests__/validate-plan.test.js | 169 ++++++++++++++++++ 15 files changed, 483 insertions(+), 190 deletions(-) create mode 100644 plugins/aidd-dev/skills/01-plan/actions/01-capture.md delete mode 100644 plugins/aidd-dev/skills/01-plan/actions/01-gather.md create mode 100644 plugins/aidd-dev/skills/01-plan/actions/03-conclude.md delete mode 100644 plugins/aidd-dev/skills/01-plan/actions/03-wireframe.md delete mode 100644 plugins/aidd-dev/skills/01-plan/actions/04-plan.md create mode 100644 plugins/aidd-dev/skills/01-plan/references/findings.md create mode 100644 plugins/aidd-dev/skills/01-plan/references/plan-checks.md delete mode 100644 plugins/aidd-dev/skills/01-plan/references/plan-status.md create mode 100644 plugins/aidd-dev/skills/01-plan/scripts/validate-plan.cjs create mode 100644 scripts/__tests__/validate-plan.test.js diff --git a/plugins/aidd-dev/CATALOG.md b/plugins/aidd-dev/CATALOG.md index 331adbca2..4d59999de 100644 --- a/plugins/aidd-dev/CATALOG.md +++ b/plugins/aidd-dev/CATALOG.md @@ -41,15 +41,15 @@ Auto-generated index of skills, agents, references and assets shipped by the `ai | Group | File | Description | |-------|------|---| -| `actions` | [01-gather.md](skills/01-plan/actions/01-gather.md) | - | +| `actions` | [01-capture.md](skills/01-plan/actions/01-capture.md) | - | | `actions` | [02-explore.md](skills/01-plan/actions/02-explore.md) | - | -| `actions` | [03-wireframe.md](skills/01-plan/actions/03-wireframe.md) | - | -| `actions` | [04-plan.md](skills/01-plan/actions/04-plan.md) | - | +| `actions` | [03-conclude.md](skills/01-plan/actions/03-conclude.md) | - | | `assets` | [phase-template.md](skills/01-plan/assets/phase-template.md) | - | | `assets` | [plan-template.md](skills/01-plan/assets/plan-template.md) | - | -| `references` | [plan-status.md](skills/01-plan/references/plan-status.md) | `Plan lifecycle status field - values, meaning, who writes each, and when.` | +| `references` | [findings.md](skills/01-plan/references/findings.md) | - | +| `references` | [plan-checks.md](skills/01-plan/references/plan-checks.md) | - | | `references` | [wireframe-conventions.md](skills/01-plan/references/wireframe-conventions.md) | - | -| `-` | [SKILL.md](skills/01-plan/SKILL.md) | `Turn a request, ticket, or file into a phased implementation plan. Use to plan a feature before building, or to turn a ticket into phases. Do NOT use to write code or review a diff.` | +| `-` | [SKILL.md](skills/01-plan/SKILL.md) | `Turn a request, ticket, or file into a phased implementation plan. Use to plan a feature before building, or to turn a ticket into phases. Not for writing code or reviewing a diff.` | #### `skills/02-implement` diff --git a/plugins/aidd-dev/skills/01-plan/SKILL.md b/plugins/aidd-dev/skills/01-plan/SKILL.md index e8649e4a6..a2789207e 100644 --- a/plugins/aidd-dev/skills/01-plan/SKILL.md +++ b/plugins/aidd-dev/skills/01-plan/SKILL.md @@ -1,31 +1,33 @@ --- name: 01-plan -description: Turn a request, ticket, or file into a phased implementation plan. Use to plan a feature before building, or to turn a ticket into phases. Do NOT use to write code or review a diff. +description: Turn a request, ticket, or file into a phased implementation plan. Use to plan a feature before building, or to turn a ticket into phases. Not for writing code or reviewing a diff. argument-hint: request | ticket | file --- - -# Skill: plan - -Turn a gathered source into an implementation plan and its phase files. Never writes code. +# Plan ## Actions -| # | Action | Role | Input | -| --- | ----------- | ---------------------------------------------------- | -------------------------- | -| 01 | `gather` | Collect and restate the source | user request | -| 02 | `explore` | Read the codebase for projection, rules, feasibility | gathered source | -| 03 | `wireframe` | Sketch a screen at low fidelity, frontend only | source + explore context | -| 04 | `plan` | Break into phases, write the plan and phase files | explore output + wireframe | - -Run them in order, `01 → 04`. The plan is the culmination. Skip `03` when there is no UI. -Before running an action, read its file in `actions/`, not only the table or assets. - -## References - -- `references/wireframe-conventions.md`: how to draw the ASCII wireframe a screen needs. -- `references/plan-status.md`: the plan lifecycle `status` values and who writes each. - -## Assets - -- `assets/plan-template.md`: the `plan.md` scaffold. -- `assets/phase-template.md`: the per-phase scaffold. +```mermaid +flowchart LR + capture --> explore + explore -->|"the code opens a question"| explore + explore -->|"a decision blocks"| user((user answers)) + user --> explore + explore -->|"nothing blocks"| conclude + conclude -->|"correct: revise, ask again"| conclude + conclude -->|"explore more"| explore + conclude -->|"approve"| done((done)) +``` + +Read an action's file, then do what it says. + +| Action | File | +| ------ | ---- | +| capture | [actions/01-capture.md](actions/01-capture.md) | +| explore | [actions/02-explore.md](actions/02-explore.md) | +| conclude | [actions/03-conclude.md](actions/03-conclude.md) | + +## Transversal rules + +- Speak only to ask or to give the final reply; otherwise call tools. +- Plan only: the plan files are the only files you write. diff --git a/plugins/aidd-dev/skills/01-plan/actions/01-capture.md b/plugins/aidd-dev/skills/01-plan/actions/01-capture.md new file mode 100644 index 000000000..8455a3d13 --- /dev/null +++ b/plugins/aidd-dev/skills/01-plan/actions/01-capture.md @@ -0,0 +1,17 @@ +# 01 - Capture + +Settle what the plan rests on. + +## Input + +The user's request. + +## Process + +1. With no source to plan from (file, ticket, or text), ask for one and stop. +2. Read it, noting what it asks and the constraints it states. +3. Go to explore. + +## Output + +What was understood, kept for explore, with the source's reference (path, ticket id, or text). diff --git a/plugins/aidd-dev/skills/01-plan/actions/01-gather.md b/plugins/aidd-dev/skills/01-plan/actions/01-gather.md deleted file mode 100644 index 22d9571cc..000000000 --- a/plugins/aidd-dev/skills/01-plan/actions/01-gather.md +++ /dev/null @@ -1,21 +0,0 @@ -# 01 - Gather - -Collect the source the plan will rest on, before any planning. Read only. - -## Input - -The user's request, which may be empty. - -## Output - -The source restated in a few bullets: what is asked and the hard constraints it states. The source kind and reference (file path, ticket id, or text) are named. - -## Process - -1. **Find.** Identify what the request points to: a file path, a ticket URL or id, or raw text. When nothing concrete is given, ask once for a file, a ticket, or a description. Do not start without a source. -2. **Pull.** Read the file, fetch the ticket, or take the text as given. Never invent content the source does not contain. -3. **Restate.** Summarize the source in a few bullets: what is asked, and the hard constraints it states. No solution, no phases, no plan. - -## Test - -- The output names the source kind and reference and restates it in bullets, states no solution, and when nothing concrete was given the user was asked before anything else. diff --git a/plugins/aidd-dev/skills/01-plan/actions/02-explore.md b/plugins/aidd-dev/skills/01-plan/actions/02-explore.md index 33a6916d5..c747b100d 100644 --- a/plugins/aidd-dev/skills/01-plan/actions/02-explore.md +++ b/plugins/aidd-dev/skills/01-plan/actions/02-explore.md @@ -1,23 +1,24 @@ # 02 - Explore -Read the codebase to ground the plan: what it touches, which rules apply, what is feasible. Read only. +Ground the plan in the codebase. ## Input -The gathered source from `01-gather`. +The source from capture. -## Output +## Process -The architecture projection (files to modify, create, delete, each with a one-line reason), the project rules that apply (each justified in one line, or none), the feasibility checks (each source consulted, a doc URL or an in-repo file, and what it settled), and the risks. +1. Read the code the source touches and gather the [findings](../references/findings.md). +2. Take stock: what each finding establishes with its evidence, and what is missing. +3. Decide what comes next: + - If something is missing, go back to step 1 with it. + - If only the user can settle it and it blocks the plan, ask and wait, then go back to step 1. + - Otherwise read [conclude](03-conclude.md). -## Process +## Output -1. **Read.** Explore the code the source touches. Build the projection and list the infrastructure assumptions. -2. **Check.** Verify against the official docs or the in-repo code. Keep each source and what it settled. Flag blockers and risks. -3. **Select.** Keep the project rules that apply to the projection. +The findings: files to modify, create, and delete, the impact, the rules, and the docs checked. -## Test +## Rules -- The projection lists files to modify, create, and delete, each with a one-line reason. -- Every feasibility check records its source and what it settled. -- The applicable rules are identified and justified, or none apply. +- Name a file to modify or delete only after reading it. diff --git a/plugins/aidd-dev/skills/01-plan/actions/03-conclude.md b/plugins/aidd-dev/skills/01-plan/actions/03-conclude.md new file mode 100644 index 000000000..3f3b87a3d --- /dev/null +++ b/plugins/aidd-dev/skills/01-plan/actions/03-conclude.md @@ -0,0 +1,27 @@ +# 03 - Conclude + +Produce the approved plan. + +## Input + +The explored source. + +## Process + +1. Split the work into phases, each shipping and verifying on its own, the first one a safety net when the findings say so. +2. Give each file to modify, create, or delete, and each impact, to the phase that owns it, losing none. + - A phase that builds a screen layout (page, view, panel) draws its Wireframe per [wireframe conventions](../references/wireframe-conventions.md); other phases omit the section. +3. Write the feature folder in the project's `aidd_docs/tasks//_/`, or in the source's own folder: + - `plan.md` from the [plan template](../assets/plan-template.md). + - One `phase-.md` per phase from the [phase template](../assets/phase-template.md). +4. From the project root, run the skill's `scripts/validate-plan.cjs` with node on the feature folder, and fix what it reports until it prints `VALID`. +5. Have a read-only subagent, independent of its author, check the plan against [plan-checks](../references/plan-checks.md), once. +6. Reply with only the folder path, the ✗ lines, and "Approve, correct, or explore more?". + +## Output + +The feature folder with `plan.md` and its phase files, then the ✗ lines. + +## Rules + +- Delete every comment. diff --git a/plugins/aidd-dev/skills/01-plan/actions/03-wireframe.md b/plugins/aidd-dev/skills/01-plan/actions/03-wireframe.md deleted file mode 100644 index 928b9cb95..000000000 --- a/plugins/aidd-dev/skills/01-plan/actions/03-wireframe.md +++ /dev/null @@ -1,20 +0,0 @@ -# 03 - Wireframe - -Sketch a low-fidelity ASCII wireframe of a screen, to fix its layout before the plan. Frontend only, skip it when the feature has no UI. - -## Input - -The screens to sketch, from the gathered source and the `02-explore` context (the existing screens and components). - -## Output - -A low-fidelity ASCII wireframe of each screen, its regions and key elements in place, with a numbered note per region. Structure only, no behavior, no styling, no final copy. - -## Process - -1. **Pick.** List the screens the feature needs. Skip the action entirely when there is no UI. -2. **Draw.** Sketch each screen per [wireframe-conventions.md](../references/wireframe-conventions.md). The wireframe feeds the plan. - -## Test - -- The output is a box-drawing wireframe of each screen with numbered regions and a one-line note each, carrying no behavior, styling, or final copy. diff --git a/plugins/aidd-dev/skills/01-plan/actions/04-plan.md b/plugins/aidd-dev/skills/01-plan/actions/04-plan.md deleted file mode 100644 index e6f0677ce..000000000 --- a/plugins/aidd-dev/skills/01-plan/actions/04-plan.md +++ /dev/null @@ -1,28 +0,0 @@ -# 04 - Plan - -Turn the explored source into a plan and its phases, save them, then review the whole before handoff. Never code. - -## Input - -The explore output from `02-explore` (projection, rules, feasibility, risks), plus any confirmed wireframe from `03-wireframe`. - -## Output - -A feature folder, always at `aidd_docs/tasks//_/`, holding `plan.md` from [plan-template.md](../assets/plan-template.md) and one `phase-.md` per phase from [phase-template.md](../assets/phase-template.md). - -## Process - -1. **Phases.** Break the work into phases, each a coherent unit of work that ships and verifies on its own, sized for one executor pass. Let the work decide how many. -2. **Folder.** Reuse the feature folder the source already lives in, or create one. -3. **Fill.** Fill the plan and each phase from their templates, following the inline contracts. Slice the projection across the phases. -4. **Show.** Display the written paths. -5. **Review.** Score the complete plan and its phases from 0 to 10, with ✓ reasons and ✗ risks. In interactive mode, show them and revise until approved. Under an autonomous orchestrator, revise against the source without waiting for approval; ask only when a product decision cannot be resolved from the source. The score is never written to the plan. - -## Test - -- `aidd_docs/tasks//_/plan.md` exists with one `phase-.md` per phase next to it. -- Every written file satisfies its template's inline contract. -- No `{...}` placeholder is left in any written file. -- The phase projection slices together cover the modify, create, and delete lists. -- A confidence score was reported and written to no file. -- An autonomous run waits only for a product decision that the source cannot resolve. diff --git a/plugins/aidd-dev/skills/01-plan/assets/phase-template.md b/plugins/aidd-dev/skills/01-plan/assets/phase-template.md index 21bba1457..48a689e5e 100644 --- a/plugins/aidd-dev/skills/01-plan/assets/phase-template.md +++ b/plugins/aidd-dev/skills/01-plan/assets/phase-template.md @@ -2,9 +2,9 @@ status: pending --- - + -# Instruction: {title} +# Instruction: {{title}} ## Architecture projection @@ -14,59 +14,37 @@ status: pending . ``` -## User Journey - -```mermaid -flowchart TD - A[TODO] -``` - ## Test Scope - + -```mermaid ---- -title: Test scope ---- -journey - %% Every task has exactly one actor: browser, api, cli, or system. - section Setup - %% Deterministic preconditions and fixtures. Its channel may differ. - {prepare fixture} => {ready state}: 5: {channel} - section Happy path - %% Required. Every task states action => observable outcome. All tasks use the same channel. - {action} => {observable expected outcome}: 5: {channel} - %% Add one named section per edge case only when its trigger and expected outcome are known. - section Edge case - {name} - %% Every task names trigger => action => observable outcome. All tasks use the same channel. - {trigger} => {action} => {observable expected outcome}: 1: {channel} - %% Include only when a scenario changes state. Its channel may differ. - section Teardown - {cleanup or reset action} => {baseline restored}: 5: {channel} -``` +| Scenario | Action | Observable outcome | Actor | +| ------------ | ----------------------- | -------------------- | ------------------------------- | +| Setup | {{prepare fixture}} | {{ready state}} | {{browser, api, cli, or system}} | +| Happy path | {{action}} | {{observable outcome}} | {{browser, api, cli, or system}} | +| Edge case | {{trigger and action}} | {{observable outcome}} | {{browser, api, cli, or system}} | +| Teardown | {{cleanup}} | {{baseline restored}} | {{browser, api, cli, or system}} | ## Wireframe - + ```txt -{the confirmed wireframe} +{{the confirmed wireframe}} ``` ## Tasks to do -### `{number})` {name} +### `{{number}})` {{name}} -> {straight to point goal} +> {{straight to point goal}} -1. {ultra concise step} - ... +1. {{ultra concise step}} ## Test acceptance criteria - + | Task | Acceptance criteria | | ---- | -------------------------------- | -| 1... | {observable behavior of the task} | +| 1... | {{observable behavior of the task}} | diff --git a/plugins/aidd-dev/skills/01-plan/assets/plan-template.md b/plugins/aidd-dev/skills/01-plan/assets/plan-template.md index 79301f70f..733166425 100644 --- a/plugins/aidd-dev/skills/01-plan/assets/plan-template.md +++ b/plugins/aidd-dev/skills/01-plan/assets/plan-template.md @@ -1,33 +1,46 @@ --- -objective: "{What must be true when done. One sentence.}" +objective: "{{What must be true when done. One sentence.}}" status: pending --- - + -# Plan: {title} +# Plan: {{title}} -## Overview - -| Field | Value | -| ---------- | ----------------------- | -| **Goal** | {one-line summary} | -| **Source** | {file, ticket, or text} | +Source: {{file, ticket, or text}} ## Phases -| # | Phase | File | -| --- | ------------ | ---------------------------- | -| 1 | {phase-name} | [`phase-1.md`](./phase-1.md) | -| 2 | {phase-name} | [`phase-2.md`](./phase-2.md) | + + +| # | Phase | File | Depends on | +| --- | ------------ | ---------------------------- | ---------- | +| 1 | {{phase-name}} | [`phase-1.md`](./phase-1.md) | none | +| 2 | {{phase-name}} | [`phase-2.md`](./phase-2.md) | {{earlier phase numbers, or `none`}} | + +## External sources + + + +| URL | What it settled | +| ----- | ----------------- | +| {{url}} | {{what it settled}} | + +## Impact + + + +| Category | Verdict and evidence | Handled in | +| -------------------------------------------------- | -------------------- | ------------------ | +| {{runtime, regression, architecture, documentation}} | {{verdict and its evidence, or `none`}} | {{phase, or `none`}} | -## Resources +## Rules - + -| Source | Verified | -| ------ | ----------------- | -| {url} | {what it settled} | +| Rule | Why it applies | +| ------ | -------------- | +| {{rule}} | {{why}} | ## Decisions @@ -35,4 +48,4 @@ status: pending | Decision | Why | | ---------- | ----- | -| {decision} | {why} | +| {{decision}} | {{why}} | diff --git a/plugins/aidd-dev/skills/01-plan/references/findings.md b/plugins/aidd-dev/skills/01-plan/references/findings.md new file mode 100644 index 000000000..a07524893 --- /dev/null +++ b/plugins/aidd-dev/skills/01-plan/references/findings.md @@ -0,0 +1,11 @@ +# Findings + +| Finding | When | Evidence | +| --- | --- | --- | +| Files to modify, create, delete | always | read in the code, one-line reason each | +| Runtime impact | the feature relies on something outside the repository | observed: validated; else unverified or missing | +| Regression impact | it changes existing code | tests covering the areas, risk (low, medium, high), safety-net phase of tests that lock the current behavior (yes, no) | +| Architecture impact | it changes how parts connect | the architecture it starts from, the change it intends | +| Documentation impact | always | documents to change and why, or none | +| Docs consulted | an assumption the code cannot settle | official URL, what it settled | +| Project rules | they govern the feature | one-line reason each | diff --git a/plugins/aidd-dev/skills/01-plan/references/plan-checks.md b/plugins/aidd-dev/skills/01-plan/references/plan-checks.md new file mode 100644 index 000000000..fd6ddaea9 --- /dev/null +++ b/plugins/aidd-dev/skills/01-plan/references/plan-checks.md @@ -0,0 +1,9 @@ +# Plan checks + +| Check | ✓ when | +| --- | --- | +| Requirements | each requirement of the source has a task | +| Reading | every file to modify was read, and what the plan relies on was looked for | +| Evidence | each impact verdict is observed, or says unverified | + +Each ✗ names what was not dug into, left unverified, or assumed. diff --git a/plugins/aidd-dev/skills/01-plan/references/plan-status.md b/plugins/aidd-dev/skills/01-plan/references/plan-status.md deleted file mode 100644 index 51e98bf1e..000000000 --- a/plugins/aidd-dev/skills/01-plan/references/plan-status.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -name: plan-status -description: Plan lifecycle status field - values, meaning, who writes each, and when. ---- - -# Plan status lifecycle - -The plan's `status` frontmatter field tracks its lifecycle for kanban views. The FILENAME carries no status suffix - status lives in frontmatter only. - -| status | meaning | written by | when | -| ------------- | ----------------------------- | ----------------------------------------------------------------- | ---------------------------------------------------------------- | -| `pending` | created, not started | plan creation | at plan creation | -| `in-progress` | implementation started | the implement step | when implementation starts | -| `implemented` | implemented, not yet reviewed | the implement step | all phases complete / all acceptance criteria ticked | -| `reviewed` | reviewed and approved | the review step | the review passes (approved) | -| `blocked` | cannot proceed; needs a human | the implement step | a blocking condition holds | - -## Rules - -- Linear: `pending → in-progress → implemented → reviewed`. `blocked` is reachable from any active state. -- Review reject (`iterate` / `changes-requested`) does NOT set `reviewed`. The plan stays `implemented` while the loop fixes the diff, not the plan. -- `implemented` ≠ reviewed. Only the review layer sets `reviewed`. -- Workers never write `status`; only plan creation and the orchestration layers do. diff --git a/plugins/aidd-dev/skills/01-plan/scripts/validate-plan.cjs b/plugins/aidd-dev/skills/01-plan/scripts/validate-plan.cjs new file mode 100644 index 000000000..b0e1bedba --- /dev/null +++ b/plugins/aidd-dev/skills/01-plan/scripts/validate-plan.cjs @@ -0,0 +1,158 @@ +#!/usr/bin/env node +// Usage, from the project root: node validate-plan.cjs +// The templates in ../assets are the contract: sections, order, table headers, front matter. +const fs = require("fs"); +const path = require("path"); + +const errors = []; +const fail = (file, rule) => errors.push(`${file}: ${rule}`); +const norm = (s) => s.replace(/\s+/g, " ").trim(); +const asset = (name) => fs.readFileSync(path.join(__dirname, "..", "assets", name), "utf8"); + +function parse(md) { + const fm = {}; + const head = md.match(/^---\n([\s\S]*?)\n---\n/); + for (const l of head ? head[1].split("\n") : []) { + const k = l.match(/^(\w+):\s*(.*)$/); + if (k) fm[k[1]] = k[2].replace(/^"|"$/g, ""); + } + const sections = []; + let title = ""; + let cur = null; + let fence = false; + for (const line of (head ? md.slice(head[0].length) : md).split("\n")) { + if (line.startsWith("```")) fence = !fence; + const h = !fence && line.match(/^(#{1,2}) (.*)$/); + if (h && h[1] === "#") title = title || line; + else if (h) sections.push((cur = { name: norm(h[2]), body: "" })); + else if (cur) cur.body += `${line}\n`; + } + return { fm, title, sections }; +} + +const cells = (l) => l.split("|").slice(1, -1).map(norm).join("|"); +function tableHeaders(body) { + const lines = body.split("\n"); + return lines.filter((l, i) => l.startsWith("|") && /^\|[\s:|-]+\|$/.test(lines[i + 1] || "")).map(cells); +} +const optional = (body) => (body.match(//g) || []).some((c) => /omit/i.test(c)); +const section = (doc, name) => doc.sections.find((s) => s.name === name); + +function matchTemplate(file, md, tpl) { + const doc = parse(md); + const noCode = md.replace(/```[\s\S]*?```/g, "").replace(/`[^`\n]*`/g, ""); + if (/\n*/g, "") + .replace(/\{\{number\}\}/g, "1") + .replace(/\{\{url\}\}/g, "https://example.test") + .replace(/\| 1\.\.\. \|/g, "| 1 |") + .replace(/\{\{the confirmed wireframe\}\}\n```/, "(1) x\n```\n\n1. x") + .replace(/\{\{earlier phase numbers[^}]*\}\}/, "1") + .replace(/\{\{[^}]*\}\}/g, "x") + .replace(/```txt\n\.\n```/, tree ? `\`\`\`txt\n.\n${tree}\n\`\`\`` : "```txt\n.\n```"); +} + +function write(files) { + const dir = fs.mkdtempSync(path.join(root, "plan-")); + for (const [name, content] of Object.entries(files)) fs.writeFileSync(path.join(dir, name), content); + return dir; +} + +const good = (tree) => ({ + "plan.md": fill(template("plan-template.md")), + "phase-1.md": fill(template("phase-template.md"), tree), + "phase-2.md": fill(template("phase-template.md")), +}); + +function run(files, skill = SKILL) { + const dir = write(files); + const r = spawnSync(process.execPath, [path.join(skill, "scripts/validate-plan.cjs"), dir], { cwd: project, encoding: "utf8" }); + return { out: r.stdout.trim(), code: r.status }; +} + +const mutate = (file, edit, tree) => { + const files = good(tree); + files[file] = edit(files[file]); + return files; +}; + +before(() => { + root = fs.mkdtempSync(path.join(os.tmpdir(), "validate-plan-")); + project = path.join(root, "project"); + fs.mkdirSync(path.join(project, "src"), { recursive: true }); + fs.writeFileSync(path.join(project, "src/a.js"), ""); +}); +after(() => fs.rmSync(root, { recursive: true, force: true })); + +describe("validate-plan", () => { + it("accepts a plan filled from the templates", () => { + const r = run(good()); + assert.equal(r.code, 0); + assert.match(r.out, /^VALID$/); + }); + + it("asks for the files when the folder holds no plan", () => { + assert.match(run({}).out, /^INVALID\n.*plan\.md: missing/); + }); + + it("accepts dependencies on earlier phases and none", () => { + for (const deps of ["none", "`none`", "1"]) { + const r = run(mutate("plan.md", (s) => s.replace(/\| 2\s+\|(.*)\| 1 \|/, `| 2 |$1| ${deps} |`))); + assert.equal(r.code, 0, deps); + } + }); + + it("rejects a dependency that is not an earlier phase number", () => { + for (const deps of ["2", "3", "x", "", "1, 2"]) { + const r = run(mutate("plan.md", (s) => s.replace(/\| 2\s+\|(.*)\| 1 \|/, `| 2 |$1| ${deps} |`))); + assert.match(r.out, /phase 2 "Depends on" must be/, JSON.stringify(deps)); + } + }); + + it("rejects a wireframe without numbered regions", () => { + for (const body of ["none", "N/A", "(1) x"]) { + const r = run(mutate("phase-1.md", (s) => s.replace(/(## Wireframe\n\n)[\s\S]*?(?=## Tasks)/, `$1\`\`\`txt\n${body}\n\`\`\`\n\n`))); + assert.match(r.out, /phase-1\.md: section "Wireframe" needs a frame/, body); + } + }); + + it("accepts a phase that omits the wireframe", () => { + const r = run(mutate("phase-1.md", (s) => s.replace(/## Wireframe\n\n[\s\S]*?(?=## Tasks)/, ""))); + assert.equal(r.code, 0); + }); + + const cases = [ + ["a comment left", "plan.md", (s) => `${s}\n`, /comment left/], + ["a placeholder left", "plan.md", (s) => s.replace("Source: x", "Source: {{file}}"), /placeholder/], + ["a required section missing", "phase-1.md", (s) => s.replace(/## Test Scope[\s\S]*?(?=## Wireframe)/, ""), /section "Test Scope" missing/], + ["an unknown section", "plan.md", (s) => `${s}\n## Extra\n`, /section "Extra" is not in the template/], + ["sections out of order", "plan.md", (s) => s.replace("## Rules", "## Zzz").replace("## Impact", "## Rules").replace("## Zzz", "## Impact"), /out of order/], + ["a table header renamed", "plan.md", (s) => s.replace("| Rule ", "| Law "), /table header differs/], + ["a front matter key missing", "plan.md", (s) => s.replace(/objective:.*\n/, ""), /front matter key "objective" missing/], + ["a front matter value changed", "phase-1.md", (s) => s.replace("status: pending", "status: done"), /"status" must be "pending"/], + ["an empty objective", "plan.md", (s) => s.replace(/objective:.*\n/, 'objective: ""\n'), /"objective" empty/], + ["a source without a URL", "plan.md", (s) => s.replace("https://example.test", "somewhere"), /without a URL/], + ["a gap in the task numbers", "phase-1.md", (s) => s.replace("### `1)`", "### `2)`"), /task numbers must run 1 to 1 with no gap/], + ["a criterion mapping no task", "phase-1.md", (s) => s.replace(/^\| 1 \| x/m, "| 9 | x"), /acceptance row 9 maps to no task/], + ["a listed phase missing", "plan.md", (s) => s.replace(/(\| 2 .*\n)/, "$1| 3 | x | x |\n"), /phase-3\.md: listed in plan\.md, file missing/], + ]; + for (const [name, file, edit, expected] of cases) { + it(`refuses ${name}`, () => { + const r = run(mutate(file, edit)); + assert.equal(r.code, 1); + assert.match(r.out, expected); + }); + } + + it("accepts an omitted optional section and a placeholder inside code", () => { + const files = mutate("plan.md", (s) => s.replace(/## Rules[\s\S]*?(?=## Decisions)/, "").replace("Source: x", "Source: `{{kept}}`")); + assert.match(run(files).out, /^VALID$/); + }); + + it("refuses a phase file the plan does not list", () => { + assert.match(run({ ...good(), "phase-3.md": good()["phase-1.md"] }).out, /phase-3\.md: not listed/); + }); + + describe("files to modify and delete", () => { + const tree = (lines) => lines.join("\n"); + it("resolves a nested tree against the project", () => { + assert.match(run(good(tree(["└── src/", " └── ✏️ a.js"]))).out, /^VALID$/); + }); + it("refuses a file to modify that does not exist", () => { + assert.match(run(good(tree(["└── src/", " └── ✏️ ghost.js"]))).out, /src\/ghost\.js is marked modify/); + }); + it("refuses a file to delete that does not exist", () => { + assert.match(run(good(tree(["└── ❌ src/ghost.js"]))).out, /marked delete but does not exist/); + }); + it("skips names that are not plain paths: globs, braces, notes", () => { + assert.match(run(good(tree(["├── ✏️ src/**/*.ts", "├── ✏️ src/{a,b}.js", "└── ✏️ (several files)"]))).out, /^VALID$/); + }); + it("accepts a file to create that is absent", () => { + assert.match(run(good(tree(["└── ✅ src/new.js"]))).out, /^VALID$/); + }); + }); + + it("refuses a folder inside the skill itself", () => { + const copy = path.join(root, "skill-inside"); + fs.cpSync(SKILL, copy, { recursive: true }); + const inside = path.join(copy, "runs/x"); + fs.mkdirSync(inside, { recursive: true }); + for (const [name, content] of Object.entries(good())) fs.writeFileSync(path.join(inside, name), content); + const r = spawnSync(process.execPath, [path.join(copy, "scripts/validate-plan.cjs"), inside], { cwd: project, encoding: "utf8" }); + assert.match(r.stdout, /folder is inside the skill itself/); + }); + + it("follows the template: a section added there becomes required", () => { + const copy = path.join(root, "skill"); + fs.cpSync(SKILL, copy, { recursive: true }); + fs.appendFileSync(path.join(copy, "assets/plan-template.md"), "\n## Added\n\nx\n"); + assert.match(run(good(), copy).out, /plan\.md: section "Added" missing/); + }); +}); From 8053b590d01d3b1d04fd09b535063b12545a0a79 Mon Sep 17 00:00:00 2001 From: Baptiste LAFOURCADE Date: Fri, 23 Oct 2026 08:56:56 +0200 Subject: [PATCH 2/5] fix(aidd-dev): accept a plan folder on another drive in the plan validator path.relative returns an absolute path across Windows drives, which the inside-the-skill check read as inside. Co-Authored-By: Claude Sonnet 5.5 --- plugins/aidd-dev/skills/01-plan/scripts/validate-plan.cjs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/aidd-dev/skills/01-plan/scripts/validate-plan.cjs b/plugins/aidd-dev/skills/01-plan/scripts/validate-plan.cjs index b0e1bedba..1d0613d8b 100644 --- a/plugins/aidd-dev/skills/01-plan/scripts/validate-plan.cjs +++ b/plugins/aidd-dev/skills/01-plan/scripts/validate-plan.cjs @@ -107,7 +107,8 @@ if (!dir || !fs.existsSync(planFile)) { report(); } -if (!path.relative(path.join(__dirname, ".."), fs.realpathSync(dir)).startsWith("..")) { +const fromSkill = path.relative(path.join(__dirname, ".."), fs.realpathSync(dir)); +if (!fromSkill.startsWith("..") && !path.isAbsolute(fromSkill)) { fail(dir, "folder is inside the skill itself, write the plan in the project"); } From 4a7e888b72c6b0c09da42028e194349f55617cdb Mon Sep 17 00:00:00 2001 From: Baptiste LAFOURCADE Date: Fri, 23 Oct 2026 09:04:22 +0200 Subject: [PATCH 3/5] test(aidd-dev): strip template comments without a regex in the plan validator test CodeQL flags the multi-character comment replace as incomplete sanitization. Co-Authored-By: Claude Sonnet 5.5 --- scripts/__tests__/validate-plan.test.js | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/scripts/__tests__/validate-plan.test.js b/scripts/__tests__/validate-plan.test.js index 48aba16d2..66196c35f 100644 --- a/scripts/__tests__/validate-plan.test.js +++ b/scripts/__tests__/validate-plan.test.js @@ -18,9 +18,21 @@ let project; const template = (name, skill = SKILL) => fs.readFileSync(path.join(skill, "assets", name), "utf8"); +function withoutComments(text) { + let out = ""; + for (let i = 0; i < text.length; ) { + const open = text.indexOf("", open); + i = close < 0 ? text.length : close + 3; + while (text[i] === "\n") i += 1; + } + return out; +} + function fill(tpl, tree) { - return tpl - .replace(/\n*/g, "") + return withoutComments(tpl) .replace(/\{\{number\}\}/g, "1") .replace(/\{\{url\}\}/g, "https://example.test") .replace(/\| 1\.\.\. \|/g, "| 1 |") From e0030e751b446b030448cbbae9f825448b3e37d1 Mon Sep 17 00:00:00 2001 From: Baptiste LAFOURCADE Date: Fri, 23 Oct 2026 10:04:27 +0200 Subject: [PATCH 4/5] fix(aidd-dev): read a plan saved with Windows line endings in the plan validator A CRLF plan lost its front matter and table headers to the line-ending sensitive patterns and was rejected. Co-Authored-By: Claude Sonnet 5.5 --- plugins/aidd-dev/skills/01-plan/scripts/validate-plan.cjs | 7 ++++--- scripts/__tests__/validate-plan.test.js | 5 +++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/plugins/aidd-dev/skills/01-plan/scripts/validate-plan.cjs b/plugins/aidd-dev/skills/01-plan/scripts/validate-plan.cjs index 1d0613d8b..2873c0870 100644 --- a/plugins/aidd-dev/skills/01-plan/scripts/validate-plan.cjs +++ b/plugins/aidd-dev/skills/01-plan/scripts/validate-plan.cjs @@ -7,7 +7,8 @@ const path = require("path"); const errors = []; const fail = (file, rule) => errors.push(`${file}: ${rule}`); const norm = (s) => s.replace(/\s+/g, " ").trim(); -const asset = (name) => fs.readFileSync(path.join(__dirname, "..", "assets", name), "utf8"); +const read = (file) => fs.readFileSync(file, "utf8").replace(/\r\n/g, "\n"); +const asset = (name) => read(path.join(__dirname, "..", "assets", name)); function parse(md) { const fm = {}; @@ -114,7 +115,7 @@ if (!fromSkill.startsWith("..") && !path.isAbsolute(fromSkill)) { const planTpl = parse(asset("plan-template.md")); const phaseTpl = parse(asset("phase-template.md")); -const plan = matchTemplate("plan.md", fs.readFileSync(planFile, "utf8"), planTpl); +const plan = matchTemplate("plan.md", read(planFile), planTpl); const listed = (section(plan, "Phases")?.body.match(/^\|\s*\d+\s*\|/gm) || []).length; if (!listed) fail("plan.md", "no row in ## Phases"); @@ -141,7 +142,7 @@ for (let n = 1; n <= listed; n++) { const file = `phase-${n}.md`; const f = path.join(dir, file); if (!fs.existsSync(f)) continue; - const md = fs.readFileSync(f, "utf8"); + const md = read(f); const doc = matchTemplate(file, md, phaseTpl); checkTree(file, section(doc, "Architecture projection")?.body || ""); const frame = section(doc, "Wireframe")?.body; diff --git a/scripts/__tests__/validate-plan.test.js b/scripts/__tests__/validate-plan.test.js index 66196c35f..700cbaec3 100644 --- a/scripts/__tests__/validate-plan.test.js +++ b/scripts/__tests__/validate-plan.test.js @@ -81,6 +81,11 @@ describe("validate-plan", () => { assert.match(r.out, /^VALID$/); }); + it("accepts the same plan saved with Windows line endings", () => { + const files = Object.fromEntries(Object.entries(good()).map(([name, text]) => [name, text.replace(/\n/g, "\r\n")])); + assert.match(run(files).out, /^VALID$/); + }); + it("asks for the files when the folder holds no plan", () => { assert.match(run({}).out, /^INVALID\n.*plan\.md: missing/); }); From 0022f7bc61bf4adca26a4c00c30c41691d35cc21 Mon Sep 17 00:00:00 2001 From: Baptiste LAFOURCADE Date: Fri, 23 Oct 2026 11:01:18 +0200 Subject: [PATCH 5/5] refactor(aidd-dev): link capture to explore in the plan skill The plain "Go to explore" let a small model stop after capture to ask whether to continue. A markdown link makes the next action a file to read. Co-Authored-By: Claude Sonnet 5.5 --- plugins/aidd-dev/skills/01-plan/actions/01-capture.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/aidd-dev/skills/01-plan/actions/01-capture.md b/plugins/aidd-dev/skills/01-plan/actions/01-capture.md index 8455a3d13..adfcb9981 100644 --- a/plugins/aidd-dev/skills/01-plan/actions/01-capture.md +++ b/plugins/aidd-dev/skills/01-plan/actions/01-capture.md @@ -10,7 +10,7 @@ The user's request. 1. With no source to plan from (file, ticket, or text), ask for one and stop. 2. Read it, noting what it asks and the constraints it states. -3. Go to explore. +3. Read [explore](02-explore.md). ## Output