diff --git a/README.md b/README.md index 760238e8d..c0934ef38 100644 --- a/README.md +++ b/README.md @@ -236,7 +236,7 @@ Start with a functional need or User Story, then track it as an issue or ticket before planning. Shipping follows the project's own delivery process. Capture a learning only when it is durable enough to improve the next feature. -> ๐Ÿณ **More flows** โ†’ bundled recipes: [start a project](plugins/aidd-context/skills/12-cook/assets/recipes/start-a-project.md), [ship a feature](plugins/aidd-context/skills/12-cook/assets/recipes/ship-a-feature.md), and more. +> ๐Ÿณ **More flows** โ†’ bundled recipes: [MCP installations](plugins/aidd-context/skills/12-cook/assets/recipes/mcp-installation.md), [token optimization](plugins/aidd-context/skills/12-cook/assets/recipes/token-optimization.md), and [installing AIDDy](plugins/aidd-context/skills/12-cook/assets/recipes/install-aiddy-in-codex.md). ## ๐Ÿงฉ Plugins @@ -338,7 +338,7 @@ Full catalog โ†’ [`CATALOG.md`](docs/CATALOG.md). | | | | --- | --- | -| ๐Ÿณ **Recipes** | Bundled how-to sheets: [start a project](plugins/aidd-context/skills/12-cook/assets/recipes/start-a-project.md), [ship a feature](plugins/aidd-context/skills/12-cook/assets/recipes/ship-a-feature.md), [MCP installations](plugins/aidd-context/skills/12-cook/assets/recipes/mcp-installation.md), [token optimization](plugins/aidd-context/skills/12-cook/assets/recipes/token-optimization.md). Project recipes created by cook live in `aidd_docs/recipes/`. | +| ๐Ÿณ **Recipes** | Bundled how-to sheets: [MCP installations](plugins/aidd-context/skills/12-cook/assets/recipes/mcp-installation.md), [token optimization](plugins/aidd-context/skills/12-cook/assets/recipes/token-optimization.md), [install AIDDy](plugins/aidd-context/skills/12-cook/assets/recipes/install-aiddy-in-codex.md). Project recipes created by cook live in `aidd_docs/recipes/`. | | ๐Ÿ›๏ธ **[Architecture](docs/ARCHITECTURE.md)** | How the framework composes: plugins, skills, hooks, agents. | | ๐Ÿงฉ **[Create a plugin](docs/CREATE_PLUGIN.md)** | Build and publish your own. | | ๐Ÿ›’ **[Marketplace](docs/MARKETPLACE.md)** | Install scopes, versioning, LLM tiers. | diff --git a/aidd_docs/tasks/2026_10/2026_10_01_cook-pr-cleanup/review.md b/aidd_docs/tasks/2026_10/2026_10_01_cook-pr-cleanup/review.md new file mode 100644 index 000000000..9368716e8 --- /dev/null +++ b/aidd_docs/tasks/2026_10/2026_10_01_cook-pr-cleanup/review.md @@ -0,0 +1,41 @@ +# Review: Cook recipe authoring cleanup + +- **Verdict**: approve +- **Diff**: `ff28a45c...HEAD` +- **Axes run**: code, functional, relevancy +- **Date**: 2026_10_01 +- **Findings**: 0 critical, 0 warning, 0 minor + +## Phases + +### Phase 1: Recipe authoring + +- [x] Restore the original English Markdown scaffold with optional sections: `plugins/aidd-context/skills/12-cook/assets/recipe-template.md:1`. +- [x] Keep nine concise contract principles without duplicated scaffolding: `plugins/aidd-context/skills/12-cook/references/recipe-contract.md:3`. +- [x] Preserve standalone routes, repair routing, action tests, and catalog consistency: `plugins/aidd-context/skills/12-cook/SKILL.md:9`, `plugins/aidd-context/skills/12-cook/actions/02-upsert.md:28`, `plugins/aidd-context/skills/12-cook/actions/05-validate.md:22`. + +### Phase 2: Reliable validation + +- [x] Accept useful Markdown variants while reporting broken structure, examples, and links: `scripts/__tests__/validate-recipe.test.js:139`. +- [x] Read and inspect the same file descriptor, close it, and report read failures: `plugins/aidd-context/skills/12-cook/scripts/validate-recipe.mjs:39`. +- [x] Run through symlinked entry points without executing on library import: `plugins/aidd-context/skills/12-cook/scripts/validate-recipe.mjs:504`, `scripts/__tests__/validate-recipe.test.js:391`. +- [x] Remove punctuation-based description false positives and keep the RTK recipe correction scoped to step 19: `scripts/__tests__/validate-recipe.test.js:208`, `plugins/aidd-context/skills/12-cook/assets/recipes/token-optimization.md:383`. + +## Findings + +None. + +## Verification + +| Metric | Value | +| --- | --- | +| Verified | 7/7 cleanup criteria | +| Files checked | Nine staged cleanup files; independent checker reviewed code, behavior, and relevance | +| Unchecked | None within the bounded cleanup review | +| Unplanned | None | +| Checker execution | 27 guarded validator tests passed; bundled validation returned `PASS: 3 recipe(s) validated.` | +| Main execution | Global pre-commit passed: 576 script tests, 140 CLI architecture tests, typecheck, lint, manifests, paths, and links | +| Snippet syntax | 4 JSON, 2 YAML, 8 TOML, and 9 shell examples parsed successfully | +| Distribution execution | Fresh Codex flat and Claude marketplace builds each returned the exact three-recipe PASS output | +| CI follow-up | The character-based heading parser and LF/CRLF template assertion were independently approved after the first remote run exposed those gaps | +| Limits | CodeQL and native Windows remain to be confirmed on the follow-up commit after push; interactive client workflows were not executed | diff --git a/docs/CATALOG.md b/docs/CATALOG.md index 5bc2852a4..d06e8928d 100644 --- a/docs/CATALOG.md +++ b/docs/CATALOG.md @@ -32,7 +32,7 @@ Bootstrap, project init, context-artifact generation, diagrams, learning, and ex | `09-mermaid` | Generate Mermaid diagrams via a plan-validate workflow | `01-mermaid` | | `10-learn` | Capture learnings, conventions, and decisions into memory, decisions, rules | `01-gather`, `02-assess`, `03-write`, `04-sync` | | `11-explore` | Survey the project across tooling, context, and codebase, then drill into one axis | `01-survey`, `02-drill` | -| `12-cook` | Manage project and bundled recipes: list, create/update, research, or apply one | `01-list`, `02-upsert`, `03-research`, `04-apply` | +| `12-cook` | Manage and validate technical project or bundled recipes | `01-list`, `02-upsert`, `03-research`, `04-apply`, `05-validate` | ## ๐Ÿ’ป aidd-dev diff --git a/lefthook.yml b/lefthook.yml index 00d136edb..2b08c0561 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -53,6 +53,13 @@ pre-commit: exit 1 fi done + recipe-validity: + run: | + if ! command -v node >/dev/null 2>&1; then + echo "โŒ node not available; cannot validate recipes" + exit 1 + fi + node plugins/aidd-context/skills/12-cook/scripts/validate-recipe.mjs --all check-skill-argument-hints: glob: "plugins/*/skills/**" run: | diff --git a/plugins/aidd-context/CATALOG.md b/plugins/aidd-context/CATALOG.md index d549a084e..b8ba3b0b1 100644 --- a/plugins/aidd-context/CATALOG.md +++ b/plugins/aidd-context/CATALOG.md @@ -207,11 +207,13 @@ Auto-generated index of skills, agents, references and assets shipped by the `ai | `actions` | [02-upsert.md](skills/12-cook/actions/02-upsert.md) | - | | `actions` | [03-research.md](skills/12-cook/actions/03-research.md) | - | | `actions` | [04-apply.md](skills/12-cook/actions/04-apply.md) | - | +| `actions` | [05-validate.md](skills/12-cook/actions/05-validate.md) | - | | `assets` | [recipe-template.md](skills/12-cook/assets/recipe-template.md) | - | | `assets` | [research-checklist.md](skills/12-cook/assets/research-checklist.md) | - | | `assets` | [research-goal-checklist.md](skills/12-cook/assets/research-goal-checklist.md) | - | +| `assets` | [validation-report-template.md](skills/12-cook/assets/validation-report-template.md) | - | | `references` | [recipe-contract.md](skills/12-cook/references/recipe-contract.md) | - | | `references` | [recipe-locations.md](skills/12-cook/references/recipe-locations.md) | - | | `references` | [research-playbook.md](skills/12-cook/references/research-playbook.md) | - | -| `-` | [SKILL.md](skills/12-cook/SKILL.md) | `Manage project recipes/how-to sheets by listing, creating, updating, researching, or applying a recipe. Use for recipe, cook, /cook, list, new, update, research, apply.` | +| `-` | [SKILL.md](skills/12-cook/SKILL.md) | `Manages project recipes and practical guides. Use when the user wants to find a recipe, document a technique, research improvements, follow an existing guide, or check that its steps are usable.` | diff --git a/plugins/aidd-context/README.md b/plugins/aidd-context/README.md index 4eb56548d..8b723c276 100644 --- a/plugins/aidd-context/README.md +++ b/plugins/aidd-context/README.md @@ -21,7 +21,7 @@ Covers project bootstrap, the project memory bank, generation of context artifac | [1.4] | [mermaid](skills/09-mermaid/SKILL.md) | Generate high-quality Mermaid diagrams from markdown content using a structured plan-validate workflow. | | [1.5] | [learn](skills/10-learn/SKILL.md) | Capture durable learnings from the conversation or git history, score each, and route the worthwhile ones to memory, a decision record, a rule, or a new skill. | | [1.6] | [explore](skills/11-explore/SKILL.md) | Survey the project across three axes (tooling, context, codebase), then drill into one axis and point to the best-matching item for a goal. | -| [1.7] | [cook](skills/12-cook/SKILL.md) | Maintain project recipes in `aidd_docs/recipes/` and bundled recipes shipped with the skill: list, research, create/update, or apply one. | +| [1.7] | [cook](skills/12-cook/SKILL.md) | Maintain project and bundled recipes: list, research, create/update, apply, or validate one or all. | ## Onboarding diff --git a/plugins/aidd-context/skills/12-cook/SKILL.md b/plugins/aidd-context/skills/12-cook/SKILL.md index e9f92d63e..80a416143 100644 --- a/plugins/aidd-context/skills/12-cook/SKILL.md +++ b/plugins/aidd-context/skills/12-cook/SKILL.md @@ -1,31 +1,51 @@ --- name: 12-cook -description: Manage project recipes/how-to sheets by listing, creating, updating, researching, or applying a recipe. Use for recipe, cook, /cook, list, new, update, research, apply. +description: Manages project recipes and practical guides. Use when the user wants to find a recipe, document a technique, research improvements, follow an existing guide, or check that its steps are usable. argument-hint: recipe --- # Cook -Maintains recipe how-to sheets. Project recipes live in `aidd_docs/recipes/`; bundled recipes ship inside this skill under `assets/recipes/`. +```mermaid +flowchart LR + unnamed([unnamed recipe]) --> list + named-new([new recipe]) --> research + named-update([update recipe]) --> research + named-research([research topic]) --> research + named-apply([apply recipe]) --> apply + named-validate([validate recipe or all]) --> validate + list -->|list only| listed([listed]) + list -->|create, update, or reselect for research| research + list -->|select or reselect to apply| apply + list -->|resume dedup| upsert + list -->|reselect to validate| validate + research -->|unnamed or stale number| list + research -->|standalone research| researched([researched]) + research -->|create, update, or selected insights| upsert + upsert -->|new or substantial, missing verified results| research + upsert -->|new, before dedup| list + upsert -->|written| validate + validate -->|stale number| list + validate -->|standalone, pass| validated([validated]) + validate -->|standalone, findings| findings([findings]) + validate -->|upsert, pass| saved([saved]) + validate -->|upsert, findings: repair at Scaffold| upsert + apply -->|unnamed or stale number| list + apply -->|report only or chosen steps complete| reported([reported]) +``` ## Actions -| # | Action | Role | Input | -| --- | ---------- | ----------------------------------------------------------- | --------------------- | -| 01 | `list` | List every recipe as a table | none | -| 02 | `upsert` | Create or update one recipe from the template | recipe topic + fields | -| 03 | `research` | Survey modern alternatives, gaps, and counter-intuitive wins | recipe or topic | -| 04 | `apply` | Execute a recipe on the project as a confirmed todo list | recipe | +Run the flow above. Read only the next action file. -Run `list` to survey project and bundled recipes, `research` to gather insights, `upsert` to author one, `apply` to run an existing one against the project. Always run `research` before authoring or substantially updating a recipe โ€” never draft from memory alone. Run `list` first when the user names no recipe. -Before running an action, read its file in `actions/`, not only the table or assets. +| Action | Does | +| --- | --- | +| list | list project and bundled recipes | +| research | research one recipe or topic | +| upsert | create or update one recipe | +| apply | apply one existing recipe | +| validate | validate one or all recipes | -## References +## Transversal rules -- `references/recipe-locations.md`: where project and bundled recipes live, how resolution works, and when writes target each home. -- `references/recipe-contract.md`: the rules every recipe file follows; `upsert` writes to it. - -## Assets - -- `assets/recipe-template.md`: the canonical recipe scaffold `upsert` renders from, and the shape `list` parses. -- `assets/recipes/`: bundled recipes shipped with this skill. +- Never maintain a separate recipe index. diff --git a/plugins/aidd-context/skills/12-cook/actions/01-list.md b/plugins/aidd-context/skills/12-cook/actions/01-list.md index 042919a82..7583a11e6 100644 --- a/plugins/aidd-context/skills/12-cook/actions/01-list.md +++ b/plugins/aidd-context/skills/12-cook/actions/01-list.md @@ -4,25 +4,29 @@ List project recipes and bundled recipes as one table. ## Output +A numbered table of project and bundled recipes, or `No recipes yet.` + ```md | # | Recipe | Source | Description | | ---: | --- | --- | --- | | | [](<path>) | project \| bundled | <description> | ``` -One row per recipe file, sorted by source then file name, numbered from 1 after sorting. If both homes are absent or empty: `No recipes yet.` - ## Process -1. **Read.** Read every recipe in both homes of [recipe-locations.md](../references/recipe-locations.md), excluding `README.md`. +1. **Read.** Read every recipe in both homes of [recipe-locations.md](../references/recipe-locations.md). + - Exclude `README.md`. 2. **Title.** Pull the H1 title and the one-sentence description right below it. -3. **Shadow.** Mark a project row active and its bundled twin shadowed when both share a slug. -4. **Number.** Sort, then assign contiguous numbers from 1 to N. -5. **Render.** Render the table above. +3. **Shadow.** Mark matching-slug project rows active and their bundled twins shadowed. +4. **Number.** Sort by source then file name and assign contiguous numbers from 1 to N. +5. **Render.** Render one row per recipe file using the table above. + - If both homes are absent or empty, return `No recipes yet.` without an error. ## Test -- One row per project and bundled recipe file, each with number, title, source, and description. -- A project recipe with the same slug as a bundled recipe is marked active and overrides the bundled copy. -- Numbers are contiguous, start at 1, and match the displayed sort order. -- Absent/empty project and bundled homes โ†’ `No recipes yet`, no error. +| Case | Pass | +| --- | --- | +| Project and bundled recipe files | Each file has one row with number, title, source, and description | +| Matching project and bundled slugs | The project copy is active and the bundled copy is shadowed | +| Numbered rows | Numbers are contiguous from 1 and follow source then file name order | +| Both homes absent or empty | `No recipes yet.` is returned without an error | diff --git a/plugins/aidd-context/skills/12-cook/actions/02-upsert.md b/plugins/aidd-context/skills/12-cook/actions/02-upsert.md index 9e95ad9f9..8613fbd6a 100644 --- a/plugins/aidd-context/skills/12-cook/actions/02-upsert.md +++ b/plugins/aidd-context/skills/12-cook/actions/02-upsert.md @@ -1,10 +1,10 @@ # 02 - Upsert recipe -Create or update one project recipe at `aidd_docs/recipes/<slug>.md`, scaffolded from the recipe template and following the recipe contract. +Create or update one recipe from verified research. ## Input -The recipe topic. Ask for any missing field (description, steps, verify, related) before writing. +The recipe topic and any verified results from `research`. ## Output @@ -12,19 +12,30 @@ The recipe file at `aidd_docs/recipes/<slug>.md`, filled from the template. ## Process -1. **Research.** For a new recipe or any substantial update, run `research` (03) on the topic and draft only from its verified results, never from memory. +1. **Evidence.** Use verified research results for a new recipe or any substantial update. + - Never draft those changes from memory. 2. **Slug.** Derive a kebab-case `<slug>` from the topic. 3. **Resolve.** Resolve existing recipes with [recipe-locations.md](../references/recipe-locations.md). - The project recipe exists: update `aidd_docs/recipes/<slug>.md` in place. - - Only a bundled recipe exists: ask whether to copy it into `aidd_docs/recipes/<slug>.md` or edit the bundled one. Edit a bundled recipe only when the user asks for that framework-source change. -4. **Dedup.** For a new recipe, run `list` and rate each near match in an overlap table `| Existing recipe | Source | Shared scope | Overlap |`, where `Overlap` is none, partial, or high. + - If only a bundled recipe exists, ask whether to copy it into `aidd_docs/recipes/<slug>.md` or edit the bundled one. + - Edit a bundled recipe only on an explicit framework-source change request. +4. **Ask.** Ask only for a missing decision that changes the recipe's outcome or scope. +5. **Dedup.** Compare a new recipe with each near match in the current recipe list using `| Existing recipe | Source | Shared scope | Overlap |`. + - Compare before scaffolding. + - Use none, partial, or high for `Overlap`. - On any `high`, recommend updating that recipe instead and ask update-or-create before scaffolding. -5. **Scaffold.** Scaffold from [recipe-template.md](../assets/recipe-template.md) when needed, applying [recipe-contract.md](../references/recipe-contract.md) to every section. -6. **Fill.** Fill every placeholder. Never maintain a separate recipe index; `list` reads the files directly. +6. **Scaffold.** Use [recipe-template.md](../assets/recipe-template.md) when needed and apply [recipe-contract.md](../references/recipe-contract.md). + - Preserve verified useful content on updates. +7. **Fill.** Fill every placeholder and save the recipe. + - On validation findings, resume at Scaffold and Fill, reusing verified research until both checks pass. ## Test -- A new or substantially-updated recipe is drafted from `research` results, not from memory. -- `aidd_docs/recipes/<slug>.md` exists and follows the recipe contract: opens with a one-sentence description (no Goal label, no table), each step a `#### N)` emoji heading with a real example, no `<...>` placeholder left. -- A bundled recipe is never overwritten unless the user explicitly asks to change a bundled/framework recipe. -- A new recipe that highly overlaps an existing project or bundled recipe triggers an update-or-create prompt before scaffolding. +| Case | Pass | +| --- | --- | +| New or substantially updated recipe | The draft uses verified research results rather than memory | +| Project recipe written | `aidd_docs/recipes/<slug>.md` exists and passes the recipe contract | +| Update or validation repair | Verified useful content and research results are preserved | +| Validation after writing | Both checks pass and no finding is silently waived | +| Bundled recipe selected | It is overwritten only on an explicit bundled/framework change request | +| High overlap with an existing recipe | An update-or-create prompt precedes scaffolding | diff --git a/plugins/aidd-context/skills/12-cook/actions/03-research.md b/plugins/aidd-context/skills/12-cook/actions/03-research.md index de77ccd30..323cbc1f3 100644 --- a/plugins/aidd-context/skills/12-cook/actions/03-research.md +++ b/plugins/aidd-context/skills/12-cook/actions/03-research.md @@ -1,32 +1,37 @@ # 03 - Research alternatives -Refine the target recipe with a checklist, scout for the highest-value insights, verify each one exists, and propose them as sorted lists. +Research verified improvements to one recipe or topic. ## Input -The recipe or topic to modernize, named by number from the latest `list`, slug, title, or topic. If a recipe exists, resolve it from project recipes first, then bundled recipes. +The recipe or topic to modernize, named by number from the latest list, slug, title, or topic. ## Output -Three parts, then a recommendation: - -1. An alternatives table `| Alternative | What it is | Pros | Cons | Official link |`, sorted by value. -2. A coverage-gaps list: important sub-topics the recipe omits, each with why it matters. -3. A counter-intuitive wins list: surprising tips, each with the result it produces. - -Every presented item is confirmed to exist, with its latest state and official link. Ephemeral: nothing is written to either recipe home. +Verified alternatives, coverage gaps, and counter-intuitive wins sorted by value, with a recommendation. ## Process -1. **Refine.** Fill [research-goal-checklist.md](../assets/research-goal-checklist.md) with the user until the target is precise: outcome, level, scope, grouping. Resolve and read the recipe with [recipe-locations.md](../references/recipe-locations.md) when it exists. Run `list` when it is unnamed. -2. **Fan out.** Spawn one agent per angle in [research-playbook.md](../references/research-playbook.md) via the `Task` tool. Each applies the playbook criteria (freshness, community signal, tips), pushes for the most insights it can, and includes counter-intuitive ones with evidence. Each returns candidates with sources. -3. **Curate.** Dedupe the candidates. Drop anything that neither beats nor extends the recipe. Sort each bucket by value. Clear [research-checklist.md](../assets/research-checklist.md): gaps filled, unknowns surfaced, claims corroborated. -4. **Verify.** Spawn one agent per surviving candidate via the `Task` tool to confirm it exists, capture its official link, and record its latest state (version or date). Drop any candidate that cannot be confirmed against an official source. -5. **Present.** Render the alternatives table, the coverage-gaps list, and the counter-intuitive wins list, each item carrying its official link, then state a recommendation and why. -6. **Hand off.** If the user picks insights to keep, route to `upsert` to fold them into the recipe. +1. **Refine.** Fill [research-goal-checklist.md](../assets/research-goal-checklist.md) with the user until the outcome, level, scope, and grouping are precise. + - Resolve and read an existing recipe with [recipe-locations.md](../references/recipe-locations.md). +2. **Scout.** Cover every angle in [research-playbook.md](../references/research-playbook.md), returning candidates with sources. + - The caller may isolate or parallelize independent angles; no particular delegation mechanism is required. +3. **Curate.** Dedupe the candidates and sort each bucket by value. + - Drop anything that neither beats nor extends the recipe. + - Clear [research-checklist.md](../assets/research-checklist.md): gaps filled, unknowns surfaced, claims corroborated. +4. **Verify.** Apply the playbook's candidate checks to confirm each surviving item's existence, latest state, and official link. + - Drop anything that cannot be confirmed against an official source. +5. **Present.** Render the three parts below, each sorted by value with official links, then state a recommendation and why. + - Alternatives table: `| Alternative | What it is | Pros | Cons | Official link |`. + - Coverage-gaps list: omitted sub-topics and why each matters. + - Counter-intuitive wins list: surprising tips and the result each produces. + - Keep research ephemeral; do not write files. ## Test -- The output has an alternatives table with pros and cons, a coverage-gaps list, and a counter-intuitive wins list, plus an explicit recommendation, and nothing is written to disk. -- Every presented item carries an official link and was confirmed to exist; unverifiable candidates are dropped. -- The research checklist clears (gaps filled, unknowns surfaced, claims confirmed) before any hand-off to `upsert`. +| Case | Pass | +| --- | --- | +| Completed research | Alternatives with pros and cons, coverage gaps, counter-intuitive wins, and a recommendation are presented without writing files | +| Presented items | Each exists in its latest verified state and carries an official link; unverifiable candidates are dropped | +| Candidate evidence | Every candidate clears the playbook's evidence and transferability criteria | +| Research completion | The checklist clears with gaps filled, unknowns surfaced, and claims confirmed before any write hand-off | diff --git a/plugins/aidd-context/skills/12-cook/actions/04-apply.md b/plugins/aidd-context/skills/12-cook/actions/04-apply.md index 16ea664d2..201d08d35 100644 --- a/plugins/aidd-context/skills/12-cook/actions/04-apply.md +++ b/plugins/aidd-context/skills/12-cook/actions/04-apply.md @@ -1,6 +1,6 @@ # 04 - Apply recipe -Analyse a chosen recipe, ask the user what to do, then run the agent-doable steps and report the rest. +Apply chosen recipe steps and report what remains for the human. ## Input @@ -8,18 +8,26 @@ The recipe to apply, named by number from the latest `list`, slug, title, or top ## Output -A short analysis of the recipe โ€” what it achieves, and which steps the agent can run versus which are the human's โ€” then, on the user's choice, the chosen steps carried out and a report of what is left for the human. +A recipe analysis and, on the user's choice, completed steps with a report of remaining human steps. ## Process -1. **Locate.** Resolve the recipe with [recipe-locations.md](../references/recipe-locations.md) and read it. Run `list` first when the recipe is unnamed or when the user gives a number but no current numbered list is available. -2. **Analyse.** Read each step and classify it: agent-doable (a file edit, a config change) or human-only (a TUI command, an install, anything needing the user's terminal or UI). Summarise what the recipe achieves and what is in scope for the agent. -3. **Ask.** Show the analysis and ask the user what to do โ€” run all agent-doable steps, a subset, or just report. Never mutate before this answer. -4. **Execute.** For the chosen steps, work them as a tracked todo list, pausing for confirmation on any step that changes a file. Leave the human-only steps untouched. +1. **Locate.** Resolve and read the recipe with [recipe-locations.md](../references/recipe-locations.md). +2. **Analyse.** Classify each step as agent-doable or human-only and summarise the recipe's outcome and the agent's scope. + - Agent-doable examples: a file edit or configuration change. + - Human-only examples: a TUI command, an install, or anything requiring the user's terminal or UI. +3. **Ask.** Show the analysis and ask whether to run all agent-doable steps, a subset, or just report. + - Never mutate before this answer. +4. **Execute.** Carry out the chosen agent-doable steps as a tracked todo list. + - Pause for confirmation on any step that changes a file. + - Leave human-only steps untouched. 5. **Report.** Report what was done, list the human-only steps as instructions for the user, and run any `## Verify` checks. ## Test -- Applying a recipe first produces an analysis that marks each step agent-doable or human-only, then asks the user what to do before any change. -- A numeric choice maps to the matching row from the latest `list`; if no current list exists, `apply` reruns `list` and asks for the number again. -- The chosen agent-doable steps run as a todo list; human-only steps are reported as instructions, never executed. +| Case | Pass | +| --- | --- | +| Recipe selected | Each step is classified and the user chooses what to run before any change | +| Numeric selection | It matches the latest list row; a missing current list is refreshed and the user selects again | +| Chosen agent-doable steps | They run as a tracked todo list, with confirmation before each file-changing step | +| Human-only steps | They are reported as instructions and never executed | diff --git a/plugins/aidd-context/skills/12-cook/actions/05-validate.md b/plugins/aidd-context/skills/12-cook/actions/05-validate.md new file mode 100644 index 000000000..80254b7bf --- /dev/null +++ b/plugins/aidd-context/skills/12-cook/actions/05-validate.md @@ -0,0 +1,31 @@ +# 05 - Validate recipes + +Check one or all recipes against their structure and writing rules. + +## Input + +The recipe name, title, path, or `all`. + +## Output + +A report filled from [validation-report-template.md](../assets/validation-report-template.md). + +## Process + +1. **Resolve.** Resolve one recipe with [recipe-locations.md](../references/recipe-locations.md), or keep `all` as the full project-plus-bundled scope. + - Keep validation read-only; never repair, reformat, or rewrite a recipe during this action. +2. **Check structure.** Resolve `../scripts/validate-recipe.mjs` from this loaded action file's directory and invoke `node <resolved-script-path> <resolved-recipe-path>` or `node <resolved-script-path> --all` from the project root, preserving its exit code and findings. +3. **Check semantics.** Record one line-specific finding per violation of the Writing, Steps, and Evidence rules in [recipe-contract.md](../references/recipe-contract.md). + - For non-JSON snippets, use available native YAML, TOML, and shell parsers and record which languages could not be checked mechanically. +4. **Report.** Fill the report template with merged deterministic and semantic findings, or the success summary. + - An unavailable optional parser is disclosed but does not fail an otherwise valid recipe. + - Do not suppress a finding because it requires editorial judgment. + +## Test + +| Case | Pass | +| --- | --- | +| One valid recipe or `all` | PASS is returned without changing tracked files | +| Structural failure | The table includes file, line, rule, and fix, with a non-zero exit code | +| Semantic failure after deterministic PASS | The finding appears in the same table | +| Snippet syntax | JSON is parsed mechanically; available YAML, TOML, and shell tools are used, and unavailable parsers are disclosed | diff --git a/plugins/aidd-context/skills/12-cook/assets/recipe-template.md b/plugins/aidd-context/skills/12-cook/assets/recipe-template.md index 95a189c41..85402afd7 100644 --- a/plugins/aidd-context/skills/12-cook/assets/recipe-template.md +++ b/plugins/aidd-context/skills/12-cook/assets/recipe-template.md @@ -2,6 +2,8 @@ <One sentence describing what this recipe gets the reader.> +> Fill every placeholder and remove these instructions. Omit Why, Verify, and difficulty categories when they add no value; without categories, use direct `### N) <emoji> Title` steps. + ## Why <Short and benefit-first, one idea per line. Lead with the keywords a reader would search, **bold** the key terms.> diff --git a/plugins/aidd-context/skills/12-cook/assets/recipes/install-aiddy-in-codex.md b/plugins/aidd-context/skills/12-cook/assets/recipes/install-aiddy-in-codex.md index 0c92c2cfb..a44b2a0e3 100644 --- a/plugins/aidd-context/skills/12-cook/assets/recipes/install-aiddy-in-codex.md +++ b/plugins/aidd-context/skills/12-cook/assets/recipes/install-aiddy-in-codex.md @@ -1,27 +1,22 @@ # Install AIDDy in Codex -Install AIDDy, the Agent Y mascot, as a local Codex pet. +Install AIDDy, the Agent Y mascot, as a local custom pet in the ChatGPT desktop app. -## Why - -**AIDDy** reflects Codex task activity with the AI-Driven Development mascot. - -**Sprite version 2** ensures Codex reads the extended 11-row animation atlas correctly. +<!-- Sources checked: 2026-08-07. --> ## Steps to install and wake AIDDy -#### 1) ๐Ÿ”— Open the AIDDy installer +### 1) ๐Ÿ”— Open the AIDDy installer -The versioned deep link opens the Codex pet installer with the canonical AIDDy atlas. +The immutable deep link opens the canonical 11-row AIDDy atlas with sprite version 2. -1. Use a machine with the Codex desktop app and internet access. -2. Click [Install AIDDy](codex://pets/install?name=AIDDy&imageUrl=https%3A%2F%2Fraw.githubusercontent.com%2Fai-driven-dev%2Fframework%2Fmain%2Fassets%2Fpets%2Faiddy-spritesheet.webp&description=Agent%20Y%20for%20AI-Driven%20Development&spriteVersionNumber=2). +1. In the ChatGPT desktop app, click [Install AIDDy](codex://pets/install?name=AIDDy&imageUrl=https%3A%2F%2Fraw.githubusercontent.com%2Fai-driven-dev%2Fframework%2F1a52770253628062e4e8cbcda1bd062452354c27%2Fassets%2Fpets%2Faiddy-spritesheet.webp&description=Agent%20Y%20for%20AI-Driven%20Development&spriteVersionNumber=2). ```text -codex://pets/install?name=AIDDy&imageUrl=https%3A%2F%2Fraw.githubusercontent.com%2Fai-driven-dev%2Fframework%2Fmain%2Fassets%2Fpets%2Faiddy-spritesheet.webp&description=Agent%20Y%20for%20AI-Driven%20Development&spriteVersionNumber=2 +codex://pets/install?name=AIDDy&imageUrl=https%3A%2F%2Fraw.githubusercontent.com%2Fai-driven-dev%2Fframework%2F1a52770253628062e4e8cbcda1bd062452354c27%2Fassets%2Fpets%2Faiddy-spritesheet.webp&description=Agent%20Y%20for%20AI-Driven%20Development&spriteVersionNumber=2 ``` -#### 2) ๐Ÿ“ฆ Confirm the installation +### 2) ๐Ÿ“ฆ Confirm the installation The confirmation screen lets you verify the pet before writing it to local Codex storage. @@ -34,14 +29,23 @@ Description: Agent Y for AI-Driven Development Sprite version: 2 ``` -#### 3) ๐Ÿพ Wake AIDDy +The public pet reference documents installation but no removal command; use the current **Settings > Pets** removal control when available. + +### 3) ๐Ÿพ Select AIDDy + +Selecting the installed custom pet makes it active in the ChatGPT desktop app. + +1. Open **Settings > Pets**, select **Refresh** if needed, then choose **AIDDy**. + +```text +Settings > Pets > AIDDy +``` + +### 4) โ–ถ๏ธ Wake AIDDy -Selecting the custom pet makes it available as the animated task companion documented in [Codex pets](https://learn.chatgpt.com/docs/pets). +The `/pet` command wakes the selected companion so it can report task activity. -1. Open **Settings > Pets**. -2. Refresh custom pets if AIDDy is not visible yet. -3. Select **AIDDy**. -4. Enter `/pet` in a task. +1. Enter `/pet` in a task. ```text /pet @@ -51,4 +55,5 @@ Selecting the custom pet makes it available as the animated task companion docum - AIDDy appears in **Settings > Pets** as a custom pet. - `/pet` shows AIDDy and its animation changes with task activity. -- The link uses only the supported [pet install parameters](https://learn.chatgpt.com/docs/reference/app-commands#pets), including `spriteVersionNumber=2`. +- The link uses only the supported [pet install parameters](https://learn.chatgpt.com/docs/reference/commands#pets), including `spriteVersionNumber=2`. +- The pinned atlas has SHA-256 `a97872f637a6711da4a88cbbb56f9038b2fcb4ce870e021c9d2355a3421ad5a4`. diff --git a/plugins/aidd-context/skills/12-cook/assets/recipes/mcp-installation.md b/plugins/aidd-context/skills/12-cook/assets/recipes/mcp-installation.md index 698280dd3..9952c474c 100644 --- a/plugins/aidd-context/skills/12-cook/assets/recipes/mcp-installation.md +++ b/plugins/aidd-context/skills/12-cook/assets/recipes/mcp-installation.md @@ -1,60 +1,146 @@ # MCP installations -Decide when to use an MCP server vs a CLI, and wire up the recommended ones. +Choose between a CLI and MCP by capability, then install the integration through the configuration native to your AI client. -## Why +<!-- Sources checked: 2026-08-07. --> -**MCP servers** load their full tool schema into every turn, which bloats the context window. +## Steps to install the right integration -**CLI calls** cost a few tokens and return only what you ask for. +### 1) ๐Ÿ”Ž Choose by capability -**Reach for MCP only when no CLI covers the service.** +Choose the smallest interface that fully covers the workflow, authentication model, and data scope. -**Audit what you install** before connecting any server. +1. Compare the exact operation you need against the table. -## Steps to choose and install the right integration +| Need | Prefer a CLI | Prefer MCP | +| --- | --- | --- | +| Scriptable commands and raw API calls | The official CLI covers the operation and its output is manageable | The client needs typed discovery or structured results | +| Live design or knowledge context | The CLI exposes the required operation and data | The server advertises the required tools, resources, or prompts | +| Authentication | The CLI's login and credential storage fit the workflow | The server's authentication, requested scopes, and host credential storage fit the workflow | +| Context cost | Output is requested only when the command runs | The client supports deferred loading such as [Claude Code Tool Search](https://code.claude.com/docs/en/mcp#scale-with-mcp-tool-search) | -#### 1) ๐Ÿ”Ž Check for a CLI first +The last column is a decision heuristic for this recipe, not a provider guarantee. -The CLI is usually cheaper because it does not inject a large tool schema into every turn. +| Service | CLI route | MCP route | Recipe heuristic | +| --- | --- | --- | --- | +| [GitHub](https://github.com/github/github-mcp-server) | [`gh`](https://cli.github.com/) | Remote toolsets, including read-only modes | CLI for routine API work; MCP for typed or remote-only capabilities | +| [Atlassian](https://www.atlassian.com/platform/rovo-mcp) | [`acli`](https://developer.atlassian.com/cloud/acli/guides/introduction/) for Jira | The current Rovo MCP tool catalog; [OAuth 2.1 is recommended](https://support.atlassian.com/atlassian-rovo-mcp-server/docs/configuring-oauth-2-1/), with API-token authentication only when an administrator enables it | Decide from the Atlassian product, operation, and permitted authentication | +| [Playwright](https://github.com/microsoft/playwright-mcp) | [`@playwright/cli`](https://playwright.dev/agent-cli/installation) | `@playwright/mcp` | CLI for direct browser automation; MCP for live tool calls | +| [Figma](https://developers.figma.com/docs/figma-mcp-server/remote-server-installation/) | The [`@figma/code-connect` CLI](https://developers.figma.com/docs/code-connect/cli-reference/) manages component mappings, not live design context or canvas operations | `https://mcp.figma.com/mcp` | CLI for Code Connect mappings; MCP for live design and canvas workflows | +| [Notion](https://developers.notion.com/guides/mcp/get-started-with-mcp) | The official [`ntn` CLI](https://developers.notion.com/cli/get-started/overview) supports API, page, data-source, file, and Worker operations | `https://mcp.notion.com/mcp` | CLI for scripts and raw API work; MCP for agent workflows, with human confirmation | -1. Look for an official CLI before adding an MCP server. -2. Install and authenticate the CLI when it covers the workflow. -3. Keep MCP for services with no useful CLI alternative. +### 2) โŒจ๏ธ Install the CLI route -| Service | Official MCP | CLI alternative | Recommended | -| --- | --- | --- | --- | -| **GitHub** | [`api.githubcopilot.com/mcp/`](https://github.com/github/github-mcp-server) | [`gh`](https://cli.github.com/) | **CLI**: issues, PRs, releases, and API calls without the MCP schema cost | -| **Atlassian** (Jira / Confluence) | [`mcp.atlassian.com/v1/mcp`](https://www.atlassian.com/platform/remote-mcp-server) | [`acli`](https://developer.atlassian.com/cloud/acli/guides/introduction/) for Jira | **CLI** for Jira, **MCP** for Confluence | -| **Playwright** | [`@playwright/mcp`](https://github.com/microsoft/playwright-mcp) | [`npx playwright`](https://playwright.dev/docs/test-cli) | **CLI**: a real browser through `playwright open`, `codegen`, or `--headed` | -| **Figma** | [`mcp.figma.com/mcp`](https://developers.figma.com/docs/figma-mcp-server/remote-server-installation/) | none for design data | **MCP** | -| **Notion** | [`mcp.notion.com/mcp`](https://developers.notion.com/guides/mcp/get-started-with-mcp) | none official | **MCP** | +The agent-oriented Playwright CLI is a direct browser alternative to Playwright MCP. + +1. Install `@playwright/cli` from the [official guide](https://playwright.dev/agent-cli/installation), then invoke its `playwright-cli` executable. + +```bash +npm install -g @playwright/cli@latest +playwright-cli open https://example.com --headed +npm uninstall -g @playwright/cli +``` + +Use `npx playwright test` for the test runner and `npx playwright codegen` for test generation; they are different command surfaces. + +### 3) ๐Ÿ”Œ Install the MCP route + +Use a provider-supported client and distinguish the recommended plugin route from manual MCP configuration. -#### 2) ๐Ÿ”Œ Add MCP only when it is the right integration +1. Pick the [documented Figma route](https://developers.figma.com/docs/figma-mcp-server/remote-server-installation/) for your client. -An MCP config belongs in the assistant's MCP configuration file, and only for services where MCP is the best available interface. +| Client | Figma's recommended route | Manual alternative and scope | +| --- | --- | --- | +| Claude Code | `claude plugin install figma@claude-plugins-official` | `claude mcp add --transport http --scope project figma https://mcp.figma.com/mcp` writes project configuration to `.mcp.json` under `mcpServers` | +| Codex | In the Codex app, open **Plugins**, select **+** next to Figma, then **Install Figma** | `codex mcp add figma --url https://mcp.figma.com/mcp` writes to [`~/.codex/config.toml`](https://learn.chatgpt.com/docs/extend/mcp?surface=cli) under `mcp_servers`; trusted projects may instead use `.codex/config.toml` | +| Cursor | Run `/add-plugin figma` | Manual project configuration uses `.cursor/mcp.json` under `mcpServers` | +| GitHub Copilot in VS Code | Run **MCP: Open Workspace Folder MCP Configuration**, paste Figma's documented server object, then select **Start** | Workspace configuration uses `.vscode/mcp.json` under `servers` | -1. Read the provider docs and permission model. -2. Add the server to `.mcp.json`. -3. Restart the assistant so it picks up the new tools. +```bash +claude plugin install figma@claude-plugins-official +``` + +[Figma accepts only clients in its MCP catalog](https://www.figma.com/mcp-catalog/); do not extrapolate these instructions to an unlisted host. + +### 4) ๐Ÿ” Restrict access + +Use the server's documented authentication, expose only required capabilities, and enforce confirmation for mutations. + +1. If the remote server uses OAuth, authorize it in the client and review the requested scopes; otherwise follow its documented authentication method. +2. Prefer read-only endpoints, toolsets, or allowlists when the workflow only reads. +3. Configure the host to require confirmation for mutating tools; if it cannot, disable write tools or use a provider-enforced read-only mode. + +[GitHub documents](https://github.com/github/github-mcp-server/blob/main/docs/server-configuration.md#read-only-mode) this VS Code-format server-entry fragment; place it under `servers.github` in `.vscode/mcp.json`, or adapt the wrapper to the selected host: ```json { - "mcpServers": { - "figma": { "url": "https://mcp.figma.com/mcp" } + "type": "http", + "url": "https://api.githubcopilot.com/mcp/", + "headers": { + "X-MCP-Toolsets": "repos,issues", + "X-MCP-Readonly": "true" } } ``` -#### 3) โœ… Verify the integration +[Notion MCP inherits the connected user's workspace access](https://developers.notion.com/guides/mcp/mcp-security-best-practices), so review every write and any data sent to another tool. -Verification keeps a bad install out of later sessions. +### 5) โœ… Verify the connection -1. For MCP, confirm the tools appear in the assistant. -2. For a CLI, run a read-only identity command. +Verify the configured server, authenticated identity, exposed tools, and one read-only operation before relying on it. + +1. Inspect the server through the client, then run a harmless read. + +```text +Claude Code: /mcp +Codex: codex mcp list +Cursor: Settings > Cursor Settings > Tools & MCP +VS Code: MCP: List Servers +``` + +Figma's documented successful Claude Code flow ends with: + +```text +Authentication successful. Connected to figma +``` + +Use Figma's read-only [`whoami`](https://developers.figma.com/docs/figma-mcp-server/tools-and-prompts/#whoami-remote-only) tool to confirm the authenticated email, plans, and seat type. + +### 6) ๐Ÿงน Remove local access + +Clearing authentication and local configuration stops the client from using the server. + +1. Clear authentication before removing the server entry. +2. Remove the server or plugin from the client. + +| Client | Clear authentication | Remove local configuration | +| --- | --- | --- | +| Claude Code | Run `/mcp`, select Figma, then **Clear authentication** | Plugin: `claude plugin uninstall figma@claude-plugins-official`; manual project server: `claude mcp remove --scope project figma` | +| Codex | Manual server: `codex mcp logout figma` | Plugin: remove Figma in the Codex app's **Plugins** panel; manual server: `codex mcp remove figma` | +| Cursor | Disconnect Figma under **Settings > Cursor Settings > Tools & MCP** | Remove the Figma plugin or its manual `.cursor/mcp.json` entry | +| GitHub Copilot in VS Code | Select the server through **MCP: List Servers** | Uninstall the server or remove its `.vscode/mcp.json` entry | ```bash -$ gh auth status -github.com - โœ“ Logged in to github.com account octocat +codex mcp logout figma +codex mcp remove figma ``` + +Removing configuration alone does not prove that the OAuth grant was revoked. + +### 7) ๐Ÿšซ Revoke provider access + +Revoking the provider grant invalidates the remote OAuth authorization independently of local configuration. + +1. For Figma, open [**Settings > Security > Connected apps**](https://help.figma.com/hc/en-us/articles/15021280611607-How-do-I-keep-my-account-secure). +2. Find the MCP client and select **Revoke access**. + +```text +Figma > Settings > Security > Connected apps > Revoke access +``` + +## Verify + +- The server appears in the chosen client's server list with the expected transport and scope. +- Authentication uses the intended account and workspace; for Figma, `whoami` returns that identity. +- Only the required tools are enabled; mutating calls require confirmation or are disabled. +- Authentication clearing, local removal, and provider revocation pass as three separate checks. diff --git a/plugins/aidd-context/skills/12-cook/assets/recipes/ship-a-feature.md b/plugins/aidd-context/skills/12-cook/assets/recipes/ship-a-feature.md deleted file mode 100644 index e07f7c513..000000000 --- a/plugins/aidd-context/skills/12-cook/assets/recipes/ship-a-feature.md +++ /dev/null @@ -1,79 +0,0 @@ -# Ship a feature end to end - -Take a feature from a rough idea to a reviewed, shipped pull request with the AIDD flow. - -## Why - -The common loop, at a glance โ€” the exact commands to run so you never wonder what comes next. - -> Prefer a guided walkthrough? `/aidd-context:00-onboard` inspects your project and routes you step by step instead of running the sequence by hand. - -## Steps to ship a feature - -#### 1) ๐Ÿ’ก Clarify - -Brainstorm turns the rough idea into a precise request. - -1. Run `/aidd-refine:01-brainstorm`. - -```text -/aidd-refine:01-brainstorm -``` - -#### 2) ๐Ÿ“‹ Plan - -Planning drafts the phased technical plan before implementation starts. - -1. Run `/aidd-dev:01-plan`. - -```text -/aidd-dev:01-plan -``` - -#### 3) ๐Ÿ”ง Implement - -Implementation writes the code phase by phase. - -1. Run `/aidd-dev:02-implement`. - -```text -/aidd-dev:02-implement -``` - -#### 4) ๐Ÿ” Review - -Review checks the diff before it ships. - -1. Run `/aidd-dev:05-review`. - -```text -/aidd-dev:05-review -``` - -#### 5) ๐Ÿ“ฆ Commit - -Commit records one atomic conventional change. - -1. Run `/aidd-vcs:01-commit`. - -```text -/aidd-vcs:01-commit -``` - -#### 6) โœ… Pull request - -The pull-request step opens the PR. - -1. Run `/aidd-vcs:02-pull-request`. - -```text -/aidd-vcs:02-pull-request -``` - -> One command for the whole loop: `/aidd-orchestrator:01-sdlc` runs frame when needed โ†’ plan โ†’ implement โ†’ validate โ†’ review โ†’ challenge โ†’ ship. - -## Verify - -- A pull request is open on your branch, with the diff reviewed and tests passing. - -Start with [Start a project](start-a-project.md) before your first feature. diff --git a/plugins/aidd-context/skills/12-cook/assets/recipes/start-a-project.md b/plugins/aidd-context/skills/12-cook/assets/recipes/start-a-project.md deleted file mode 100644 index b2710395a..000000000 --- a/plugins/aidd-context/skills/12-cook/assets/recipes/start-a-project.md +++ /dev/null @@ -1,69 +0,0 @@ -# Start a project (greenfield) - -Take a greenfield idea to a set-up project with its AIDD context, ready for the first feature. - -## Why - -The greenfield sequence, at a glance โ€” from a raw idea to a project the AIDD flow can build on. - -> Prefer a guided walkthrough? `/aidd-context:00-onboard` inspects your project and routes you step by step instead of running the sequence by hand. - -## Steps to start a project - -#### 1) ๐Ÿ’ก Brainstorm the idea - -Brainstorming sharpens the raw idea into a precise request. - -1. Run `/aidd-refine:01-brainstorm`. - -```text -/aidd-refine:01-brainstorm -``` - -#### 2) ๐Ÿ“„ Discover the product and draft the PRD - -The PRD turns the idea into structured product requirements. - -1. Run `/aidd-pm:06-product-brief`. -2. Pass its brief to `/aidd-pm:03-prd`. - -```text -/aidd-pm:06-product-brief -/aidd-pm:03-prd -``` - -#### 3) ๐Ÿ—๏ธ Design the architecture - -Bootstrap validates a stack through Q&A and outputs an `INSTALL.md`. - -1. Run `/aidd-context:01-bootstrap`. - -```text -/aidd-context:01-bootstrap -``` - -#### 4) ๐Ÿง  Build project memory - -Project memory creates the memory bank and AI context files. - -1. Run `/aidd-context:02-project-memory`. - -```text -/aidd-context:02-project-memory -``` - -#### 5) ๐Ÿš€ Ship the first feature - -The feature recipe takes the project through the per-feature loop. - -1. Follow [Ship a feature](ship-a-feature.md). - -```text -/aidd-orchestrator:01-sdlc -``` - -## Verify - -- `aidd_docs/memory/` holds the memory files, and an `INSTALL.md` describes the chosen stack. - -Use `/aidd-context:00-onboard` any time to see where the project sits. diff --git a/plugins/aidd-context/skills/12-cook/assets/recipes/token-optimization.md b/plugins/aidd-context/skills/12-cook/assets/recipes/token-optimization.md index afcfd3644..4ff71d782 100644 --- a/plugins/aidd-context/skills/12-cook/assets/recipes/token-optimization.md +++ b/plugins/aidd-context/skills/12-cook/assets/recipes/token-optimization.md @@ -1,277 +1,559 @@ -# Token optimization for AI IDEs +# Token optimization techniques + +Cut token usage across coding agents with client-specific commands and measurable checks. + +- [Token optimization techniques](#token-optimization-techniques) + - [Steps to cut token usage](#steps-to-cut-token-usage) + - [๐ŸŸข Beginner](#-beginner) + - [1) ๐Ÿ“ Measure session usage](#1--measure-session-usage) + - [2) ๐Ÿ“Ÿ Keep usage visible](#2--keep-usage-visible) + - [3) ๐Ÿ“Š Analyze local history](#3--analyze-local-history) + - [4) ๐Ÿ”Ž Inspect Claude's loaded context](#4--inspect-claudes-loaded-context) + - [5) โœ‚๏ธ Keep AGENTS.md and CLAUDE.md short](#5-๏ธ-keep-agentsmd-and-claudemd-short) + - [6) ๐ŸŽฏ Scope rules to matching files](#6--scope-rules-to-matching-files) + - [7) ๐Ÿงฉ Load skills only on demand](#7--load-skills-only-on-demand) + - [8) ๐Ÿง  Disable stale auto-memory](#8--disable-stale-auto-memory) + - [9) ๐Ÿชจ Compress instruction files with Caveman](#9--compress-instruction-files-with-caveman) + - [10) ๐Ÿงญ Plan before expensive work](#10--plan-before-expensive-work) + - [11) โ™ป๏ธ Reset unrelated work](#11-๏ธ-reset-unrelated-work) + - [12) ๐Ÿ—œ๏ธ Compact the same task](#12-๏ธ-compact-the-same-task) + - [๐ŸŸก Intermediate](#-intermediate) + - [13) ๐Ÿ“ˆ Export token telemetry](#13--export-token-telemetry) + - [14) ๐Ÿ—ฃ๏ธ Set native concise output](#14-๏ธ-set-native-concise-output) + - [15) ๐Ÿชจ Compress output with Caveman](#15--compress-output-with-caveman) + - [16) ๐Ÿง  Shape actionable output with i-have-adhd](#16--shape-actionable-output-with-i-have-adhd) + - [17) ๐Ÿงน Filter shell output with RTK](#17--filter-shell-output-with-rtk) + - [18) โœ‚๏ธ Filter shell output with SNIP](#18-๏ธ-filter-shell-output-with-snip) + - [19) ๐Ÿช Automate RTK where supported](#19--automate-rtk-where-supported) + - [20) โœ‹ Cap Codex tool history](#20--cap-codex-tool-history) + - [21) ๐Ÿšซ Block bulky paths](#21--block-bulky-paths) + - [22) ๐Ÿ”Œ Limit MCP exposure](#22--limit-mcp-exposure) + - [๐Ÿ”ด Expert](#-expert) + - [23) ๐Ÿ”ฌ Inspect model-visible behavior](#23--inspect-model-visible-behavior) + - [24) ๐ŸŽฏ Route model and effort by difficulty](#24--route-model-and-effort-by-difficulty) + - [25) ๐Ÿงซ Isolate noisy work with subagents](#25--isolate-noisy-work-with-subagents) + - [26) ๐ŸงŠ Preserve Claude prompt-cache prefixes](#26--preserve-claude-prompt-cache-prefixes) + - [27) โœ… Lower reasoning on routine work](#27--lower-reasoning-on-routine-work) + - [Verify the result](#verify-the-result) +<!-- Sources checked: 2026-08-07. --> -Cut token usage and cost in AI coding assistants without losing output quality. - -## Why optimize your tokens - -**Token usage** is the bill โ€” every turn re-sends your whole **context window** and you pay for it again. +## Steps to cut token usage -Most of it is **waste**: filler prose, noisy logs, stale context, bloated instruction files. +### ๐ŸŸข Beginner -Reach for this recipe when **cost** climbs faster than your output. +#### 1) ๐Ÿ“ Measure session usage -## Steps to cut token usage +Measure equivalent tasks end to end: input comes from instructions, history, tools, and logs; output comes from verbosity and reasoning; optimizers and subagents can be net-negative. -### ๐ŸŸข Beginner +| Client | Command | Measures | +| --- | --- | --- | +| Claude Code | `/usage` (`/cost`, `/stats`) | Session tokens, estimated cost, and supported attribution | +| Codex | `/status`; `/usage daily`, `/usage weekly`, `/usage cumulative` | Current context and account token activity | -#### 1) ๐Ÿ”Ž See what fills the window โ€” `/context` +See [Claude Code costs](https://code.claude.com/docs/en/costs) and [Codex commands](https://learn.chatgpt.com/docs/developer-commands?surface=cli). -`/context` paints your context as a grid, so you cut the biggest consumers instead of guessing. +#### 2) ๐Ÿ“Ÿ Keep usage visible -1. Run `/context` in Claude Code. -2. Find the heavy blocks: tool schemas, instruction files, long file reads. -3. Attack the biggest block first. +Use the native status line instead of asking the model for usage. ```text -$ /context - MCP tool schemas โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ 28% โ† biggest, cut first - file reads โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ 19% - CLAUDE.md โ–ˆโ–ˆโ–ˆโ–ˆ 9% -(illustrative โ€” replace with a screenshot of your real /context) +Claude Code: /statusline +Codex: /statusline ``` -#### 2) ๐Ÿ’ธ Read the bill โ€” `/cost` +Rollback: run Claude Code `/statusline clear`; remove Codex `tui.status_line` from `config.toml` to restore its default. + +![Claude Code status line showing context-window usage](https://mintcdn.com/claude-code/nibzesLaJVh4ydOq/images/statusline-context-window-usage.png?fit=max&auto=format&n=nibzesLaJVh4ydOq&q=85&s=15b58ab3602f036939145dde3165c6f7) + +See [Claude Code status lines](https://code.claude.com/docs/en/statusline). -`/cost` tells you what a session actually costs and where the spend goes. +#### 3) ๐Ÿ“Š Analyze local history -1. Run `/cost` (alias `/usage`). -2. Read the breakdown by skill, subagent, and MCP server. -3. Re-run it after a change to confirm the spend really dropped. +Use local logs when one session counter is insufficient. + +| Need | Tool | Command | +| --- | --- | --- | +| Aggregate supported coding agents | [`ccusage`](https://github.com/ccusage/ccusage) | `npx ccusage@latest` | +| Inspect individual Claude Code prompts | [`prompt-analytics-for-claude-code`](https://github.com/romainfjgaspard/prompt-analytics-for-claude-code) | `uvx --from prompt-analytics-for-claude-code prompt-analytics summary` | + +Local logs can contain prompt text. Treat exports as sensitive. + +![Prompt Analytics dashboard](https://raw.githubusercontent.com/romainfjgaspard/prompt-analytics-for-claude-code/main/docs/screenshots/dashboard-home.png) + +#### 4) ๐Ÿ”Ž Inspect Claude's loaded context + +Target the largest source shown by Claude Code. ```text -$ /cost - Session: $0.42 ยท 1.2M tokens - By: subagents 38% ยท MCP 21% ยท main 41% -(illustrative โ€” replace with a screenshot of your real /cost) +/context all +/memory ``` -#### 3) ๐Ÿ” Find your bad habits โ€” `/insights` +#### 5) โœ‚๏ธ Keep AGENTS.md and CLAUDE.md short -`/insights` analyses how you prompt โ€” probably sub-optimal โ€” so you fix the pattern, not one prompt. What you repeat every session belongs in the knowledge base, and the counter-intuitive habits you never noticed get surfaced so you can drop them. +Keep durable constraints and validation commands while removing architecture prose, task notes, and duplicated instructions. -1. Run `/insights`. -2. Move what you repeat into `CLAUDE.md` or a rule, and drop the habits it flags. +| Client | Persistent file | +| --- | --- | +| Claude Code | `CLAUDE.md` | +| Codex | `AGENTS.override.md` when present, otherwise `AGENTS.md` | +| GitHub Copilot | `.github/copilot-instructions.md` | -```text -$ /insights - โ€ข You restate the test command in ~60% of sessions โ†’ put it in CLAUDE.md - โ€ข Long "summary" turns inflate output โ†’ ask for terse replies -(illustrative โ€” replace with a screenshot of your real /insights) +```md +# Persistent instructions + +- Lead with the result. +- Preserve unrelated changes. +- Run the smallest relevant validation. +- Quote only the decisive error line. ``` -#### 4) ๐Ÿ“ˆ Track per-prompt with an analytics tool +See the `AGENTS.md` template supplied by the project-memory skill, [Claude Code memory](https://code.claude.com/docs/en/memory), and [Codex AGENTS.md](https://learn.chatgpt.com/docs/agent-configuration/agents-md). -Built-ins show one session; an analytics tool reads all your local logs and reveals where the bill truly sits. The lesson it surfaces: **cache reads dwarf input + output**, so caching, not generation, is most of the bill. +#### 6) ๐ŸŽฏ Scope rules to matching files -1. Pick one: [`prompt-analytics-for-claude-code`](https://github.com/romainfjgaspard/prompt-analytics-for-claude-code) or [`ccusage`](https://www.npmjs.com/package/ccusage). -2. Run it โ€” `uvx --from prompt-analytics-for-claude-code prompt-analytics summary` โ€” no setup, it parses `~/.claude`. +Move file-specific guidance out of the root instruction file. -![prompt-analytics dashboard โ€” cost by token type shows cache reads dominate the bill](https://raw.githubusercontent.com/romainfjgaspard/prompt-analytics-for-claude-code/main/docs/screenshots/dashboard-home.png) +Claude Code `.claude/rules/api.md`: -#### 5) โœ‚๏ธ Trim your instruction file +```md +--- +paths: + - "src/api/**/*.ts" +--- -Your instruction file ships every turn, so each cut line saves on every message. +- Validate every API input. +- Use the standard error response. +``` -1. Open `CLAUDE.md` (or `.github/copilot-instructions.md`). -2. Cut it to essentials and add explicit conciseness rules. -3. Model it on the instruction file the project-memory skill scaffolds, which is already written this way. +Codex `src/api/AGENTS.md`: ```md -# CLAUDE.md โ€” keep it terse -- Answer first. Lead with the result, then the reason. Drop pleasantries and hedging. -- No tool-call narration. No decorative tables or emoji unless they carry information. -- Keep verbatim: code, quoted errors, security warnings. Cut the rest. +# API rules + +- Validate every API input. +- Use the standard error response. ``` -#### 6) ๐Ÿงญ Plan before you edit โ€” plan mode +#### 7) ๐Ÿงฉ Load skills only on demand -Approving the wrong direction burns tokens on rework, so let Claude explore read-only and propose a plan first. +Hide manual Claude skills and disable unused Codex skills. -1. Press `Shift+Tab` twice to enter plan mode (or start with `claude --permission-mode plan`). -2. Review the plan, then approve to switch to execution. +Claude Code skill frontmatter: -```text -Shift+Tab Shift+Tab โ†’ โธ plan mode - Claude reads and proposes; no edits until you approve +```yaml +--- +name: deploy +description: Deploy the application +disable-model-invocation: true +--- +``` + +Remove `disable-model-invocation` or set it to `false` to restore automatic Claude invocation. + +Codex `config.toml`: + +```toml +[[skills.config]] +path = "/absolute/path/to/skill" +enabled = false ``` -See [permission modes](https://code.claude.com/docs/en/permission-modes). +Set `enabled = true` to restore the Codex skill. An invoked skill stays in the session context, so keep its body short. -#### 7) โ™ป๏ธ Clear context between tasks โ€” `/clear` +#### 8) ๐Ÿง  Disable stale auto-memory -Stale early turns ride along and get re-billed every turn, so reset when the task changes. +Disable memory only when repeated rediscovery costs less than loading it. -1. Finish a task, then run `/clear` to drop the history and reload only `CLAUDE.md` and memory. -2. Use `/compact` instead when you want to keep a summary of the same task. +Claude Code `.claude/settings.local.json`: + +```json +{ + "autoMemoryEnabled": false +} +``` + +Codex `config.toml` when experimental Memories are enabled: + +```toml +[memories] +use_memories = false +generate_memories = false +``` + +Set the flags back to `true` to restore memory generation and use. Inspect first with Claude Code `/memory` or Codex `/memories`. + +#### 9) ๐Ÿชจ Compress instruction files with Caveman + +Run `caveman-compress`, inspect the diff, then restore any weakened constraint. + +```bash +# Claude Code +claude plugin marketplace add JuliusBrussee/caveman +claude plugin install caveman@caveman +# Invoke: /caveman-compress CLAUDE.md + +# Codex +npx skills add JuliusBrussee/caveman -a codex +# Invoke: $caveman-compress AGENTS.md +``` + +See [Caveman's install matrix](https://github.com/JuliusBrussee/caveman/blob/main/INSTALL.md) and [compression documentation](https://github.com/JuliusBrussee/caveman/blob/main/README.md#what-you-get). + +Rollback the edited instruction file from version control if compression weakens a constraint. + +#### 10) ๐Ÿงญ Plan before expensive work + +Plan only when a wrong implementation would cost more than the planning turn. + +| Client | Command | +| --- | --- | +| Claude Code | `/plan`, or `claude --permission-mode plan` | +| Codex | `/plan` | ```text -$ /clear - history dropped โ†’ fresh window, CLAUDE.md + memory reloaded +/plan Propose the smallest implementation and its validation before editing ``` -See [reduce token usage](https://code.claude.com/docs/en/costs). +See [Claude Code permission modes](https://code.claude.com/docs/en/permission-modes) and [Codex best practices](https://learn.chatgpt.com/guides/best-practices). -#### 8) ๐Ÿ—œ๏ธ Compact deliberately +#### 11) โ™ป๏ธ Reset unrelated work -Compacting on your terms keeps what matters instead of letting auto-compaction guess. +Start a fresh context when the goal changes. -1. Watch context use and act around 60โ€“70%. -2. Run `/compact` with focus instructions naming what to keep. +| Situation | Claude Code | Codex | +| --- | --- | --- | +| New task | `/clear` | `/new` | +| Abandon a wrong branch | `/rewind` | Start or fork a chat | +| Disposable aside | `/btw` | `/side` or `/btw` | + +#### 12) ๐Ÿ—œ๏ธ Compact the same task + +Keep the goal, decisions, failing evidence, and next validation. ```text -$ /compact keep the repro steps and the failing test; drop the file dumps +Claude Code: /compact keep the repro and failing test; drop file dumps +Codex: /compact ``` +See [Claude Code commands](https://code.claude.com/docs/en/commands) and [Codex commands](https://learn.chatgpt.com/docs/developer-commands?surface=cli). + ### ๐ŸŸก Intermediate -#### 9) ๐Ÿ—ฃ๏ธ Make the agent talk less +#### 13) ๐Ÿ“ˆ Export token telemetry + +Export structured metrics without prompt or tool content. + +Claude Code: + +```bash +CLAUDE_CODE_ENABLE_TELEMETRY=1 \ +OTEL_METRICS_EXPORTER=otlp \ +OTEL_EXPORTER_OTLP_METRICS_ENDPOINT=http://localhost:4318/v1/metrics \ +OTEL_EXPORTER_OTLP_METRICS_PROTOCOL=http/protobuf \ +claude +``` + +Codex `config.toml`: + +```toml +[otel] +environment = "dev" +log_user_prompt = false +exporter = { otlp-http = { endpoint = "http://localhost:4318/v1/logs", protocol = "binary" } } +``` + +See [Claude Code monitoring](https://code.claude.com/docs/en/monitoring-usage) and [Codex observability](https://learn.chatgpt.com/docs/config-file/config-advanced#observability-and-telemetry). + +Remove the telemetry environment variables or the `[otel]` table to stop exporting. + +#### 14) ๐Ÿ—ฃ๏ธ Set native concise output + +Preserve code, errors, evidence, and security warnings while removing filler. + +Claude Code `.claude/output-styles/concise-coding.md`: + +```md +--- +name: Concise coding +description: Short, evidence-preserving coding responses +keep-coding-instructions: true +--- + +Answer directly. Preserve code, decisive errors, evidence, and security warnings; omit filler. +``` + +Codex `config.toml`: + +```toml +model_verbosity = "low" +model_reasoning_summary = "concise" +``` + +`model_reasoning_summary` changes the reasoning summary, not the final answer. See [Claude Code output styles](https://code.claude.com/docs/en/output-styles) and [Codex configuration](https://learn.chatgpt.com/docs/config-file/config-reference). -Output is repetition you pay to generate, so cap the chatter. caveman forces short, filler-free replies (reported ~65% output cut, code intact) and auto-detects 30+ agents. +Select Claude Code's built-in default style or delete the custom file; remove the two Codex keys to restore defaults. -1. Built-in route: set `"outputStyle": "concise"` in `settings.json`. -2. Harder cut: install the [`caveman`](https://github.com/JuliusBrussee/caveman) skill and invoke it like any skill โ€” `/caveman` (or `/caveman ultra`); stop with "normal mode". +#### 15) ๐Ÿชจ Compress output with Caveman + +Use `lite` first because stronger modes trade readability for fewer output tokens. + +Install Caveman with [step 9](#9--compress-instruction-files-with-caveman), then invoke its output mode: + +```text +Claude Code: /caveman lite +Codex: $caveman lite +``` ```text -/caveman +Before: +The reason your React component is re-rendering is likely because you create a new object reference on each render. When you pass an inline object as a prop, React's shallow comparison sees a different object every time. Wrap the object in `useMemo`. + +After: +New object ref each render. Inline object prop = new ref = re-render. Wrap in `useMemo`. +``` + +The maintainer reports 65% fewer output tokens on a small chat benchmark and warns that the skill adds about 1โ€“1.5k input tokens per turn. A [JetBrains benchmark](https://blog.jetbrains.com/ai/2026/07/speak-to-ai-agents-like-cavemen-tosave-tokens/) measured 8.5% on agentic coding tasks. Either workload can be net-negative; measure total usage. -before: The reason your React component is re-rendering is likely because you're creating a new object reference on each render cycle. When you pass an inline object as a prop, React's shallow comparison sees it as a different object every time, which triggers a re-render. I'd recommend using useMemo to memoize the object. +#### 16) ๐Ÿง  Shape actionable output with i-have-adhd -after: New object ref each render. Inline object prop = new ref = re-render. Wrap in `useMemo`. +Use [`i-have-adhd`](https://github.com/ayghri/i-have-adhd) for action-first output, not guaranteed compression. + +```bash +# Claude Code +claude plugin marketplace add ayghri/i-have-adhd +claude plugin install i-have-adhd@i-have-adhd +# Invoke: /i-have-adhd + +# Codex +codex plugin marketplace add ayghri/i-have-adhd --ref main +codex plugin add i-have-adhd@i-have-adhd +# Invoke: $i-have-adhd ``` -See [output styles](https://code.claude.com/docs/en/output-styles). +Canonical action-first example: + +```text +Bad: "Let's think about this. Your auth flow has a few moving pieces..." +Good: "Run `npm install jsonwebtoken`, then edit `src/auth.ts:42`." +``` -#### 10) ๐Ÿงน Filter noisy command output +| Need | Use | +| --- | --- | +| Maximum prose compression | Caveman | +| Action-first structure | `i-have-adhd` | +| Both | `i-have-adhd`, then Caveman `lite`; benchmark the combination | -Test, install, and build logs flood context with lines the model never needs. +Stop one mode with `stop adhd mode` or `stop caveman`. `normal mode` stops both. See the [installation matrix](https://github.com/ayghri/i-have-adhd/blob/main/INSTALL.md) and [rules](https://github.com/ayghri/i-have-adhd/blob/main/skills/i-have-adhd/SKILL.md). -1. Install a CLI proxy: [`RTK`](https://github.com/rtk-ai/rtk) (Rust) or [`SNIP`](https://github.com/edouard-claude/snip) (Go, YAML filters). -2. Prefix your command with it: `rtk cargo test`. +#### 17) ๐Ÿงน Filter shell output with RTK + +Run RTK only for commands whose raw output is materially noisy. + +```bash +brew install rtk +rtk gain +rtk cargo test +``` ```mermaid flowchart LR - A["rtk cargo test"] --> R[RTK proxy] - R --> C["cargo test runs"] - C -->|"~25,000 tokens"| R - R -->|"filter ยท group ยท dedupe"| M["~2,500 tokens to model"] + A["rtk cargo test"] --> R["RTK"] + R --> C["cargo test"] + C -->|"raw output"| R + R -->|"filtered output"| M["Agent context"] ``` -Real saving: `git push` (15 lines, ~200 tokens) -> `rtk git push` (1 line, ~10 tokens). +An independent [JetBrains benchmark](https://blog.jetbrains.com/ai/2026/07/rtk-claude-code-token-savings/) measured a 7.6% median cost increase at low effort and no material cost change at high effort. Measure the full task before keeping RTK. + +Run `brew uninstall rtk` to remove the installed binary. -#### 11) ๐Ÿšซ Keep big paths out of context โ€” deny reads +#### 18) โœ‚๏ธ Filter shell output with SNIP + +Use YAML filters when RTK's fixed command set does not fit. + +```bash +brew install edouard-claude/tap/snip +snip init # Claude Code +snip init --agent codex # Codex +snip init --uninstall # Remove from Claude Code +snip init --agent codex --uninstall # Remove from Codex +``` + +See [`SNIP`](https://github.com/edouard-claude/snip). + +#### 19) ๐Ÿช Automate RTK where supported + +RTK rewrites eligible shell calls through hooks in Claude Code and Codex CLI when the host supports and trusts the installed hook. + +```bash +rtk init -g # Claude Code hook +rtk init -g --codex # Codex CLI hook + AGENTS.md + RTK.md +rtk init -g --uninstall # Remove the Claude Code integration +rtk init -g --codex --uninstall # Remove the Codex integration +``` -Vendor dirs, build output, and secrets get pulled into context by accident. Deny reads on them so they stay out โ€” they remain grep-able. +Verify Claude Code with `/hooks`; in Codex CLI, inspect the generated hook, `AGENTS.md`, and `RTK.md`, confirm hook trust, then check that an eligible shell call is rewritten. See [RTK's current client matrix](https://github.com/rtk-ai/rtk/blob/develop/README.md#supported-ai-tools) and [Claude Code filtering hooks](https://code.claude.com/docs/en/costs#offload-processing-to-hooks-and-skills). -1. In `settings.json`, add `Read(...)` deny rules for large or sensitive paths. -2. `.claudeignore` is not shipped โ€” deny rules are the official way. +#### 20) โœ‹ Cap Codex tool history + +Limit stored tool output only after raw diagnostics have been captured. + +```toml +tool_output_token_limit = 12000 +``` + +Raise or remove the limit when evidence is truncated. + +#### 21) ๐Ÿšซ Block bulky paths + +Prevent accidental reads of generated output and dependency trees. + +Claude Code `.claude/settings.json`: ```json { + "$schema": "https://json.schemastore.org/claude-code-settings.json", "permissions": { - "deny": ["Read(./vendor/**)", "Read(./dist/**)", "Read(./.env)"] + "deny": [ + "Read(./.env)", + "Read(./.env.*)", + "Read(./secrets/**)", + "Read(./vendor/**)", + "Read(./dist/**)" + ] } } ``` -See [permissions](https://code.claude.com/docs/en/permissions). +This saves tokens only when the agent would otherwise read those paths. Secrets are primarily a security concern. See [Claude Code permissions](https://code.claude.com/docs/en/permissions). -#### 12) ๐Ÿ”Œ Prefer CLI over MCP +Remove only the added `deny` entries to restore access. -An MCP server's schema rides along every turn; a CLI costs tokens only when you call it. Newer MCP tooling adds tool/context selection that loads only the tools you pick โ€” cheaper than before โ€” but a CLI is still leaner and faster. +#### 22) ๐Ÿ”Œ Limit MCP exposure -| | CLI (`gh`, `acli`, โ€ฆ) | MCP server | -| --- | --- | --- | -| Token cost | a few, only when called | a schema every turn; less with tool/context selection | -| Speed | fastest | slower | -| Use when | a CLI exists | no CLI, or you need typed/live tool calls | +Disable unused servers and tools. -See [`mcp-installation.md`](mcp-installation.md). +| Client | Control | +| --- | --- | +| Claude Code | Tool Search defers supported MCP schemas; avoid unnecessary `alwaysLoad` | +| Codex | `/mcp verbose`; `enabled = false`; `enabled_tools`; `disabled_tools` | -### ๐Ÿ”ด Expert +```toml +[mcp_servers.figma] +url = "https://mcp.figma.com/mcp" +enabled = false +``` + +Set `enabled = true` or remove the override to restore the server. -#### 13) ๐Ÿ”ฌ Audit which skills and tools run โ€” `Ctrl+O` +See [Claude Code MCP](https://code.claude.com/docs/en/mcp#scale-with-mcp-tool-search), [Codex MCP](https://learn.chatgpt.com/docs/extend/mcp?surface=cli), and [`mcp-installation.md`](mcp-installation.md). + +### ๐Ÿ”ด Expert -You optimise what you can see, so expand the transcript to watch what each turn actually invokes and pulls into context. +#### 23) ๐Ÿ”ฌ Inspect model-visible behavior -1. Press `Ctrl+O` to toggle the transcript โ€” it shows detailed tool and skill usage and expands collapsed MCP calls. -2. Spot skills or tools that load on turns that don't need them, then scope or remove them. +Use diagnostics native to each client. ```text -Ctrl+O โ€” transcript expanded - โŽฟ Skill: token-optimization - โŽฟ Called slack 3 times โ†’ expanded: 3 tool calls -(illustrative โ€” replace with a screenshot of your real Ctrl+O transcript) +Claude Code: /insights +Codex CLI: codex debug prompt-input "Explain the current context sources" ``` -See the [keyboard shortcuts](https://code.claude.com/docs/en/interactive-mode). +See [Claude Code commands](https://code.claude.com/docs/en/commands) and [Codex commands](https://learn.chatgpt.com/docs/developer-commands?surface=cli). -#### 14) ๐ŸŽฏ Route by difficulty +#### 24) ๐ŸŽฏ Route model and effort by difficulty -The top model on routine work is wasted spend, so pin the model per skill or agent โ€” cheap for routine, top-tier for hard reasoning. +Use cheaper agents for bounded exploration while keeping difficult review and architecture on stronger settings. -1. Set `model` in a skill's or an agent's frontmatter (`haiku` / `sonnet` / `opus`, a full id, or `inherit`). -2. Give routine scouts a small model; reserve `opus` for the hard reasoning. +Claude Code `.claude/agents/explorer.md`: ```yaml -# .claude/agents/explore.md โ€” routine scouting on a cheap model --- -name: explore +name: explorer description: Read-only codebase scout tools: Read, Grep, Glob model: haiku +effort: low --- ``` -A skill's `SKILL.md` takes the same `model:` field (e.g. `model: opus` for a heavy step). See [sub-agents](https://code.claude.com/docs/en/sub-agents) and [skills](https://code.claude.com/docs/en/skills). +Codex `.codex/agents/explorer.toml`: -#### 15) ๐Ÿงซ Offload high-volume work to subagents +```toml +name = "explorer" +description = "Read-only codebase scout" +developer_instructions = "Explore without edits. Return paths, symbols, and decisive evidence only." +model = "gpt-5.6-terra" +model_reasoning_effort = "low" +``` -Test runs, log parsing, and wide exploration flood the main window. A subagent does it in its own context and hands back only a summary, so the bloat never lands in your session. +See [Claude Code model configuration](https://code.claude.com/docs/en/model-config) and [Codex subagents](https://learn.chatgpt.com/docs/agent-configuration/subagents). -1. Define an agent in `.claude/agents/<name>.md` with only the tools it needs and a small `model`. -2. Let it run the noisy op and return a short result. +Delete these optional agent files to restore each client's default routing. -```yaml -# .claude/agents/test-runner.md ---- -name: test-runner -description: Run the suite and return only the failures -tools: Bash, Read -model: haiku ---- +#### 25) ๐Ÿงซ Isolate noisy work with subagents + +Return only findings, evidence, blockers, and the command used. + +```text +Delegate the test run. Return only failing tests, the shortest decisive error, and the command used. +``` + +```mermaid +flowchart LR + M["Main context"] --> S["Test subagent"] + S --> L["Full logs stay isolated"] + S --> F["Failures + command"] + F --> M ``` -See [sub-agents](https://code.claude.com/docs/en/sub-agents). +Subagents reduce main-context noise, not total token usage. See [Claude Code subagents](https://code.claude.com/docs/en/sub-agents) and [Codex subagents](https://learn.chatgpt.com/docs/agent-configuration/subagents). -#### 16) ๐ŸงŠ Protect your cache hits +#### 26) ๐ŸงŠ Preserve Claude prompt-cache prefixes -Cached input bills far cheaper, and cache reads are most of your tokens (step 4) โ€” so don't throw the cache away mid-task. A model switch, an MCP connect or disconnect, or an effort change rebuilds it from scratch. +Choose model, effort, and MCP tools before the first substantive prompt. -1. Set the model and reasoning effort at the start of a task, not mid-work. -2. Switch model or toggle MCP servers only at task boundaries, where a cache rebuild is acceptable. +![Claude Code prompt caching reuses an unchanged request prefix](https://mintcdn.com/claude-code/VbDJw--l6T9a9Wvm/images/prompt-caching-prefix.svg?fit=max&auto=format&n=VbDJw--l6T9a9Wvm&q=85&s=f2e8f0b8298a50305fe428ca3f1d1594) ```text -mid-task model switch โ†’ cache invalidated โ†’ full re-bill -same model to a boundary โ†’ cache reads stay cheap +/model sonnet +/effort medium ``` -See [prompt caching](https://code.claude.com/docs/en/prompt-caching). +For Codex, observe `cached_input` through telemetry. See [Claude Code prompt caching](https://code.claude.com/docs/en/prompt-caching) and [Codex observability](https://learn.chatgpt.com/docs/config-file/config-advanced#observability-and-telemetry). -#### 17) โœ… Cap extended thinking +#### 27) โœ… Lower reasoning on routine work -Extended reasoning can silently add thousands of tokens on tasks that don't need it. +Lower effort first, and disable thinking only when a representative task still passes. -1. In `settings.json`, set `MAX_THINKING_TOKENS` to `0` for routine work. -2. See the [Claude Code settings docs](https://code.claude.com/docs/en/settings). +```text +Claude Code: /effort low +Codex: /reasoning +``` + +Claude Code `.claude/settings.json`: ```json { "env": { - "MAX_THINKING_TOKENS": "0" + "CLAUDE_CODE_DISABLE_THINKING": "1" } } ``` -## In short +Codex `config.toml`: + +```toml +model_reasoning_effort = "low" +``` + +Remove the environment variable and Codex key, then restore the prior `/effort` or `/reasoning` value. + +See [Claude Code model configuration](https://code.claude.com/docs/en/model-config) and [Codex configuration](https://learn.chatgpt.com/docs/config-file/config-reference). + +## Verify the result -Measure first, then stack the cheap wins โ€” trimmed instructions, plan mode, a clean context, less chatter, filtered output โ€” before the advanced routing, subagents, and cache discipline. Most of the bill is cache and repetition; cut those and the cost follows. +- Compare the same task, model, effort, and quality gate before and after each change. +- Record input, output, reasoning, tool-output, and subagent tokens when the client exposes them. +- Keep only changes that reduce total usage without weakening the required result. diff --git a/plugins/aidd-context/skills/12-cook/assets/research-checklist.md b/plugins/aidd-context/skills/12-cook/assets/research-checklist.md index 89a99785c..4ec51e1c2 100644 --- a/plugins/aidd-context/skills/12-cook/assets/research-checklist.md +++ b/plugins/aidd-context/skills/12-cook/assets/research-checklist.md @@ -1,9 +1,9 @@ -<!-- Tick before drafting in 03-research. A research pass is done only when all three clear. --> +<!-- Tick before drafting in 03-research. A research pass is done only when every item clears. --> # Research checklist -A pass is done only when all three clear. Never draft from memory alone. +A pass is done only when every item clears. Never draft from memory alone. - [ ] **Fill gaps** โ€” cover what the starting list missed. - [ ] **Surface unknowns** โ€” search past what you already know; assume blind spots exist. -- [ ] **Confirm claims** โ€” corroborate every assertion you would write, or mark it unverified. +- [ ] **Clear candidate criteria** โ€” every retained item passes `references/research-playbook.md`, including verification and transferability. diff --git a/plugins/aidd-context/skills/12-cook/assets/research-goal-checklist.md b/plugins/aidd-context/skills/12-cook/assets/research-goal-checklist.md index 3ad21d328..ddce3cf0c 100644 --- a/plugins/aidd-context/skills/12-cook/assets/research-goal-checklist.md +++ b/plugins/aidd-context/skills/12-cook/assets/research-goal-checklist.md @@ -2,11 +2,11 @@ # Refine the goal -Fill this with the user before scouting. A vague goal returns vague insights. +Resolve this from the request and existing recipe before scouting. Ask the user only for a missing choice that materially changes the result. - [ ] **Goal** โ€” one sentence naming the precise outcome the recipe delivers. - [ ] **Audience and level** โ€” who it is for, mapped to Beginner / Intermediate / Expert. - [ ] **Scope** โ€” what is in and explicitly out, so the search stays bounded. - [ ] **Actionable insights** โ€” every item is a concrete action with an observable result, never theory alone. -- [ ] **Grouping** โ€” insights are organized under the three recipe levels (Beginner / Intermediate / Expert). +- [ ] **Grouping** โ€” use Beginner / Intermediate / Expert only when difficulty grouping helps the reader. - [ ] **Counter-intuitive wins** โ€” leave room for surprising tips that outperform the obvious default. diff --git a/plugins/aidd-context/skills/12-cook/assets/validation-report-template.md b/plugins/aidd-context/skills/12-cook/assets/validation-report-template.md new file mode 100644 index 000000000..b7e6472e9 --- /dev/null +++ b/plugins/aidd-context/skills/12-cook/assets/validation-report-template.md @@ -0,0 +1,14 @@ +<!-- Choose success only when deterministic and semantic checks both pass; otherwise choose failure. Replace every placeholder and remove these instructions and the unused branch. Emit the report without writing a file. --> + +<!-- Success: count validated recipes and list unavailable optional parsers, or none. --> +```text +PASS: <n> recipe(s) validated. +Checks: deterministic and semantic; unavailable parsers: <languages or none>. +``` + +<!-- Failure: include one row per deterministic or semantic finding, with a line-specific correction. Disclose unavailable optional parsers in a relevant finding or after the table. --> +```md +| File | Line | Rule | Fix | +| --- | ---: | --- | --- | +| <path> | <line> | <rule> | <specific correction> | +``` diff --git a/plugins/aidd-context/skills/12-cook/references/recipe-contract.md b/plugins/aidd-context/skills/12-cook/references/recipe-contract.md index f06f1bf0b..0c1ae2af7 100644 --- a/plugins/aidd-context/skills/12-cook/references/recipe-contract.md +++ b/plugins/aidd-context/skills/12-cook/references/recipe-contract.md @@ -1,32 +1,19 @@ -<!-- Cited by upsert. The rules every recipe file follows. --> - # Recipe contract -Rules for every recipe file the skill writes. - -## File - -- Project path: `aidd_docs/recipes/<kebab-slug>.md`. -- Bundled path, only for explicit framework-source edits: `plugins/aidd-context/skills/12-cook/assets/recipes/<kebab-slug>.md`. -- The recipe opens with the H1 title, then one plain sentence of description โ€” no "Goal:" label, no blockquote, no metadata table. -- Sections: the description, `## Why`, then the steps. `## Verify` is optional โ€” omit it when it adds little. End with an optional short conclusion. Never add a `## Related` section: links live inline where they are used. - ## Writing -- One idea per sentence. Prefer removing over adding. -- No filler line under a heading (no "Ranked by impact", "Start at the top", and the like). -- `## Why`: short and benefit-first, one idea per line. Lead with the keywords a reader would search, **bold** the key terms. +- Keep prose that helps the reader perform, understand, or verify the technique. +- State where and when a technique applies. ## Steps -- The steps section heading is named after the goal: `## Steps to <outcome>`, never a bare `## Steps`. -- One step = one action: never bundle several tools or commands under one heading; split them. -- Each step is a `#### N) <emoji> Title` heading, then one benefit-focused line of what and why in prose. Number steps continuously. -- Write the actions to take as a numbered list; write any description or indication as prose, never as a bullet. -- For a tool: where it is, install it from its URL, then how to invoke it (its real command or slash invocation, e.g. `/caveman`). -- Reuse the tool's canonical example captured verbatim from its site or README โ€” never a paraphrase, and never on the strength of a summary that says one exists. -- Every step carries one concrete example. Prefer an image โ€” a screenshot or short video/GIF that matches the action โ€” over text whenever one exists; for a tool, use its official screenshot. Otherwise: a command with its real output, a config in the file's real syntax (valid JSON for `settings.json`, valid YAML for frontmatter, โ€ฆ), or a snippet. -- A step that prefers one option over another uses a comparison table, not prose. -- For a structural or flow concept (a proxy, a pipeline, an architecture), add a small Mermaid diagram with concrete example values. -- Level subheadings are optional. Group steps under `### ๐ŸŸข Beginner`, `### ๐ŸŸก Intermediate`, `### ๐Ÿ”ด Expert` only when the recipe spans difficulty levels and grouping helps the reader; a short or single-level recipe lists its steps directly. Include only the levels that have a step. -- Link to a reference when applicable. +- One step covers one technique, named for its action. +- Headings express nesting without skipping a level; number steps continuously from 1 across categories. +- Give each step a concrete, usable command, configuration, output, table, or operational image; code examples must be syntactically valid. +- Local links and table-of-contents anchors must resolve; remove unfinished placeholders. + +## Evidence + +- Verify commands, configuration, and behavior against current official sources; link them where they are used. +- Qualify claims as measurements, maintainer claims, or inferences when their strength is not obvious; state relevant conditions and limits. +- Support promised savings with measurements and their workload; never present an unverified claim as fact. diff --git a/plugins/aidd-context/skills/12-cook/references/research-playbook.md b/plugins/aidd-context/skills/12-cook/references/research-playbook.md index 40db46c73..2d920fa8f 100644 --- a/plugins/aidd-context/skills/12-cook/references/research-playbook.md +++ b/plugins/aidd-context/skills/12-cook/references/research-playbook.md @@ -1,12 +1,10 @@ -<!-- Read before spawning research agents in 03-research. The scouting angles, the bar each candidate must clear, and how to verify them. Not a recipe itself. --> - # Research playbook Guidance for `03-research`: the angles to scout, the criteria each candidate must clear, and the verification each must pass. Define the target with `assets/research-goal-checklist.md` first, and clear `assets/research-checklist.md` before drafting. ## Angles to cover -Spawn one agent per angle so coverage stays wide: +Cover every angle: - **Alternatives** โ€” competing tools, libraries, or methods that could replace the current approach. - **New methods** โ€” techniques that emerged since the recipe was written. @@ -20,19 +18,22 @@ Spawn one agent per angle so coverage stays wide: - **Community signal** โ€” weigh what practitioners actually say: GitHub activity, open issues, Reddit, Hacker News, discussions. Separate real adoption from hype. - **Tips and gotchas** โ€” capture practical advice, common pitfalls, and migration notes worth keeping. - **Credibility** โ€” corroborate across sources. Trust official docs over a single blog post. +- **Evidence type** โ€” label an independent measurement, maintainer claim, or inference. Do not report an unmeasured saving as fact. +- **Transferability** โ€” for benchmarks, record model, effort, task, quality gate, input/output/reasoning usage, tool output, and subagent overhead when available. ## Verify each candidate -After curating, confirm every surviving candidate before presenting it. Spawn one agent per candidate to check: +After curating, confirm every surviving candidate before presenting it: - **Exists** โ€” confirm it is real and current, not invented or abandoned. - **Official link** โ€” capture the canonical source: official docs, repository, or release page. - **Latest state** โ€” record the current version or release date. +- **Applicability** โ€” record supported clients, configuration scope, known incompatibilities, and stop/rollback instructions when relevant. - **Real example** โ€” prefer an image: grab the official screenshot or GIF that matches the action. Else a real output: copy it from the docs, or run the command and capture what it prints. For interactive output that cannot be scripted, mark it for the human to paste (ideally a screenshot). Never fabricate. - Drop any candidate that cannot be confirmed against an official source. ## Reporting -- Return each candidate with its angle, level, value, and a source; verified ones carry an official link. +- Return each candidate with its angle, level, value, evidence type, and source; verified ones carry an official link. - Push for the most insights possible, then drop anything that neither beats nor extends the recipe. - `03-research` sorts them into three buckets: alternatives (with pros/cons), coverage gaps, and counter-intuitive wins. diff --git a/plugins/aidd-context/skills/12-cook/scripts/validate-recipe.mjs b/plugins/aidd-context/skills/12-cook/scripts/validate-recipe.mjs new file mode 100644 index 000000000..ad036049a --- /dev/null +++ b/plugins/aidd-context/skills/12-cook/scripts/validate-recipe.mjs @@ -0,0 +1,514 @@ +#!/usr/bin/env node + +import fs from "node:fs"; +import path from "node:path"; +import process from "node:process"; +import { fileURLToPath } from "node:url"; + +const SKILL_DIRECTORY = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); +const BUNDLED_DIRECTORY = path.join(SKILL_DIRECTORY, "assets", "recipes"); +const PROJECT_DIRECTORY = path.join(process.cwd(), "aidd_docs", "recipes"); +const STEP_PATTERN = /^(#{3,4})\s+(\d+)\)\s+(\S+)\s+(.+?)\s*$/u; +const HEADING_PATTERN = /^ {0,3}(#{1,6})\s+(.+?)\s*#*\s*$/u; +const LINK_PATTERN = /(!?)\[[^\]\n]*\]\(([^)\n]+)\)/gu; +const HTML_TAGS = new Set([ + "a", "article", "aside", "body", "br", "button", "code", "details", "div", "em", + "footer", "form", "h1", "h2", "h3", "h4", "h5", "h6", "head", "header", "hr", + "html", "i", "iframe", "img", "input", "label", "li", "link", "main", "meta", "nav", + "ol", "option", "p", "path", "picture", "pre", "script", "section", "select", "small", + "source", "span", "strong", "style", "summary", "svg", "table", "tbody", "td", "template", + "textarea", "tfoot", "th", "thead", "title", "tr", "ul", "video", +]); +const ANGLE_SYNTAX_LANGUAGES = new Set([ + "c", "c++", "cpp", "cs", "csharp", "go", "html", "java", "jsx", "kotlin", "objc", + "objective-c", "rust", "svg", "swift", "tsx", "xml", +]); + +function maskHtmlComments(content) { + return content.replace(/<!--[\s\S]*?-->/gu, (comment) => comment.replace(/[^\n]/gu, " ")); +} + +function lineNumberAt(content, index) { + return content.slice(0, index).split("\n").length; +} + +function addFinding(findings, file, line, rule, fix) { + findings.push({ file, line: Math.max(1, line), rule, fix }); +} + +function readRegularFile(file, readContent = true) { + let descriptor; + try { + descriptor = fs.openSync(file, "r"); + if (!fs.fstatSync(descriptor).isFile()) { + throw Object.assign(new Error("Target is not a regular file."), { code: "EISDIR" }); + } + return readContent ? fs.readFileSync(descriptor, "utf8") : null; + } finally { + if (descriptor !== undefined) fs.closeSync(descriptor); + } +} + +function scanFences(lines, file, findings) { + const fencedLines = new Set(); + const blocks = []; + let open = null; + + for (let index = 0; index < lines.length; index += 1) { + const line = lines[index]; + const match = line.match(/^ {0,3}(`{3,}|~{3,})(.*)$/u); + + if (!open) { + if (!match) continue; + const language = match[2].trim().split(/\s+/u)[0] ?? ""; + if (!language) { + addFinding(findings, file, index + 1, "fence-language", "Add a language after the opening fence, such as ```bash or ```text."); + } + open = { + marker: match[1][0], + length: match[1].length, + start: index, + language: language.toLowerCase(), + }; + fencedLines.add(index); + continue; + } + + fencedLines.add(index); + if ( + match && + match[1][0] === open.marker && + match[1].length >= open.length && + match[2].trim() === "" + ) { + blocks.push({ + ...open, + end: index, + body: lines.slice(open.start + 1, index).join("\n"), + }); + open = null; + } + } + + if (open) { + addFinding(findings, file, open.start + 1, "fence-balance", "Close this fenced code block with the same marker."); + } + + return { fencedLines, blocks }; +} + +function parseHeadings(lines, fencedLines) { + const headings = []; + for (let index = 0; index < lines.length; index += 1) { + if (fencedLines.has(index)) continue; + const match = lines[index].match(HEADING_PATTERN); + if (!match) continue; + headings.push({ + depth: match[1].length, + text: match[2].trim(), + line: index + 1, + index, + }); + } + return headings; +} + +function baseSlug(text) { + let plain = ""; + let tagDepth = 0; + for (const character of text) { + if (character === "<") tagDepth += 1; + else if (character === ">") tagDepth = Math.max(0, tagDepth - 1); + else if (tagDepth === 0) plain += character; + } + return plain + .toLowerCase() + .replace(/[`*_~]/gu, "") + .replace(/[\u200d\ufe0e\ufe0f]/gu, "") + .replace(/[^\p{L}\p{N}\p{M}\s_-]/gu, "") + .replace(/\s/gu, "-"); +} + +function headingAnchors(content) { + const masked = maskHtmlComments(content); + const lines = masked.split(/\r?\n/u); + const { fencedLines } = scanFences(lines, "", []); + const counts = new Map(); + const anchors = new Set(); + + for (const heading of parseHeadings(lines, fencedLines)) { + const slug = baseSlug(heading.text); + const count = counts.get(slug) ?? 0; + counts.set(slug, count + 1); + anchors.add(count === 0 ? slug : `${slug}-${count}`); + } + return anchors; +} + +function normalizedAnchor(raw) { + try { + return decodeURIComponent(raw.replace(/^#/u, "")) + .toLowerCase() + .replace(/[\u200d\ufe0e\ufe0f]/gu, ""); + } catch { + return raw.replace(/^#/u, "").toLowerCase().replace(/[\u200d\ufe0e\ufe0f]/gu, ""); + } +} + +function splitTarget(raw) { + const trimmed = raw.trim(); + if (trimmed.startsWith("<") && trimmed.includes(">")) { + return trimmed.slice(1, trimmed.indexOf(">")); + } + return trimmed.split(/\s+["']/u)[0]; +} + +function checkLinks(content, fencedLines, file, findings, anchors) { + const cache = new Map([[file, { anchors }]]); + + for (const match of content.matchAll(LINK_PATTERN)) { + const line = lineNumberAt(content, match.index); + if (fencedLines.has(line - 1)) continue; + + const target = splitTarget(match[2]); + if (!target || /^(?:https?|mailto|tel|codex):/iu.test(target)) continue; + + const hashIndex = target.indexOf("#"); + const rawPath = hashIndex === -1 ? target : target.slice(0, hashIndex); + const rawAnchor = hashIndex === -1 ? "" : target.slice(hashIndex + 1); + let targetFile = file; + + if (rawPath) { + let decodedPath; + try { + decodedPath = decodeURI(rawPath); + } catch { + addFinding(findings, file, line, "link-encoding", `Encode the link target correctly: ${rawPath}`); + continue; + } + targetFile = path.resolve(path.dirname(file), decodedPath); + } + + if (!cache.has(targetFile)) { + try { + const markdown = targetFile.toLowerCase().endsWith(".md"); + const targetContent = readRegularFile(targetFile, markdown); + cache.set(targetFile, { anchors: markdown ? headingAnchors(targetContent) : null }); + } catch (error) { + cache.set(targetFile, { error: error.code ?? error.message }); + } + } + const targetInfo = cache.get(targetFile); + if (targetInfo.error) { + addFinding(findings, file, line, match[1] ? "image-target" : "link-target", `Point the local target to a readable file: ${rawPath} (${targetInfo.error}).`); + continue; + } + + if (!rawAnchor) continue; + if (!targetFile.toLowerCase().endsWith(".md")) { + addFinding(findings, file, line, "link-anchor", `Remove the anchor or target a Markdown heading: #${rawAnchor}`); + continue; + } + + if (!targetInfo.anchors.has(normalizedAnchor(rawAnchor))) { + addFinding(findings, file, line, "link-anchor", `Point the link to an existing heading: #${rawAnchor}`); + } + } +} + +function hasTable(lines) { + for (let index = 0; index < lines.length - 1; index += 1) { + if (/^\s*\|.*\|\s*$/u.test(lines[index]) && /^\s*\|?\s*:?-{3,}/u.test(lines[index + 1])) return true; + } + return false; +} + +function findAnglePlaceholders(content) { + const matches = []; + + for (const match of content.matchAll(/<([a-z][^<>\n]*)>/giu)) { + const value = match[1]; + const tag = value.split(/[\s/]/u)[0].toLowerCase(); + if ((HTML_TAGS.has(tag) && (!value.includes(" ") || value.includes("="))) || content.includes(`</${value}>`) || /^(?:https?:\/\/|mailto:)/iu.test(value)) continue; + matches.push(match); + } + + return matches; +} + +function maskAngleSyntaxBlocks(content, blocks) { + const lines = content.split(/\r?\n/u); + for (const block of blocks) { + if (!ANGLE_SYNTAX_LANGUAGES.has(block.language)) continue; + for (let index = block.start; index <= block.end; index += 1) { + lines[index] = lines[index].replace(/[^\r\n]/gu, " "); + } + } + return lines.join("\n"); +} + +function checkSteps(lines, headings, stepsHeading, file, findings, fencedLines) { + const nextH2 = headings.find((heading) => heading.depth === 2 && heading.index > stepsHeading.index); + const endIndex = nextH2?.index ?? lines.length; + const scopedHeadings = headings.filter( + (heading) => heading.index > stepsHeading.index && heading.index < endIndex && [3, 4].includes(heading.depth), + ); + const steps = []; + const categories = []; + + for (const heading of scopedHeadings) { + const source = `${"#".repeat(heading.depth)} ${heading.text}`; + const match = source.match(STEP_PATTERN); + if (match) { + const icon = match[3]; + if (!/\p{Extended_Pictographic}/u.test(icon)) { + addFinding(findings, file, heading.line, "step-emoji", "Add one meaningful emoji between the step number and title."); + } + steps.push({ ...heading, number: Number.parseInt(match[2], 10), icon, title: match[4] }); + continue; + } + + if (/^\d+[.)]?\s/u.test(heading.text)) { + addFinding(findings, file, heading.line, "step-heading", "Use `### N) <emoji> Title`, or `#### N) <emoji> Title` below a category."); + } else if (heading.depth === 3) { + categories.push(heading); + } else { + addFinding(findings, file, heading.line, "category-depth", "Use level 3 for a category and level 4 only for its numbered steps."); + } + } + + if (steps.length === 0) { + addFinding(findings, file, stepsHeading.line, "steps-missing", "Add at least one numbered step heading."); + return []; + } + + steps.forEach((step, index) => { + const expected = index + 1; + if (step.number !== expected) { + addFinding(findings, file, step.line, "step-number", `Renumber this step to ${expected}; numbering must start at 1 and remain continuous.`); + } + }); + + if (categories.length > 0) { + for (const step of steps) { + if (step.depth !== 4) { + addFinding(findings, file, step.line, "step-depth", "Use level 4 for every numbered step when level 3 categories are present."); + continue; + } + const parent = [...categories].reverse().find((category) => category.index < step.index); + if (!parent) { + addFinding(findings, file, step.line, "step-parent", "Place this level 4 step below a level 3 category."); + } + } + } else { + for (const step of steps) { + if (step.depth !== 3) { + addFinding(findings, file, step.line, "step-depth", "Use level 3 for direct numbered steps."); + } + } + } + + for (const step of steps) { + const nextBoundary = headings.find( + (heading) => heading.index > step.index && heading.index < endIndex && heading.depth <= step.depth, + ); + const stepEnd = nextBoundary?.index ?? endIndex; + const body = lines.slice(step.index + 1, stepEnd); + const hasFence = body.some((_, offset) => fencedLines.has(step.index + 1 + offset)); + const hasImage = body.some((line) => /!\[[^\]]*\]\([^)]+\)/u.test(line)); + if (!hasFence && !hasImage && !hasTable(body)) { + addFinding(findings, file, step.line, "step-example", "Add a fenced command/config/output, a concrete table, or an operational image."); + } + } + + return steps; +} + +export function validateRecipe(filePath) { + const file = path.resolve(filePath); + const findings = []; + + let content; + try { + content = readRegularFile(file); + } catch (error) { + const rule = error.code === "ENOENT" ? "file-exists" : error.code === "EISDIR" ? "file-type" : "file-read"; + addFinding(findings, file, 1, rule, `Pass a readable Markdown recipe file (${error.code ?? error.message}).`); + return findings; + } + if (path.extname(file).toLowerCase() !== ".md") { + addFinding(findings, file, 1, "file-type", "Pass a Markdown recipe file ending in .md."); + return findings; + } + + const masked = maskHtmlComments(content); + const lines = masked.split(/\r?\n/u); + const { fencedLines, blocks } = scanFences(lines, file, findings); + const headings = parseHeadings(lines, fencedLines); + const h1s = headings.filter((heading) => heading.depth === 1); + + if (h1s.length !== 1 || h1s[0]?.index !== 0) { + addFinding(findings, file, h1s[0]?.line ?? 1, "title", "Start the file with exactly one H1 title."); + } + + const title = h1s[0]; + if (title) { + const descriptionIndex = lines.findIndex((line, index) => index > title.index && line.trim() !== ""); + const description = descriptionIndex === -1 ? "" : lines[descriptionIndex].trim(); + if ( + !description || + /^(?:#|[-*+]|\d+[.)]\s|>|\||```|~~~|!\[)/u.test(description) + ) { + addFinding(findings, file, descriptionIndex + 1 || title.line + 1, "description", "Put a non-empty plain description paragraph directly after the H1."); + } + } + + for (let index = 1; index < headings.length; index += 1) { + const previous = headings[index - 1]; + const current = headings[index]; + if (current.depth > previous.depth + 1) { + addFinding(findings, file, current.line, "heading-depth", `Use level ${previous.depth + 1} or shallower after the previous heading.`); + } + } + + const h2s = headings.filter((heading) => heading.depth === 2); + const stepsHeadings = h2s.filter((heading) => /^Steps to\s+\S/u.test(heading.text)); + const verifyHeadings = h2s.filter((heading) => /^Verify(?:\s|$)/u.test(heading.text)); + + if (stepsHeadings.length !== 1) { + addFinding(findings, file, stepsHeadings[0]?.line ?? 1, "steps-section", "Add exactly one outcome-specific `## Steps to ...` section."); + } + if (verifyHeadings.length > 1) { + addFinding(findings, file, verifyHeadings[1].line, "verify-section", "Keep at most one `## Verify` section."); + } + + const stepsHeading = stepsHeadings[0]; + for (const verify of verifyHeadings) { + if (!stepsHeading || verify.index < stepsHeading.index) { + addFinding(findings, file, verify.line, "section-order", "Place `## Verify` after the steps section."); + } + } + + if (stepsHeading) checkSteps(lines, headings, stepsHeading, file, findings, fencedLines); + + for (const block of blocks.filter((candidate) => candidate.language === "json")) { + try { + JSON.parse(block.body); + } catch (error) { + addFinding(findings, file, block.start + 1, "json-syntax", `Fix the JSON example: ${error.message}`); + } + } + + const placeholderContent = masked; + const anglePlaceholderContent = maskAngleSyntaxBlocks(placeholderContent, blocks); + for (const match of findAnglePlaceholders(anglePlaceholderContent)) { + addFinding(findings, file, lineNumberAt(anglePlaceholderContent, match.index), "placeholder", "Replace or remove this angle-bracket placeholder."); + } + for (const pattern of [ + { regex: /\{\{[^}\n]+\}\}/gu, rule: "placeholder", fix: "Replace or remove this template placeholder." }, + { regex: /\b(?:TODO|TBD|FIXME)\b/gu, rule: "todo", fix: "Resolve this unfinished marker before publishing the recipe." }, + ]) { + for (const match of placeholderContent.matchAll(pattern.regex)) { + addFinding(findings, file, lineNumberAt(placeholderContent, match.index), pattern.rule, pattern.fix); + } + } + + const anchors = headingAnchors(content); + checkLinks(content, fencedLines, file, findings, anchors); + + return findings.sort((left, right) => left.line - right.line || left.rule.localeCompare(right.rule)); +} + +function collectMarkdown(directory) { + if (!fs.existsSync(directory)) return []; + return fs.readdirSync(directory, { withFileTypes: true }) + .filter((entry) => entry.isFile() && entry.name.endsWith(".md") && entry.name !== "README.md") + .map((entry) => path.join(directory, entry.name)); +} + +function displayPath(file) { + const relative = path.relative(process.cwd(), file).replaceAll(path.sep, "/"); + return relative && !relative.startsWith("../") ? relative : file.replaceAll(path.sep, "/"); +} + +function escapeCell(value) { + return String(value).replaceAll("|", "\\|").replaceAll("\n", " "); +} + +export function report(findings, fileCount, output = console.log, errorOutput = console.error) { + if (findings.length === 0) { + output(`PASS: ${fileCount} recipe(s) validated.`); + return 0; + } + + errorOutput("| File | Line | Rule | Fix |"); + errorOutput("| --- | ---: | --- | --- |"); + for (const finding of findings) { + errorOutput(`| ${escapeCell(displayPath(finding.file))} | ${finding.line} | ${escapeCell(finding.rule)} | ${escapeCell(finding.fix)} |`); + } + errorOutput(""); + errorOutput(`FAIL: ${findings.length} finding(s) in ${fileCount} recipe(s).`); + return 1; +} + +function parseArguments(argv) { + if (argv.includes("--help") || argv.includes("-h")) { + return { help: true, all: false, paths: [] }; + } + const unknown = argv.filter((argument) => argument.startsWith("-") && argument !== "--all"); + if (unknown.length > 0) throw new Error(`Unknown option: ${unknown.join(", ")}`); + return { + help: false, + all: argv.includes("--all"), + paths: argv.filter((argument) => argument !== "--all"), + }; +} + +function usage() { + return [ + "Usage:", + " node validate-recipe.mjs <recipe.md> [more-recipes.md]", + " node validate-recipe.mjs --all", + ].join("\n"); +} + +export function run(argv = process.argv.slice(2)) { + let parsed; + try { + parsed = parseArguments(argv); + } catch (error) { + console.error(`FAIL: ${error.message}`); + return 1; + } + + if (parsed.help) { + console.log(usage()); + return 0; + } + + const candidates = parsed.paths.map((candidate) => path.resolve(process.cwd(), candidate)); + if (parsed.all) { + candidates.push(...collectMarkdown(PROJECT_DIRECTORY), ...collectMarkdown(BUNDLED_DIRECTORY)); + } + const files = [...new Set(candidates)].sort((left, right) => left.localeCompare(right)); + if (files.length === 0) { + console.error(usage()); + console.error("FAIL: pass at least one recipe path or --all."); + return 1; + } + + const findings = files.flatMap((file) => validateRecipe(file)); + return report(findings, files.length); +} + +function isMainModule() { + try { + return fs.realpathSync(process.argv[1]) === fileURLToPath(import.meta.url); + } catch { + return false; + } +} + +if (isMainModule()) { + process.exitCode = run(); +} diff --git a/scripts/__tests__/validate-recipe.test.js b/scripts/__tests__/validate-recipe.test.js new file mode 100644 index 000000000..8e40280f9 --- /dev/null +++ b/scripts/__tests__/validate-recipe.test.js @@ -0,0 +1,572 @@ +const assert = require("node:assert/strict"); +const { spawnSync } = require("node:child_process"); +const fs = require("node:fs"); +const os = require("node:os"); +const path = require("node:path"); +const { pathToFileURL } = require("node:url"); +const test = require("node:test"); + +const root = path.resolve(__dirname, "../.."); +const script = path.join( + root, + "plugins/aidd-context/skills/12-cook/scripts/validate-recipe.mjs", +); + +function run(args, cwd = root) { + return spawnSync(process.execPath, [script, ...args], { + cwd, + encoding: "utf8", + }); +} + +function fixture(lines) { + const directory = fs.mkdtempSync(path.join(os.tmpdir(), "aidd-recipe-validator-")); + const file = path.join(directory, "recipe.md"); + fs.writeFileSync(file, `${lines.join("\n")}\n`, "utf8"); + return { directory, file }; +} + +function validDirect(title = "Valid recipe") { + return [ + `# ${title}`, + "", + "Produce one observable result.", + "", + "## Steps to produce the result", + "", + "### 1) โœ… Run the check", + "", + "This command returns the observable result.", + "", + "```bash", + "echo ok", + "```", + "", + "## Verify", + "", + "- Confirm the command prints `ok`.", + ]; +} + +function filledTemplate() { + let template = fs.readFileSync( + path.join(root, "plugins/aidd-context/skills/12-cook/assets/recipe-template.md"), + "utf8", + ); + template = template.split("\n").filter((line) => !line.startsWith("> Fill ")).join("\n"); + const replacements = new Map([ + ["<Recipe title>", "Check a Node.js installation"], + ["<One sentence describing what this recipe gets the reader.>", "Check the installed Node.js runtime and configuration."], + ["<Short and benefit-first, one idea per line. Lead with the keywords a reader would search, **bold** the key terms.>", "**Runtime checks** reveal whether this environment can run the project."], + ["<the outcome the reader achieves>", "check the runtime"], + ["<emoji>", "โœ…"], + ["<First step title>", "Check the version"], + ["<One benefit-focused line of what and why, in prose.>", "Check the version before running project commands."], + ["<where it is, then install it from its URL>", "Install Node.js from https://nodejs.org/en/download."], + ["<how to invoke it โ€” its real command or slash>", "Run `node --version`."], + ["<command the reader runs>", "node --version"], + ["<the useful output it prints, trimmed to what matters>", "v22.12.0"], + ["<Next step title>", "Check the configuration"], + ["<Benefit-focused what and why, in prose.>", "Check the configuration before enabling the feature."], + ["<action>", "Read the configuration."], + ["<lang>", "json"], + ["<a config or snippet the reader can copy>", '{"enabled": true}'], + ["<Last step title โ€” until the goal is reached>", "Identify the runtime"], + ["<what this screenshot or video shows>", "Node.js logo"], + ["<path-or-url>", "https://nodejs.org/static/logos/nodejsDark.svg"], + ["<Optional. An observable check that proves it worked: a command, a UI state, a file that now exists.>", "Confirm `node --version` prints the installed version."], + ]); + for (const [placeholder, value] of replacements) template = template.replaceAll(placeholder, value); + return template.split("\n"); +} + +test("validates the filled Markdown template with Why, all categories, and command/config/image examples", () => { + const item = fixture(filledTemplate()); + try { + const before = fs.readFileSync(item.file, "utf8"); + const result = run([item.file]); + assert.equal(result.status, 0, result.stderr); + assert.equal(fs.readFileSync(item.file, "utf8"), before); + } finally { + fs.rmSync(item.directory, { recursive: true, force: true }); + } +}); + +test("validates direct and categorized recipes", () => { + const direct = fixture(validDirect()); + const categorized = fixture([ + "# Categorized recipe", + "", + "Produce two observable results.", + "", + "## Steps to produce the results", + "", + "### ๐ŸŸข Beginner", + "", + "#### 1) โœ… Run the first check", + "", + "This command confirms the prerequisite.", + "", + "```bash", + "echo first", + "```", + "", + "### ๐Ÿ”ด Expert", + "", + "#### 2) ๐Ÿ”ฌ Run the second check", + "", + "This command confirms the advanced result.", + "", + "```bash", + "echo second", + "```", + ]); + + try { + const directBefore = fs.readFileSync(direct.file, "utf8"); + const categorizedBefore = fs.readFileSync(categorized.file, "utf8"); + const result = run([direct.file, categorized.file]); + assert.equal(result.status, 0, result.stderr); + assert.equal(result.stdout, "PASS: 2 recipe(s) validated.\n"); + assert.equal(fs.readFileSync(direct.file, "utf8"), directBefore); + assert.equal(fs.readFileSync(categorized.file, "utf8"), categorizedBefore); + } finally { + fs.rmSync(direct.directory, { recursive: true, force: true }); + fs.rmSync(categorized.directory, { recursive: true, force: true }); + } +}); + +test("reports invalid title and non-continuous steps with line numbers", () => { + const item = fixture([ + "Intro before title.", + "# Broken recipe", + "", + "Produce a result.", + "", + "## Steps to produce a result", + "", + "### 2) โœ… Run the check", + "", + "This command produces a result.", + "", + "```bash", + "echo ok", + "```", + ]); + + try { + const result = run([item.file]); + assert.equal(result.status, 1); + assert.match(result.stderr, /\| .*recipe\.md \| 2 \| title \|/u); + assert.match(result.stderr, /\| .*recipe\.md \| 8 \| step-number \|/u); + } finally { + fs.rmSync(item.directory, { recursive: true, force: true }); + } +}); + +test("reports missing examples, broken fences, placeholders, and invalid JSON", () => { + const item = fixture([ + "# Broken examples", + "", + "Produce a result.", + "", + "## Steps to produce a result", + "", + "### 1) โœ… Explain only", + "", + "This step still contains <placeholder>.", + "", + "### 2) ๐Ÿ”ง Parse JSON", + "", + "This malformed example must fail.", + "", + "```json", + "{\"broken\": }", + "```", + "", + "### 3) ๐Ÿงช Close the fence", + "", + "This example has no closing marker.", + "", + "```text", + "TODO", + ]); + + try { + const result = run([item.file]); + assert.equal(result.status, 1); + assert.match(result.stderr, /step-example/u); + assert.match(result.stderr, /fence-balance/u); + assert.match(result.stderr, /placeholder/u); + assert.match(result.stderr, /json-syntax/u); + assert.match(result.stderr, /todo/u); + } finally { + fs.rmSync(item.directory, { recursive: true, force: true }); + } +}); + +test("accepts plain descriptions without guessing sentence boundaries", () => { + for (const description of [ + "Filter noisy output with a CLI, e.g. RTK.", + "Produce one result. Then produce another.", + "Filter noisy output with a CLI", + ]) { + const lines = validDirect(); + lines[2] = description; + const item = fixture(lines); + try { + const result = run([item.file]); + assert.equal(result.status, 0, result.stderr); + } finally { + fs.rmSync(item.directory, { recursive: true, force: true }); + } + } +}); + +test("requires a non-empty plain description before the sections", () => { + for (const description of [ + "", "## Description", "- Filter output.", "* Filter output.", "+ Filter output.", + "1. Filter output.", "1) Filter output.", "> Filter output.", + "| Goal | Filter output. |", "---", "```text", "~~~text", + "![Output](https://example.com/output.png)", + ]) { + const lines = validDirect(); + lines[2] = description; + const item = fixture(lines); + try { + const result = run([item.file]); + assert.equal(result.status, 1, description); + assert.match(result.stderr, /\| description \|/u, description); + } finally { + fs.rmSync(item.directory, { recursive: true, force: true }); + } + } +}); + +test("accepts useful introductory context", () => { + const lines = validDirect(); + lines.splice(3, 0, "", "Run this check after installing the project prerequisites."); + const item = fixture(lines); + + try { + const result = run([item.file]); + assert.equal(result.status, 0, result.stderr); + } finally { + fs.rmSync(item.directory, { recursive: true, force: true }); + } +}); + +test("accepts an optional useful Why section", () => { + const lines = validDirect(); + lines.splice(4, 0, "## Why", "", "Check prerequisites before spending time on a full build.", ""); + const item = fixture(lines); + + try { + const result = run([item.file]); + assert.equal(result.status, 0, result.stderr); + } finally { + fs.rmSync(item.directory, { recursive: true, force: true }); + } +}); + +test("accepts useful prose with more than one sentence", () => { + const lines = validDirect(); + lines[8] = "This command is fast. It returns the observable result."; + const item = fixture(lines); + + try { + const result = run([item.file]); + assert.equal(result.status, 0, result.stderr); + } finally { + fs.rmSync(item.directory, { recursive: true, force: true }); + } +}); + +test("accepts JSX tags as concrete examples", () => { + const lines = validDirect(); + lines[10] = "```jsx"; + lines[11] = "const view = <Button>Save</Button>;"; + const item = fixture(lines); + + try { + const result = run([item.file]); + assert.equal(result.status, 0, result.stderr); + } finally { + fs.rmSync(item.directory, { recursive: true, force: true }); + } +}); + +test("accepts C include directives as concrete examples", () => { + const lines = validDirect(); + lines[10] = "```c"; + lines[11] = "#include <stdio.h>"; + const item = fixture(lines); + + try { + const result = run([item.file]); + assert.equal(result.status, 0, result.stderr); + } finally { + fs.rmSync(item.directory, { recursive: true, force: true }); + } +}); + +test("reports broken local targets and heading anchors", () => { + const item = fixture([ + "# Broken links", + "", + "Produce a result.", + "", + "## Steps to produce a result", + "", + "### 1) โœ… Open the reference", + "", + "This step links to [nothing](missing.md) and [no heading](#absent).", + "", + "```text", + "open reference", + "```", + ]); + + try { + const result = run([item.file]); + assert.equal(result.status, 1); + assert.match(result.stderr, /link-target/u); + assert.match(result.stderr, /link-anchor/u); + } finally { + fs.rmSync(item.directory, { recursive: true, force: true }); + } +}); + +test("accepts an optional table of contents on a short recipe without Verify", () => { + const lines = validDirect(); + lines.splice(3, 0, "", "- [Steps](#steps-to-produce-the-result)", "- [Run the check](#1--run-the-check)"); + lines.splice(lines.indexOf("## Verify")); + const item = fixture(lines); + + try { + const result = run([item.file]); + assert.equal(result.status, 0, result.stderr); + } finally { + fs.rmSync(item.directory, { recursive: true, force: true }); + } +}); + +test("rejects duplicate verification", () => { + const lines = validDirect(); + lines.push("", "## Verify again", "", "- Confirm it twice."); + const item = fixture(lines); + + try { + const result = run([item.file]); + assert.equal(result.status, 1); + assert.match(result.stderr, /verify-section/u); + } finally { + fs.rmSync(item.directory, { recursive: true, force: true }); + } +}); + +test("--all includes project recipes and bundled recipes", () => { + const directory = fs.mkdtempSync(path.join(os.tmpdir(), "aidd-recipe-all-")); + const projectRecipes = path.join(directory, "aidd_docs", "recipes"); + fs.mkdirSync(projectRecipes, { recursive: true }); + fs.writeFileSync(path.join(projectRecipes, "project.md"), `${validDirect("Project recipe").join("\n")}\n`, "utf8"); + + try { + const result = run(["--all"], directory); + assert.equal(result.status, 0, result.stderr); + const match = result.stdout.match(/PASS: (\d+) recipe\(s\) validated\./u); + assert.ok(match, result.stdout); + assert.equal(Number.parseInt(match[1], 10), 4, result.stdout); + } finally { + fs.rmSync(directory, { recursive: true, force: true }); + } +}); + +test("requires paths or --all", () => { + const result = run([]); + assert.equal(result.status, 1); + assert.match(result.stderr, /pass at least one recipe path or --all/u); +}); + +test("runs --all through a symlinked script entry point", () => { + const directory = fs.mkdtempSync(path.join(os.tmpdir(), "aidd-recipe-symlink-")); + const entry = path.join(directory, "validate-recipe.mjs"); + try { + fs.symlinkSync(script, entry); + const result = spawnSync(process.execPath, [entry, "--all"], { cwd: directory, encoding: "utf8" }); + assert.equal(result.status, 0, result.stderr); + assert.equal(result.stdout, "PASS: 3 recipe(s) validated.\n"); + assert.equal(result.stderr, ""); + } finally { + fs.rmSync(directory, { recursive: true, force: true }); + } +}); + +test("imports without running validation when the process entry is absent or nonexistent", () => { + const directory = fs.mkdtempSync(path.join(os.tmpdir(), "aidd-recipe-import-")); + try { + for (const entry of [undefined, path.join(directory, "missing.mjs")]) { + const source = `process.argv[1] = ${JSON.stringify(entry)}; await import(${JSON.stringify(pathToFileURL(script).href)}); console.log("imported");`; + const result = spawnSync(process.execPath, ["--input-type=module", "-e", source], { encoding: "utf8" }); + assert.equal(result.status, 0, result.stderr); + assert.equal(result.stdout, "imported\n"); + assert.equal(result.stderr, ""); + } + } finally { + fs.rmSync(directory, { recursive: true, force: true }); + } +}); + +test("renders the recipe template as visible Markdown", () => { + const template = fs.readFileSync( + path.join(root, "plugins/aidd-context/skills/12-cook/assets/recipe-template.md"), + "utf8", + ); + for (const content of [template, template.replace(/\r?\n/gu, "\r\n")]) { + assert.match(content, /^# <Recipe title>\r?\n/u); + } + assert.ok(!template.includes("<!--")); + for (const section of ["## Why", "### ๐ŸŸข Beginner", "### ๐ŸŸก Intermediate", "### ๐Ÿ”ด Expert", "## Verify"]) { + assert.ok(template.includes(section), section); + } + assert.ok(template.includes("```bash")); + assert.ok(template.includes("```<lang>")); + assert.ok(template.includes("![<what this screenshot or video shows>](<path-or-url>)")); +}); + +test("reports skipped heading levels and discontinuous numbering across categories", () => { + const lines = validDirect(); + lines[6] = "#### 2) โœ… Run the check"; + const item = fixture(lines); + try { + const before = fs.readFileSync(item.file, "utf8"); + const result = run([item.file]); + assert.equal(result.status, 1); + assert.match(result.stderr, /heading-depth/u); + assert.match(result.stderr, /step-number/u); + assert.equal(fs.readFileSync(item.file, "utf8"), before); + } finally { + fs.rmSync(item.directory, { recursive: true, force: true }); + } +}); + +test("reports visible scaffold placeholders, including capitals and punctuation", () => { + const lines = validDirect(); + lines[8] = "<One benefit-focused line of what and why, in prose.>"; + lines[11] = "<a config or snippet the reader can copy>"; + const item = fixture(lines); + try { + const result = run([item.file]); + assert.equal(result.status, 1); + assert.match(result.stderr, /\| .*recipe\.md \| 9 \| placeholder \|/u); + assert.match(result.stderr, /\| .*recipe\.md \| 12 \| placeholder \|/u); + } finally { + fs.rmSync(item.directory, { recursive: true, force: true }); + } +}); + +test("resolves heading anchors with inline HTML and encoded local paths", () => { + const lines = validDirect(); + lines.push("", "[Reference](reference%20notes.md#details--examples)"); + const item = fixture(lines); + fs.writeFileSync(path.join(item.directory, "reference notes.md"), "# <em>Details</em> & Examples\n", "utf8"); + try { + const result = run([item.file]); + assert.equal(result.status, 0, result.stderr); + } finally { + fs.rmSync(item.directory, { recursive: true, force: true }); + } +}); + +test("resolves inline text and ignores nested or unterminated tags in heading anchors", () => { + const lines = validDirect(); + lines.push("", "[Inline](reference.md#foobar)", "[Nested](reference.md#startfinish)", "[Unterminated](reference.md#safe)"); + const item = fixture(lines); + fs.writeFileSync(path.join(item.directory, "reference.md"), "# foo<strong>bar</strong>\n## start<outer <inner>hidden>finish\n## safe<script\n", "utf8"); + try { + const result = run([item.file]); + assert.equal(result.status, 0, result.stderr); + } finally { + fs.rmSync(item.directory, { recursive: true, force: true }); + } +}); + +test("reports missing recipe paths and directories as structured findings", () => { + const item = fixture(validDirect()); + try { + for (const [target, rule] of [[path.join(item.directory, "missing.md"), "file-exists"], [item.directory, "file-type"]]) { + const result = run([target]); + assert.equal(result.status, 1); + assert.match(result.stderr, /\| File \| Line \| Rule \| Fix \|/u); + assert.ok(result.stderr.includes(`| ${rule} |`), result.stderr); + assert.doesNotMatch(result.stderr, /at validateRecipe/u); + } + } finally { + fs.rmSync(item.directory, { recursive: true, force: true }); + } +}); + +test("reports directory Markdown link targets without throwing", () => { + const lines = validDirect(); + lines.push("", "[Reference](directory.md#details)"); + const item = fixture(lines); + fs.mkdirSync(path.join(item.directory, "directory.md")); + try { + const result = run([item.file]); + assert.equal(result.status, 1); + assert.match(result.stderr, /\| .*recipe\.md \| 19 \| link-target \|/u); + assert.doesNotMatch(result.stderr, /at checkLinks/u); + } finally { + fs.rmSync(item.directory, { recursive: true, force: true }); + } +}); + +test("reports read failures as findings and closes the recipe descriptor", async (t) => { + const { validateRecipe } = await import(pathToFileURL(script).href); + const item = fixture(validDirect()); + const close = fs.closeSync; + const closed = []; + try { + t.mock.method(fs, "readFileSync", () => { throw Object.assign(new Error("Permission denied"), { code: "EACCES" }); }); + t.mock.method(fs, "closeSync", (fd) => { closed.push(fd); return close(fd); }); + const findings = validateRecipe(item.file); + assert.ok(findings.some((finding) => finding.rule === "file-read"), JSON.stringify(findings)); + assert.equal(closed.length, 1); + } finally { + t.mock.restoreAll(); + fs.rmSync(item.directory, { recursive: true, force: true }); + } +}); + +test("reports unreadable referenced Markdown and closes its descriptor", async (t) => { + const { validateRecipe } = await import(pathToFileURL(script).href); + const lines = validDirect(); + lines.push("", "[Reference](reference.md#details)"); + const item = fixture(lines); + const reference = path.join(item.directory, "reference.md"); + fs.writeFileSync(reference, "# Details\n", "utf8"); + const open = fs.openSync; + const read = fs.readFileSync; + const close = fs.closeSync; + const descriptors = new Set(); + const closed = []; + try { + t.mock.method(fs, "openSync", (target, ...args) => { + const fd = open(target, ...args); + if (target === reference) descriptors.add(fd); + return fd; + }); + t.mock.method(fs, "readFileSync", (target, ...args) => { + if (target === reference || descriptors.has(target)) throw Object.assign(new Error("Permission denied"), { code: "EACCES" }); + return read(target, ...args); + }); + t.mock.method(fs, "closeSync", (fd) => { closed.push(fd); return close(fd); }); + const findings = validateRecipe(item.file); + assert.ok(findings.some((finding) => finding.rule === "link-target" && finding.line === 19), JSON.stringify(findings)); + assert.equal(descriptors.size, 1); + for (const fd of descriptors) assert.ok(closed.includes(fd)); + } finally { + t.mock.restoreAll(); + fs.rmSync(item.directory, { recursive: true, force: true }); + } +});