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..adfcb9981 --- /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. Read [explore](02-explore.md). + +## 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..2873c0870 --- /dev/null +++ b/plugins/aidd-dev/skills/01-plan/scripts/validate-plan.cjs @@ -0,0 +1,160 @@ +#!/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 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 = {}; + 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 (/", open); + i = close < 0 ? text.length : close + 3; + while (text[i] === "\n") i += 1; + } + return out; +} + +function fill(tpl, tree) { + return withoutComments(tpl) + .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("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/); + }); + + 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/); + }); +});