Skip to content

ci: promote next to main - #809

Merged
aidd-bot[bot] merged 180 commits into
mainfrom
promote/next-to-main-34395640885
Sep 9, 2026
Merged

ci: promote next to main#809
aidd-bot[bot] merged 180 commits into
mainfrom
promote/next-to-main-34395640885

Conversation

@aidd-bot

@aidd-bot aidd-bot Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Automated promotion of next to main, from a snapshot of next taken at run 34395640885. Merged as a merge commit so main keeps every conventional commit for release-please, and so both branches keep a shared merge base for the back-merge. Do not squash, do not rebase.

blafourcade and others added 30 commits July 16, 2026 13:10
Convert every `@../` reference in the action files of skill-generate and
project-memory to standard markdown links `[name](path)`, so citations are
clickable in editors and on GitHub.

Propagate the convention to the contract so generated skills follow suit:
R6 now mandates a markdown link, R8 drops the `@`-chain wording, and
review-protocol says "cited references". Functional `@`-imports in the memory
block (`@aidd_docs/...`) are untouched: those are load mechanics, not citations.

Sync docs/CATALOG.md skill-generate action names to the shipped set.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0177RKF7c5Wpv329Q7QYquV2
…ork (#440)

* feat(aidd-context): add research and apply actions to cook skill

Extend 12-cook from author-only to a fuller loop:

- 03-research: refine the target recipe via a checklist, fan out one
  agent per scouting angle, then present alternatives / coverage gaps /
  counter-intuitive wins plus a recommendation. Ephemeral, never writes.
- 04-apply: execute an existing recipe against the project via an agent
  that tracks the steps as a todo list and confirms before mutating.
- upsert: qualitative overlap guard (none/partial/high) that suggests
  updating an existing recipe instead of creating a duplicate.
- recipe-template: steps become "#### N)" headings with what/why,
  actionable how-bullets, and a mandatory concrete example
  (command with output, snippet, or screenshot/video).

New references/assets: research-playbook, refine-goal-checklist,
research-checklist.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(aidd-context): verify each research candidate before presenting

Add a Verify step to cook's 03-research: after curation, spawn one
agent per surviving candidate to confirm it exists, capture its
official link, and record its latest state. Drop anything that cannot
be confirmed against an official source.

This makes the checklist's "Confirm claims" real and guards the recipe
against hallucinated tools or methods.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(framework): add token optimization recipe

Add recipes/token-optimization.md: a ranked list of token-saving tips
for AI coding assistants (measure first, trim CLAUDE.md, caveman,
RTK/SNIP, CLI over MCP, progressive disclosure, compact, model routing,
cap thinking), each tip in the new template format with a concrete
example and an official link verified to exist. Register it in the
recipes index.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(aidd-context): extract recipe contract and tighten cook template

Move the recipe authoring rules out of the template's HTML comment into
references/recipe-contract.md, cited by SKILL.md and upsert. The template
keeps only the scaffold.

- Steps heading carries the goal ("## Steps to <outcome>") and each step
  opens with an emoji.
- Step how-bullets follow where-it-is -> install-from-URL -> how-to-use,
  benefit-first and filler-free.
- "## Why" is short, keyword-led, and bold.
- SKILL.md description is shorter and high-level, triggered on
  "recipe" / "cook" / "/cook".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(aidd-context): repair 12-cook SKILL.md YAML frontmatter

The description contained an unquoted "sheets: list" colon, which YAML
parses as a mapping value and rejects ("mapping values are not allowed
here"), breaking the skill's frontmatter. Replace the colon with a dash.
The bug pre-dated this branch (same colon on main).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(aidd-context): refine cook recipe structure and apply R13 to cook

Review feedback on the cook skill, plus the new includes convention:

- Recipe template drops the Time field, groups steps under three level
  subheadings (Beginner / Intermediate / Expert), and aligns the Level
  enum to Expert.
- upsert now runs `research` (03) first for any new or substantial
  recipe and drafts only from its verified results, never from memory —
  the "always research" rule was being skipped as a routing note.
- Apply R13 (explicit, scoped includes): the action-specific research
  aids move out of SKILL.md and are cited from 03-research in fenced
  blocks; refine-goal-checklist is renamed research-goal-checklist;
  SKILL.md lists only the global recipe-template and recipe-contract.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(aidd-context): make recipe level subheadings optional

Level grouping was effectively mandatory. Make the contract state it is
optional: group steps under Beginner/Intermediate/Expert only when the
recipe spans difficulty levels and grouping helps; a short or
single-level recipe lists its steps directly. Relax the upsert Test
accordingly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(framework): conform token-optimization recipe to recipe contract

Align the recipe with references/recipe-contract.md: drop the Time row
(no longer in the template), name the section "Steps to <outcome>" instead
of a bare Steps, remove the "ranked by impact" filler line, and give each
step how-bullets plus a concrete example.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(framework): split measure steps per tool and drop metadata table

Apply review of the token-optimization recipe:

- Goal becomes a plain sentence (no blockquote).
- Drop the Level/Prerequisites table; level now lives in the subheadings.
- Split the bundled "Measure first" step into one step per tool
  (/context, /cost, /insights, an analytics tool) — one subheading per
  action.
- Each measure step says how to use and read the command, and carries an
  example output; the analytics step uses a real prompt-analytics run
  (volumes rounded, cost omitted) showing cache reads dominate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(framework): drop the Goal label from token-optimization

The recipe opens with a plain description sentence, no "Goal:" label.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(aidd-context): recipe header = description sentence, no table

Rules extracted from the token-optimization recipe review:

- A recipe opens with the H1 title then one plain description sentence —
  no "Goal:" label, no blockquote, no metadata table. list/upsert parse
  that sentence; the index becomes `| Recipe | Description |` (Level was
  dropped, level now lives only in the optional subheadings).
- One step = one action; never bundle several tools or commands under a
  single heading.
- Every step's example shows real command output (from docs or by
  running it); the research playbook now requires capturing a real
  example and marking interactive output for the human to paste.
- `## Why` is one idea per line.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(framework): use the real prompt-analytics dashboard image

Replace the fabricated text output with the tool's official dashboard
screenshot (cost by token type makes "cache reads dominate" visible).
The slash-command placeholders now ask for a screenshot of the real
output.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(aidd-context): prefer images over text examples

A real screenshot or GIF is unfakeable, faithful, and credible, where a
text example can be approximated (the failure we hit). Make the contract
and the research playbook prefer an image that matches the action —
e.g. a tool's official screenshot — falling back to real text output
only when no image exists.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(framework): show real tool usage in token-optimization

- caveman: show its real `/caveman` invocation (and modes), not just a
  before/after.
- RTK: a Mermaid diagram of the proxy flow + the real `rtk <command>`
  syntax (was a wrong `rtk proxy …`) and the README's own 200->10 token
  saving.
- CLI vs MCP: a comparison table instead of prose.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(aidd-context): add tool-example rules to recipe contract

From the token-optimization review:

- Show how to invoke a tool (its real command or slash invocation).
- Reuse the canonical example from the tool's own site or README, never
  an invented one.
- A "prefer X over Y" step uses a comparison table, not prose.
- A structural or flow concept (proxy, pipeline, architecture) gets a
  small Mermaid diagram.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(framework): use caveman's real before/after example

Replace the invented caveman before/after with the verbatim example from
its README (rule F: reuse the tool's own canonical example).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(framework): numbered actions, valid-JSON config, tighter sections

- Actions are numbered lists; descriptions are prose (no description-bullets).
- settings.json shown as valid JSON, not a bash fragment.
- RTK Mermaid uses concrete values (cargo test ~25k -> ~2.5k tokens).
- CLI vs MCP notes MCP tool/context selection (cheaper, still slower).
- Step 10 reworded plainly ("load knowledge on demand").
- Drop the weak Verify and the redundant Related; add a short conclusion.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(aidd-context): add bullet/diagram/config/section rules to contract

From the token-optimization review:

- Actions are a numbered list; descriptions and indications are prose,
  never bullets.
- A Mermaid diagram carries concrete example values.
- A config example uses the file's real syntax (valid JSON for
  settings.json, valid YAML for frontmatter, ...).
- Reuse a tool's canonical example captured verbatim — never a paraphrase
  or on the strength of a summary that one exists.
- No `## Related` section (links live inline); `## Verify` is optional;
  recipes may end with a short conclusion.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(framework): expert steps use verified Claude Code config

- Step 10 now audits which skills/tools run via `Ctrl+O` (toggles the
  detailed transcript), instead of the vaguer "load on demand".
- Step 11 routes by difficulty with the real `model:` frontmatter field
  on a skill or agent (haiku/sonnet/opus/inherit) — example agent pinned
  to haiku.

Both verified against the official Claude Code docs (interactive-mode,
skills, sub-agents).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(framework): point the instruction-file step at AGENTS.md

Use the framework's own concise AGENTS.md as the worked example for a
trimmed instruction file, with its real rules (answer first, no
narration), instead of a generic snippet.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(aidd-context): apply analyses a recipe and asks before acting

A recipe is often a human tutorial, not an agent-runnable procedure. So
apply now reads the recipe, classifies each step agent-doable vs
human-only, and asks the user what to do before any change — then runs
the chosen agent-doable steps and reports the human-only ones.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(framework): add five verified token levers to the recipe

From a research pass, all verified against official Claude Code docs:

- Plan mode (Shift+Tab ×2) — avoid execution-phase rework.
- `/clear` between tasks — drop stale context instead of re-billing it.
- Deny reads (`permissions.deny`) — keep vendor/dist/secrets out of
  context; the official answer to the unshipped `.claudeignore`.
- Subagent offload — run high-volume ops in an isolated context.
- Protect cache hits — avoid mid-task model/MCP/effort switches that
  invalidate the cache (closes the loop on "cache reads dominate").

Also fold the built-in `outputStyle: concise` into the "talk less" step.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(aidd-context): bundle cook recipes in skill

* feat(framework): publish AIDDy v2 pet

* docs(framework): reposition readme as agnostic token-optimized framework

* feat: avoid uncached AGENTS.md

* docs: add fixing behavior for assertions

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* docs(framework): update README title and tagline

Align the project messaging with the framework's current branding and focus.

* chore: release main (#453)

Co-authored-by: aidd-bot[bot] <290648294+aidd-bot[bot]@users.noreply.github.com>

---------

Co-authored-by: Alex <8973343+alexsoyes@users.noreply.github.com>
Co-authored-by: aidd-bot[bot] <290648294+aidd-bot[bot]@users.noreply.github.com>
Co-authored-by: Baptiste LAFOURCADE <baptiste.lafourcade@gmail.com>
…455)

Splits source out of gather so origin selection (conversation, file,
diff, review) is explicit before extraction runs, and renames
assets/decision-template.md to adr-template.md to match its actual
output. CATALOG.md's 10-learn index is regenerated to match.

Folds in fixes found during a headless claude+codex audit of the
skill (13 scenarios x 2 tools):
- sync locates update_memory.js instead of assuming a fixed path that
  doesn't exist in either tool's real install layout
- never scaffold the memory bank, even under a blanket write
  pre-approval
- sync's own git-diff-cached test reflects a pre-staged index instead
  of demanding it be empty
- sources.md again defaults to conversation when no hint is given,
  matching pre-refactor behavior, instead of always requiring context
  inference
- the two memory-write transversal rules merge into one, trimming the
  router closer to the smallest shape that still states every rule

Closes #278
Refs #419
Bumps [github/codeql-action/analyze](https://github.com/github/codeql-action) from 4.37.0 to 4.37.1.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](github/codeql-action@99df26d...7188fc3)

---
updated-dependencies:
- dependency-name: github/codeql-action/analyze
  dependency-version: 4.37.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…460)

Bumps [github/codeql-action/autobuild](https://github.com/github/codeql-action) from 4.37.0 to 4.37.1.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](github/codeql-action@99df26d...7188fc3)

---
updated-dependencies:
- dependency-name: github/codeql-action/autobuild
  dependency-version: 4.37.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…rved

Merges ai-driven-dev/aidd-cli into this repo as cli/ via git subtree —
full commit history preserved (reachable via the subtree-merge commit's
second parent, kept on the source branch chore/migrate-cli-into-framework,
not deleted by this squash). cli/ stays self-contained: own package.json,
own lockfile, not a pnpm workspace member.

- release-please: cli added as a ninth package, release-type: node
- CI: new cli-ci.yml (typecheck/lint/test/build/knip/jscpd), native
  path-filter on cli/** only
- publish-cli in ci.yml, gated on release-please's paths_released,
  real npm OIDC trusted publishing (no NPM_TOKEN secret)
- Fixed along the way: npm README stub bug, a pnpm-version/cache-path
  CI bug, a stale duplicate memory-sync script and its dead test,
  hardcoded personal paths, dead ISSUE_TEMPLATE/CONTRIBUTING.md left
  over from the standalone repo
- ai-driven-dev/aidd-cli stays active, unarchived, until a real release
  from here is proven end to end

Refs #448. Full plan, phase files, and audit reports:
cli/aidd_docs/tasks/2026_07/2026_07_20_migrate-cli-into-framework/
package.json never had repository/homepage/bugs fields at all (the
migration plan assumed they existed and needed updating — they didn't
exist). Added them pointing at ai-driven-dev/framework with
directory: "cli" so npm's package page links to the right subtree.

aidd_docs/README.md (the standard scaffold every aidd setup generates)
had one stale link to the old standalone repo.
package.json was missing license (npm shows this prominently on the
package page) and keywords (npm search discoverability). license: MIT
matches framework's own root LICENSE — same repo, same license, no
ambiguity to resolve.
…474)

Bumps [github/codeql-action/autobuild](https://github.com/github/codeql-action) from 4.37.1 to 4.37.3.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](github/codeql-action@7188fc3...e4fba86)

---
updated-dependencies:
- dependency-name: github/codeql-action/autobuild
  dependency-version: 4.37.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [@biomejs/biome](https://github.com/biomejs/biome/tree/HEAD/packages/@biomejs/biome) from 2.4.7 to 2.5.5.
- [Release notes](https://github.com/biomejs/biome/releases)
- [Changelog](https://github.com/biomejs/biome/blob/main/packages/@biomejs/biome/CHANGELOG.md)
- [Commits](https://github.com/biomejs/biome/commits/@biomejs/biome@2.5.5/packages/@biomejs/biome)

---
updated-dependencies:
- dependency-name: "@biomejs/biome"
  dependency-version: 2.5.5
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [fast-check](https://github.com/dubzzz/fast-check/tree/HEAD/packages/fast-check) from 4.7.0 to 4.9.0.
- [Release notes](https://github.com/dubzzz/fast-check/releases)
- [Changelog](https://github.com/dubzzz/fast-check/blob/main/packages/fast-check/CHANGELOG.md)
- [Commits](https://github.com/dubzzz/fast-check/commits/v4.9.0/packages/fast-check)

---
updated-dependencies:
- dependency-name: fast-check
  dependency-version: 4.9.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [knip](https://github.com/webpro-nl/knip/tree/HEAD/packages/knip) from 6.16.1 to 6.27.0.
- [Release notes](https://github.com/webpro-nl/knip/releases)
- [Commits](https://github.com/webpro-nl/knip/commits/knip@6.27.0/packages/knip)

---
updated-dependencies:
- dependency-name: knip
  dependency-version: 6.27.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [jscpd](https://github.com/kucherenko/jscpd/tree/HEAD/rust/jscpd) from 5.0.7 to 5.0.12.
- [Release notes](https://github.com/kucherenko/jscpd/releases)
- [Changelog](https://github.com/kucherenko/jscpd/blob/master/CHANGELOG.md)
- [Commits](https://github.com/kucherenko/jscpd/commits/v5.0.12/rust/jscpd)

---
updated-dependencies:
- dependency-name: jscpd
  dependency-version: 5.0.12
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [github/codeql-action/analyze](https://github.com/github/codeql-action) from 4.37.1 to 4.37.3.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](github/codeql-action@7188fc3...e4fba86)

---
updated-dependencies:
- dependency-name: github/codeql-action/analyze
  dependency-version: 4.37.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
* docs(framework): update README title and tagline

Align the project messaging with the framework's current branding and focus.

* chore: release main (#453)

Co-authored-by: aidd-bot[bot] <290648294+aidd-bot[bot]@users.noreply.github.com>

* refactor(aidd-context): action citations use markdown links, not @

Convert every `@../` reference in the action files of skill-generate and
project-memory to standard markdown links `[name](path)`, so citations are
clickable in editors and on GitHub.

Propagate the convention to the contract so generated skills follow suit:
R6 now mandates a markdown link, R8 drops the `@`-chain wording, and
review-protocol says "cited references". Functional `@`-imports in the memory
block (`@aidd_docs/...`) are untouched: those are load mechanics, not citations.

Sync docs/CATALOG.md skill-generate action names to the shipped set.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0177RKF7c5Wpv329Q7QYquV2

* docs(framework): reposition readme as agnostic token-optimized framework (#440)

* feat(aidd-context): add research and apply actions to cook skill

Extend 12-cook from author-only to a fuller loop:

- 03-research: refine the target recipe via a checklist, fan out one
  agent per scouting angle, then present alternatives / coverage gaps /
  counter-intuitive wins plus a recommendation. Ephemeral, never writes.
- 04-apply: execute an existing recipe against the project via an agent
  that tracks the steps as a todo list and confirms before mutating.
- upsert: qualitative overlap guard (none/partial/high) that suggests
  updating an existing recipe instead of creating a duplicate.
- recipe-template: steps become "#### N)" headings with what/why,
  actionable how-bullets, and a mandatory concrete example
  (command with output, snippet, or screenshot/video).

New references/assets: research-playbook, refine-goal-checklist,
research-checklist.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(aidd-context): verify each research candidate before presenting

Add a Verify step to cook's 03-research: after curation, spawn one
agent per surviving candidate to confirm it exists, capture its
official link, and record its latest state. Drop anything that cannot
be confirmed against an official source.

This makes the checklist's "Confirm claims" real and guards the recipe
against hallucinated tools or methods.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(framework): add token optimization recipe

Add recipes/token-optimization.md: a ranked list of token-saving tips
for AI coding assistants (measure first, trim CLAUDE.md, caveman,
RTK/SNIP, CLI over MCP, progressive disclosure, compact, model routing,
cap thinking), each tip in the new template format with a concrete
example and an official link verified to exist. Register it in the
recipes index.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(aidd-context): extract recipe contract and tighten cook template

Move the recipe authoring rules out of the template's HTML comment into
references/recipe-contract.md, cited by SKILL.md and upsert. The template
keeps only the scaffold.

- Steps heading carries the goal ("## Steps to <outcome>") and each step
  opens with an emoji.
- Step how-bullets follow where-it-is -> install-from-URL -> how-to-use,
  benefit-first and filler-free.
- "## Why" is short, keyword-led, and bold.
- SKILL.md description is shorter and high-level, triggered on
  "recipe" / "cook" / "/cook".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(aidd-context): repair 12-cook SKILL.md YAML frontmatter

The description contained an unquoted "sheets: list" colon, which YAML
parses as a mapping value and rejects ("mapping values are not allowed
here"), breaking the skill's frontmatter. Replace the colon with a dash.
The bug pre-dated this branch (same colon on main).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(aidd-context): refine cook recipe structure and apply R13 to cook

Review feedback on the cook skill, plus the new includes convention:

- Recipe template drops the Time field, groups steps under three level
  subheadings (Beginner / Intermediate / Expert), and aligns the Level
  enum to Expert.
- upsert now runs `research` (03) first for any new or substantial
  recipe and drafts only from its verified results, never from memory —
  the "always research" rule was being skipped as a routing note.
- Apply R13 (explicit, scoped includes): the action-specific research
  aids move out of SKILL.md and are cited from 03-research in fenced
  blocks; refine-goal-checklist is renamed research-goal-checklist;
  SKILL.md lists only the global recipe-template and recipe-contract.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(aidd-context): make recipe level subheadings optional

Level grouping was effectively mandatory. Make the contract state it is
optional: group steps under Beginner/Intermediate/Expert only when the
recipe spans difficulty levels and grouping helps; a short or
single-level recipe lists its steps directly. Relax the upsert Test
accordingly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(framework): conform token-optimization recipe to recipe contract

Align the recipe with references/recipe-contract.md: drop the Time row
(no longer in the template), name the section "Steps to <outcome>" instead
of a bare Steps, remove the "ranked by impact" filler line, and give each
step how-bullets plus a concrete example.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(framework): split measure steps per tool and drop metadata table

Apply review of the token-optimization recipe:

- Goal becomes a plain sentence (no blockquote).
- Drop the Level/Prerequisites table; level now lives in the subheadings.
- Split the bundled "Measure first" step into one step per tool
  (/context, /cost, /insights, an analytics tool) — one subheading per
  action.
- Each measure step says how to use and read the command, and carries an
  example output; the analytics step uses a real prompt-analytics run
  (volumes rounded, cost omitted) showing cache reads dominate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(framework): drop the Goal label from token-optimization

The recipe opens with a plain description sentence, no "Goal:" label.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(aidd-context): recipe header = description sentence, no table

Rules extracted from the token-optimization recipe review:

- A recipe opens with the H1 title then one plain description sentence —
  no "Goal:" label, no blockquote, no metadata table. list/upsert parse
  that sentence; the index becomes `| Recipe | Description |` (Level was
  dropped, level now lives only in the optional subheadings).
- One step = one action; never bundle several tools or commands under a
  single heading.
- Every step's example shows real command output (from docs or by
  running it); the research playbook now requires capturing a real
  example and marking interactive output for the human to paste.
- `## Why` is one idea per line.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(framework): use the real prompt-analytics dashboard image

Replace the fabricated text output with the tool's official dashboard
screenshot (cost by token type makes "cache reads dominate" visible).
The slash-command placeholders now ask for a screenshot of the real
output.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(aidd-context): prefer images over text examples

A real screenshot or GIF is unfakeable, faithful, and credible, where a
text example can be approximated (the failure we hit). Make the contract
and the research playbook prefer an image that matches the action —
e.g. a tool's official screenshot — falling back to real text output
only when no image exists.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(framework): show real tool usage in token-optimization

- caveman: show its real `/caveman` invocation (and modes), not just a
  before/after.
- RTK: a Mermaid diagram of the proxy flow + the real `rtk <command>`
  syntax (was a wrong `rtk proxy …`) and the README's own 200->10 token
  saving.
- CLI vs MCP: a comparison table instead of prose.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(aidd-context): add tool-example rules to recipe contract

From the token-optimization review:

- Show how to invoke a tool (its real command or slash invocation).
- Reuse the canonical example from the tool's own site or README, never
  an invented one.
- A "prefer X over Y" step uses a comparison table, not prose.
- A structural or flow concept (proxy, pipeline, architecture) gets a
  small Mermaid diagram.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(framework): use caveman's real before/after example

Replace the invented caveman before/after with the verbatim example from
its README (rule F: reuse the tool's own canonical example).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(framework): numbered actions, valid-JSON config, tighter sections

- Actions are numbered lists; descriptions are prose (no description-bullets).
- settings.json shown as valid JSON, not a bash fragment.
- RTK Mermaid uses concrete values (cargo test ~25k -> ~2.5k tokens).
- CLI vs MCP notes MCP tool/context selection (cheaper, still slower).
- Step 10 reworded plainly ("load knowledge on demand").
- Drop the weak Verify and the redundant Related; add a short conclusion.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(aidd-context): add bullet/diagram/config/section rules to contract

From the token-optimization review:

- Actions are a numbered list; descriptions and indications are prose,
  never bullets.
- A Mermaid diagram carries concrete example values.
- A config example uses the file's real syntax (valid JSON for
  settings.json, valid YAML for frontmatter, ...).
- Reuse a tool's canonical example captured verbatim — never a paraphrase
  or on the strength of a summary that one exists.
- No `## Related` section (links live inline); `## Verify` is optional;
  recipes may end with a short conclusion.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(framework): expert steps use verified Claude Code config

- Step 10 now audits which skills/tools run via `Ctrl+O` (toggles the
  detailed transcript), instead of the vaguer "load on demand".
- Step 11 routes by difficulty with the real `model:` frontmatter field
  on a skill or agent (haiku/sonnet/opus/inherit) — example agent pinned
  to haiku.

Both verified against the official Claude Code docs (interactive-mode,
skills, sub-agents).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(framework): point the instruction-file step at AGENTS.md

Use the framework's own concise AGENTS.md as the worked example for a
trimmed instruction file, with its real rules (answer first, no
narration), instead of a generic snippet.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(aidd-context): apply analyses a recipe and asks before acting

A recipe is often a human tutorial, not an agent-runnable procedure. So
apply now reads the recipe, classifies each step agent-doable vs
human-only, and asks the user what to do before any change — then runs
the chosen agent-doable steps and reports the human-only ones.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(framework): add five verified token levers to the recipe

From a research pass, all verified against official Claude Code docs:

- Plan mode (Shift+Tab ×2) — avoid execution-phase rework.
- `/clear` between tasks — drop stale context instead of re-billing it.
- Deny reads (`permissions.deny`) — keep vendor/dist/secrets out of
  context; the official answer to the unshipped `.claudeignore`.
- Subagent offload — run high-volume ops in an isolated context.
- Protect cache hits — avoid mid-task model/MCP/effort switches that
  invalidate the cache (closes the loop on "cache reads dominate").

Also fold the built-in `outputStyle: concise` into the "talk less" step.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(aidd-context): bundle cook recipes in skill

* feat(framework): publish AIDDy v2 pet

* docs(framework): reposition readme as agnostic token-optimized framework

* feat: avoid uncached AGENTS.md

* docs: add fixing behavior for assertions

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(aidd-context): restructure 10-learn into a 5-action router (#455)

Splits source out of gather so origin selection (conversation, file,
diff, review) is explicit before extraction runs, and renames
assets/decision-template.md to adr-template.md to match its actual
output. CATALOG.md's 10-learn index is regenerated to match.

Folds in fixes found during a headless claude+codex audit of the
skill (13 scenarios x 2 tools):
- sync locates update_memory.js instead of assuming a fixed path that
  doesn't exist in either tool's real install layout
- never scaffold the memory bank, even under a blanket write
  pre-approval
- sync's own git-diff-cached test reflects a pre-staged index instead
  of demanding it be empty
- sources.md again defaults to conversation when no hint is given,
  matching pre-refactor behavior, instead of always requiring context
  inference
- the two memory-write transversal rules merge into one, trimming the
  router closer to the smallest shape that still states every rule

Closes #278
Refs #419

* ci(deps): bump github/codeql-action/analyze from 4.37.0 to 4.37.1 (#456)

Bumps [github/codeql-action/analyze](https://github.com/github/codeql-action) from 4.37.0 to 4.37.1.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](github/codeql-action@99df26d...7188fc3)

---
updated-dependencies:
- dependency-name: github/codeql-action/analyze
  dependency-version: 4.37.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* ci(deps): bump github/codeql-action/autobuild from 4.37.0 to 4.37.1 (#460)

Bumps [github/codeql-action/autobuild](https://github.com/github/codeql-action) from 4.37.0 to 4.37.1.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](github/codeql-action@99df26d...7188fc3)

---
updated-dependencies:
- dependency-name: github/codeql-action/autobuild
  dependency-version: 4.37.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* fix(cli): migrate aidd-cli into framework as cli/, full history preserved

Merges ai-driven-dev/aidd-cli into this repo as cli/ via git subtree —
full commit history preserved (reachable via the subtree-merge commit's
second parent, kept on the source branch chore/migrate-cli-into-framework,
not deleted by this squash). cli/ stays self-contained: own package.json,
own lockfile, not a pnpm workspace member.

- release-please: cli added as a ninth package, release-type: node
- CI: new cli-ci.yml (typecheck/lint/test/build/knip/jscpd), native
  path-filter on cli/** only
- publish-cli in ci.yml, gated on release-please's paths_released,
  real npm OIDC trusted publishing (no NPM_TOKEN secret)
- Fixed along the way: npm README stub bug, a pnpm-version/cache-path
  CI bug, a stale duplicate memory-sync script and its dead test,
  hardcoded personal paths, dead ISSUE_TEMPLATE/CONTRIBUTING.md left
  over from the standalone repo
- ai-driven-dev/aidd-cli stays active, unarchived, until a real release
  from here is proven end to end

Refs #448. Full plan, phase files, and audit reports:
cli/aidd_docs/tasks/2026_07/2026_07_20_migrate-cli-into-framework/

* fix(cli): repoint self-references from aidd-cli to framework

package.json never had repository/homepage/bugs fields at all (the
migration plan assumed they existed and needed updating — they didn't
exist). Added them pointing at ai-driven-dev/framework with
directory: "cli" so npm's package page links to the right subtree.

aidd_docs/README.md (the standard scaffold every aidd setup generates)
had one stale link to the old standalone repo.

* fix(cli): add license and keywords for the npm package page

package.json was missing license (npm shows this prominently on the
package page) and keywords (npm search discoverability). license: MIT
matches framework's own root LICENSE — same repo, same license, no
ambiguity to resolve.

* chore: release main (#485)

Co-authored-by: aidd-bot[bot] <290648294+aidd-bot[bot]@users.noreply.github.com>

* fix(cli): restore correct version after release-please regression (#488)

release-please's PR #485 computed cli's next version as 4.0.0 instead
of bumping forward from 5.1.3. Root cause: cli had no prior cli-v*
tag to anchor its version walk, so it fell through to an old,
otherwise-already-consumed root Release-As: 4.0.0 directive (commit
5b0fc9d, framework's own major-bump forcing from May 2026, unrelated
to cli) and applied it to the new component.

Both npm and GitHub Packages correctly rejected the resulting publish
(existing 4.0.0 from the pre-migration standalone repo, real current
version is 5.1.3 on both registries — untouched). Bad tag/release
cli-v4.0.0 already deleted.

Reverts cli/package.json and the manifest's cli entry to 5.1.3, and
drops the bogus CHANGELOG entry. A cli-v5.1.3 tag/release will be
created on top of this commit to anchor future release-please runs
so the walk never reaches that old directive again.

* chore: release main (#489)

Co-authored-by: aidd-bot[bot] <290648294+aidd-bot[bot]@users.noreply.github.com>

* fix(cli): restore correct version again after second regression, re-anchor for release-please (#493)

A second, unrelated release-please cycle (PR #489, "chore: release
main") auto-merged moments after #488 and independently recomputed
cli's version, reintroducing 4.0.0 the same way #485 did, even
though that run's own log showed it correctly reading 5.1.3 from the
manifest as the baseline. The manifest baseline alone does not bound
the version-bump walk; only a matching cli-v* release does, and none
existed yet when that run started.

Both npm and GitHub Packages rejected the resulting publish again
(pre-existing 4.0.0, same as before), still untouched at the real
5.1.3. Bad tag/release cli-v4.0.0 deleted again.

A cli-v5.1.3 anchor release now exists (pointing at bdd12c6, the
commit right before the cli migration landed), created before this
commit merges, so the next release-please run has a real boundary to
stop its walk at and can no longer reach the old, already-consumed
version-forcing directive from commit 5b0fc9d (framework's own
unrelated major-bump forcing from May 2026, targeting 4.0.0 for the
root package only, not cli).

This commit also serves as the nudge for release-please to
re-evaluate cli against that new anchor and correctly propose 5.1.4
next, carrying the real pending fixes, migration, repoint, license
and keywords, into a real changelog entry and npm and GHP publish.

* chore: release main (#494)

Co-authored-by: aidd-bot[bot] <290648294+aidd-bot[bot]@users.noreply.github.com>

* fix(release-please): exclude manifest file from root path tracking (#497)

Any commit touching .release-please-manifest.json (a root-level
file) counts toward the "." package's own version bump, since root
has no path restriction. Today's cli version-regression fixes needed
to hand-edit that file directly and, as a side effect, cut two extra
framework releases (v5.5.3, v5.5.4) that carried no real content
change for root.

exclude-paths (documented in release-please's manifest schema,
ReleaserConfigOptions) skips a commit for a given package when every
file it touches falls under the listed paths. Scoped to the "."
package only, so cli's and the plugins' own path-based tracking are
unaffected.

* chore: release main (#498)

Co-authored-by: aidd-bot[bot] <290648294+aidd-bot[bot]@users.noreply.github.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Alex <8973343+alexsoyes@users.noreply.github.com>
Co-authored-by: aidd-bot[bot] <290648294+aidd-bot[bot]@users.noreply.github.com>
Co-authored-by: Baptiste LAFOURCADE <baptiste.lafourcade@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Baptiste LAFOURCADE <119650761+blafourcade@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…tion (#506)

Two confirmed bugs on aidd restore / aidd ai restore / aidd ide restore:

A2 (scope leak): RestoreUseCase.runPluginRestore never threaded ctx.toolIds
into RestoreAllPluginsUseCase, so `--tool X` still restored every other
installed AI tool's plugin files unscoped.

A3 (double materialization): RestoreAllUseCase ran two independent,
unconditional passes over the same (tool, plugin) pairs on every global
`aidd restore` — the second pass ignored --tool scope, ignored fileFilter
entirely, and for built-tree tools (cursor/opencode) wrote every plugin
file to disk twice with no hash guard.

RestoreAllPluginsUseCase now takes an optional toolIds filter and returns
{totalFiles, pluginNames} so the single remaining pass can still report
"Restored plugins: x, y". RestorePluginUseCase deleted — its one caller
was the removed second pass (confirmed via grep).

2048/2048 tests green (2042 existing + 6 new regression tests), tsc clean.
Pre-fix reproduction verified via git stash before each fix (double
materialization count, interactive file selection not excluding plugins).

Ported from the archived aidd-cli repo (BUG-E3-02 + SPIKE-E3-01,
pre-migration branches) — original work predates the framework migration
(PR #462).
…505)

outDir for this path is always .aidd/cache/built/<marketplace>/<target>, an
aidd-owned disposable cache, never a user directory — collision here means
"cache from a previous build exists", not data at risk. Adds a comment
stating that explicitly and a regression test (real FlatBuildStrategy, not
the fake buildFor stub) pinning the collision-bypass so it fails if force is
ever flipped or outDir stops being cache-only.

Ported from the archived aidd-cli repo (BUG-E2-01, pre-migration branch
docs/e2-01-force-cache-rebuild) — original work predates the framework
migration (PR #462).
…olUseCase (#507) (#508)

StatusAllUseCase, RestoreAllUseCase, and UpdateAllUseCase each rebuilt their
own duplicate of a collaborator deps.ts already constructs and shares with
ai.ts/ide.ts (deps.statusUseCase, deps.restoreUseCase, deps.updateOneToolUseCase
consumed directly by those commands). Two of the four duplicate sites rebuilt
on every execute() call, not just once.

Confirmed all 3 target classes stateless before sharing them as singletons
(no memo maps, no accumulator fields — BulkConflictState is passed as an
execute() parameter, never stored on UpdateOneToolUseCase). Each class now
receives the real instance by constructor injection; every now-dead
raw-sub-dependency param removed rather than left unused.

grep confirms zero remaining "new StatusUseCase/RestoreUseCase/
UpdateOneToolUseCase" outside deps.ts's 3 original construction sites.
2048/2048 tests pass unmodified — zero behavior change, pure DI wiring.

Stacked on fix/e3-02-restore-plugin-scope-dedup (touches the same
restore-all-use-case.ts) — needs that branch merged first.

Ported/implemented fresh from the archived aidd-cli repo's plan
(SPIKE-E1-01 + BUG-E1-02, pre-migration) — original work predates the
framework migration (PR #462).
…or name collisions (#509)

Two genuinely different symbols shared the same name, confirmed via full
read of both sides of each collision — real friction, not cosmetic:
mode-b-flat-materialization-translator.ts already had to locally alias one
of them (`PluginTranslator as PluginTranslatorHelper`) just to compile.

buildClaudeStyleMarketplaceEntry: the catalog-row builder in
marketplace-strategy-helpers.ts (framework build time) renamed to
buildClaudeStyleCatalogEntry — pairs naturally with the neighboring
buildClaudeStyleMarketplace. The domain/capabilities settings-entry builder
(install time, consumed by claude.ts/copilot.ts) keeps its name.

PluginTranslator: the domain content-format-conversion class renamed to
PluginContentTranslator (file renamed to match). The application-layer
strategy interface (Section C protected pattern, 4 implementer/factory
sites) is untouched.

Pure rename, zero behavior change. 2049/2049 tests pass with zero
assertion changes; grep confirms each name now maps to exactly one symbol.
… internal discovery map (#510)

Restructures 01-brainstorm from a flat probing.md + question-angles.md
pair into a discovery-map/readiness/interview-depth apparatus, moves
finalize output to a discovery-brief.md template, and hides internal
process vocabulary from user-facing text by default.

Fixes applied on top of the base restructure, found via headless and
live testing:
- restore the "state a leaning + tradeoff, even unprompted" rule that
  the restructure had silently dropped (SKILL.md, 03-integrate.md)
- restore the "stay at the idea's altitude, leave finer how-to as a
  flagged assumption" guardrail (references/probing.md)
- fold no longer silently overwrites a contradicted fact — confirms
  which stands before folding (03-integrate.md)
- drop the "use the user's language" instruction, never present before
  this refactor either
- trim 04-finalize's Ask step and Test section to stop restating what
  the linked reference files already say

Closes #504

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
* docs(framework): update README title and tagline

Align the project messaging with the framework's current branding and focus.

* chore: release main (#453)

Co-authored-by: aidd-bot[bot] <290648294+aidd-bot[bot]@users.noreply.github.com>

* refactor(aidd-context): action citations use markdown links, not @

Convert every `@../` reference in the action files of skill-generate and
project-memory to standard markdown links `[name](path)`, so citations are
clickable in editors and on GitHub.

Propagate the convention to the contract so generated skills follow suit:
R6 now mandates a markdown link, R8 drops the `@`-chain wording, and
review-protocol says "cited references". Functional `@`-imports in the memory
block (`@aidd_docs/...`) are untouched: those are load mechanics, not citations.

Sync docs/CATALOG.md skill-generate action names to the shipped set.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0177RKF7c5Wpv329Q7QYquV2

* docs(framework): reposition readme as agnostic token-optimized framework (#440)

* feat(aidd-context): add research and apply actions to cook skill

Extend 12-cook from author-only to a fuller loop:

- 03-research: refine the target recipe via a checklist, fan out one
  agent per scouting angle, then present alternatives / coverage gaps /
  counter-intuitive wins plus a recommendation. Ephemeral, never writes.
- 04-apply: execute an existing recipe against the project via an agent
  that tracks the steps as a todo list and confirms before mutating.
- upsert: qualitative overlap guard (none/partial/high) that suggests
  updating an existing recipe instead of creating a duplicate.
- recipe-template: steps become "#### N)" headings with what/why,
  actionable how-bullets, and a mandatory concrete example
  (command with output, snippet, or screenshot/video).

New references/assets: research-playbook, refine-goal-checklist,
research-checklist.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(aidd-context): verify each research candidate before presenting

Add a Verify step to cook's 03-research: after curation, spawn one
agent per surviving candidate to confirm it exists, capture its
official link, and record its latest state. Drop anything that cannot
be confirmed against an official source.

This makes the checklist's "Confirm claims" real and guards the recipe
against hallucinated tools or methods.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(framework): add token optimization recipe

Add recipes/token-optimization.md: a ranked list of token-saving tips
for AI coding assistants (measure first, trim CLAUDE.md, caveman,
RTK/SNIP, CLI over MCP, progressive disclosure, compact, model routing,
cap thinking), each tip in the new template format with a concrete
example and an official link verified to exist. Register it in the
recipes index.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(aidd-context): extract recipe contract and tighten cook template

Move the recipe authoring rules out of the template's HTML comment into
references/recipe-contract.md, cited by SKILL.md and upsert. The template
keeps only the scaffold.

- Steps heading carries the goal ("## Steps to <outcome>") and each step
  opens with an emoji.
- Step how-bullets follow where-it-is -> install-from-URL -> how-to-use,
  benefit-first and filler-free.
- "## Why" is short, keyword-led, and bold.
- SKILL.md description is shorter and high-level, triggered on
  "recipe" / "cook" / "/cook".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(aidd-context): repair 12-cook SKILL.md YAML frontmatter

The description contained an unquoted "sheets: list" colon, which YAML
parses as a mapping value and rejects ("mapping values are not allowed
here"), breaking the skill's frontmatter. Replace the colon with a dash.
The bug pre-dated this branch (same colon on main).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(aidd-context): refine cook recipe structure and apply R13 to cook

Review feedback on the cook skill, plus the new includes convention:

- Recipe template drops the Time field, groups steps under three level
  subheadings (Beginner / Intermediate / Expert), and aligns the Level
  enum to Expert.
- upsert now runs `research` (03) first for any new or substantial
  recipe and drafts only from its verified results, never from memory —
  the "always research" rule was being skipped as a routing note.
- Apply R13 (explicit, scoped includes): the action-specific research
  aids move out of SKILL.md and are cited from 03-research in fenced
  blocks; refine-goal-checklist is renamed research-goal-checklist;
  SKILL.md lists only the global recipe-template and recipe-contract.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(aidd-context): make recipe level subheadings optional

Level grouping was effectively mandatory. Make the contract state it is
optional: group steps under Beginner/Intermediate/Expert only when the
recipe spans difficulty levels and grouping helps; a short or
single-level recipe lists its steps directly. Relax the upsert Test
accordingly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(framework): conform token-optimization recipe to recipe contract

Align the recipe with references/recipe-contract.md: drop the Time row
(no longer in the template), name the section "Steps to <outcome>" instead
of a bare Steps, remove the "ranked by impact" filler line, and give each
step how-bullets plus a concrete example.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(framework): split measure steps per tool and drop metadata table

Apply review of the token-optimization recipe:

- Goal becomes a plain sentence (no blockquote).
- Drop the Level/Prerequisites table; level now lives in the subheadings.
- Split the bundled "Measure first" step into one step per tool
  (/context, /cost, /insights, an analytics tool) — one subheading per
  action.
- Each measure step says how to use and read the command, and carries an
  example output; the analytics step uses a real prompt-analytics run
  (volumes rounded, cost omitted) showing cache reads dominate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(framework): drop the Goal label from token-optimization

The recipe opens with a plain description sentence, no "Goal:" label.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(aidd-context): recipe header = description sentence, no table

Rules extracted from the token-optimization recipe review:

- A recipe opens with the H1 title then one plain description sentence —
  no "Goal:" label, no blockquote, no metadata table. list/upsert parse
  that sentence; the index becomes `| Recipe | Description |` (Level was
  dropped, level now lives only in the optional subheadings).
- One step = one action; never bundle several tools or commands under a
  single heading.
- Every step's example shows real command output (from docs or by
  running it); the research playbook now requires capturing a real
  example and marking interactive output for the human to paste.
- `## Why` is one idea per line.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(framework): use the real prompt-analytics dashboard image

Replace the fabricated text output with the tool's official dashboard
screenshot (cost by token type makes "cache reads dominate" visible).
The slash-command placeholders now ask for a screenshot of the real
output.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(aidd-context): prefer images over text examples

A real screenshot or GIF is unfakeable, faithful, and credible, where a
text example can be approximated (the failure we hit). Make the contract
and the research playbook prefer an image that matches the action —
e.g. a tool's official screenshot — falling back to real text output
only when no image exists.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(framework): show real tool usage in token-optimization

- caveman: show its real `/caveman` invocation (and modes), not just a
  before/after.
- RTK: a Mermaid diagram of the proxy flow + the real `rtk <command>`
  syntax (was a wrong `rtk proxy …`) and the README's own 200->10 token
  saving.
- CLI vs MCP: a comparison table instead of prose.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(aidd-context): add tool-example rules to recipe contract

From the token-optimization review:

- Show how to invoke a tool (its real command or slash invocation).
- Reuse the canonical example from the tool's own site or README, never
  an invented one.
- A "prefer X over Y" step uses a comparison table, not prose.
- A structural or flow concept (proxy, pipeline, architecture) gets a
  small Mermaid diagram.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(framework): use caveman's real before/after example

Replace the invented caveman before/after with the verbatim example from
its README (rule F: reuse the tool's own canonical example).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(framework): numbered actions, valid-JSON config, tighter sections

- Actions are numbered lists; descriptions are prose (no description-bullets).
- settings.json shown as valid JSON, not a bash fragment.
- RTK Mermaid uses concrete values (cargo test ~25k -> ~2.5k tokens).
- CLI vs MCP notes MCP tool/context selection (cheaper, still slower).
- Step 10 reworded plainly ("load knowledge on demand").
- Drop the weak Verify and the redundant Related; add a short conclusion.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(aidd-context): add bullet/diagram/config/section rules to contract

From the token-optimization review:

- Actions are a numbered list; descriptions and indications are prose,
  never bullets.
- A Mermaid diagram carries concrete example values.
- A config example uses the file's real syntax (valid JSON for
  settings.json, valid YAML for frontmatter, ...).
- Reuse a tool's canonical example captured verbatim — never a paraphrase
  or on the strength of a summary that one exists.
- No `## Related` section (links live inline); `## Verify` is optional;
  recipes may end with a short conclusion.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(framework): expert steps use verified Claude Code config

- Step 10 now audits which skills/tools run via `Ctrl+O` (toggles the
  detailed transcript), instead of the vaguer "load on demand".
- Step 11 routes by difficulty with the real `model:` frontmatter field
  on a skill or agent (haiku/sonnet/opus/inherit) — example agent pinned
  to haiku.

Both verified against the official Claude Code docs (interactive-mode,
skills, sub-agents).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(framework): point the instruction-file step at AGENTS.md

Use the framework's own concise AGENTS.md as the worked example for a
trimmed instruction file, with its real rules (answer first, no
narration), instead of a generic snippet.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(aidd-context): apply analyses a recipe and asks before acting

A recipe is often a human tutorial, not an agent-runnable procedure. So
apply now reads the recipe, classifies each step agent-doable vs
human-only, and asks the user what to do before any change — then runs
the chosen agent-doable steps and reports the human-only ones.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(framework): add five verified token levers to the recipe

From a research pass, all verified against official Claude Code docs:

- Plan mode (Shift+Tab ×2) — avoid execution-phase rework.
- `/clear` between tasks — drop stale context instead of re-billing it.
- Deny reads (`permissions.deny`) — keep vendor/dist/secrets out of
  context; the official answer to the unshipped `.claudeignore`.
- Subagent offload — run high-volume ops in an isolated context.
- Protect cache hits — avoid mid-task model/MCP/effort switches that
  invalidate the cache (closes the loop on "cache reads dominate").

Also fold the built-in `outputStyle: concise` into the "talk less" step.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(aidd-context): bundle cook recipes in skill

* feat(framework): publish AIDDy v2 pet

* docs(framework): reposition readme as agnostic token-optimized framework

* feat: avoid uncached AGENTS.md

* docs: add fixing behavior for assertions

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(aidd-context): restructure 10-learn into a 5-action router (#455)

Splits source out of gather so origin selection (conversation, file,
diff, review) is explicit before extraction runs, and renames
assets/decision-template.md to adr-template.md to match its actual
output. CATALOG.md's 10-learn index is regenerated to match.

Folds in fixes found during a headless claude+codex audit of the
skill (13 scenarios x 2 tools):
- sync locates update_memory.js instead of assuming a fixed path that
  doesn't exist in either tool's real install layout
- never scaffold the memory bank, even under a blanket write
  pre-approval
- sync's own git-diff-cached test reflects a pre-staged index instead
  of demanding it be empty
- sources.md again defaults to conversation when no hint is given,
  matching pre-refactor behavior, instead of always requiring context
  inference
- the two memory-write transversal rules merge into one, trimming the
  router closer to the smallest shape that still states every rule

Closes #278
Refs #419

* ci(deps): bump github/codeql-action/analyze from 4.37.0 to 4.37.1 (#456)

Bumps [github/codeql-action/analyze](https://github.com/github/codeql-action) from 4.37.0 to 4.37.1.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](github/codeql-action@99df26d...7188fc3)

---
updated-dependencies:
- dependency-name: github/codeql-action/analyze
  dependency-version: 4.37.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* ci(deps): bump github/codeql-action/autobuild from 4.37.0 to 4.37.1 (#460)

Bumps [github/codeql-action/autobuild](https://github.com/github/codeql-action) from 4.37.0 to 4.37.1.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](github/codeql-action@99df26d...7188fc3)

---
updated-dependencies:
- dependency-name: github/codeql-action/autobuild
  dependency-version: 4.37.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>

* fix(cli): migrate aidd-cli into framework as cli/, full history preserved

Merges ai-driven-dev/aidd-cli into this repo as cli/ via git subtree —
full commit history preserved (reachable via the subtree-merge commit's
second parent, kept on the source branch chore/migrate-cli-into-framework,
not deleted by this squash). cli/ stays self-contained: own package.json,
own lockfile, not a pnpm workspace member.

- release-please: cli added as a ninth package, release-type: node
- CI: new cli-ci.yml (typecheck/lint/test/build/knip/jscpd), native
  path-filter on cli/** only
- publish-cli in ci.yml, gated on release-please's paths_released,
  real npm OIDC trusted publishing (no NPM_TOKEN secret)
- Fixed along the way: npm README stub bug, a pnpm-version/cache-path
  CI bug, a stale duplicate memory-sync script and its dead test,
  hardcoded personal paths, dead ISSUE_TEMPLATE/CONTRIBUTING.md left
  over from the standalone repo
- ai-driven-dev/aidd-cli stays active, unarchived, until a real release
  from here is proven end to end

Refs #448. Full plan, phase files, and audit reports:
cli/aidd_docs/tasks/2026_07/2026_07_20_migrate-cli-into-framework/

* fix(cli): repoint self-references from aidd-cli to framework

package.json never had repository/homepage/bugs fields at all (the
migration plan assumed they existed and needed updating — they didn't
exist). Added them pointing at ai-driven-dev/framework with
directory: "cli" so npm's package page links to the right subtree.

aidd_docs/README.md (the standard scaffold every aidd setup generates)
had one stale link to the old standalone repo.

* fix(cli): add license and keywords for the npm package page

package.json was missing license (npm shows this prominently on the
package page) and keywords (npm search discoverability). license: MIT
matches framework's own root LICENSE — same repo, same license, no
ambiguity to resolve.

* chore: release main (#485)

Co-authored-by: aidd-bot[bot] <290648294+aidd-bot[bot]@users.noreply.github.com>

* fix(cli): restore correct version after release-please regression (#488)

release-please's PR #485 computed cli's next version as 4.0.0 instead
of bumping forward from 5.1.3. Root cause: cli had no prior cli-v*
tag to anchor its version walk, so it fell through to an old,
otherwise-already-consumed root Release-As: 4.0.0 directive (commit
5b0fc9d, framework's own major-bump forcing from May 2026, unrelated
to cli) and applied it to the new component.

Both npm and GitHub Packages correctly rejected the resulting publish
(existing 4.0.0 from the pre-migration standalone repo, real current
version is 5.1.3 on both registries — untouched). Bad tag/release
cli-v4.0.0 already deleted.

Reverts cli/package.json and the manifest's cli entry to 5.1.3, and
drops the bogus CHANGELOG entry. A cli-v5.1.3 tag/release will be
created on top of this commit to anchor future release-please runs
so the walk never reaches that old directive again.

* chore: release main (#489)

Co-authored-by: aidd-bot[bot] <290648294+aidd-bot[bot]@users.noreply.github.com>

* fix(cli): restore correct version again after second regression, re-anchor for release-please (#493)

A second, unrelated release-please cycle (PR #489, "chore: release
main") auto-merged moments after #488 and independently recomputed
cli's version, reintroducing 4.0.0 the same way #485 did, even
though that run's own log showed it correctly reading 5.1.3 from the
manifest as the baseline. The manifest baseline alone does not bound
the version-bump walk; only a matching cli-v* release does, and none
existed yet when that run started.

Both npm and GitHub Packages rejected the resulting publish again
(pre-existing 4.0.0, same as before), still untouched at the real
5.1.3. Bad tag/release cli-v4.0.0 deleted again.

A cli-v5.1.3 anchor release now exists (pointing at bdd12c6, the
commit right before the cli migration landed), created before this
commit merges, so the next release-please run has a real boundary to
stop its walk at and can no longer reach the old, already-consumed
version-forcing directive from commit 5b0fc9d (framework's own
unrelated major-bump forcing from May 2026, targeting 4.0.0 for the
root package only, not cli).

This commit also serves as the nudge for release-please to
re-evaluate cli against that new anchor and correctly propose 5.1.4
next, carrying the real pending fixes, migration, repoint, license
and keywords, into a real changelog entry and npm and GHP publish.

* chore: release main (#494)

Co-authored-by: aidd-bot[bot] <290648294+aidd-bot[bot]@users.noreply.github.com>

* fix(release-please): exclude manifest file from root path tracking (#497)

Any commit touching .release-please-manifest.json (a root-level
file) counts toward the "." package's own version bump, since root
has no path restriction. Today's cli version-regression fixes needed
to hand-edit that file directly and, as a side effect, cut two extra
framework releases (v5.5.3, v5.5.4) that carried no real content
change for root.

exclude-paths (documented in release-please's manifest schema,
ReleaserConfigOptions) skips a commit for a given package when every
file it touches falls under the listed paths. Scoped to the "."
package only, so cli's and the plugins' own path-based tracking are
unaffected.

* chore: release main (#498)

Co-authored-by: aidd-bot[bot] <290648294+aidd-bot[bot]@users.noreply.github.com>

* fix(docs): resolve real broken markdown links, drop temporary link-check excludes (#500)

Two real, distinct issues were hiding behind the migration's temporary
--ignore flags:

- 8 links in plugin-architecture-master.md pointed at real, existing
  sibling files whose names contain a literal '#' (e.g.
  2026_04_27-#260-plugin-architecture-part-1.md). Markdown link
  parsers treat an unescaped '#' as a URL fragment separator,
  truncating the path before the file check ever runs. URL-encoding
  it as %23 fixes it; the checker already decodeURI()s targets.
- The remaining ~235 flagged references under cli/tests/fixtures/**
  and cli/aidd_docs/tasks/** are not a mix of real links and
  shorthand: cli/tests/fixtures/** is synthetic mock trees that never
  materialize every file they reference (confirmed: the full test
  suite passes with those template files absent), and
  cli/aidd_docs/tasks/** is a historical record whose @path
  references and inline rewrite-rule examples are expected to drift
  as the codebase evolves after the fact. Scrubbing them file-by-file
  would just rewrite historical docs that were accurate when written.

Moved that exemption into the checker itself (DEFAULT_IGNORES,
applied on top of any --ignore given) rather than the hook command,
so `node scripts/check-markdown-links.js` with no flags is the thing
that's actually clean. lefthook.yml's markdown-links command now
carries no --ignore flags and no TEMPORARY comment.

* chore: release main (#501)

Co-authored-by: aidd-bot[bot] <290648294+aidd-bot[bot]@users.noreply.github.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Alex <8973343+alexsoyes@users.noreply.github.com>
Co-authored-by: aidd-bot[bot] <290648294+aidd-bot[bot]@users.noreply.github.com>
Co-authored-by: Baptiste LAFOURCADE <baptiste.lafourcade@gmail.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Baptiste LAFOURCADE <119650761+blafourcade@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [github/codeql-action/init](https://github.com/github/codeql-action) from 4.37.0 to 4.37.3.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](github/codeql-action@99df26d...e4fba86)

---
updated-dependencies:
- dependency-name: github/codeql-action/init
  dependency-version: 4.37.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [actions/checkout](https://github.com/actions/checkout) from 7.0.0 to 7.0.1.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](actions/checkout@9c091bb...3d3c42e)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 7.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 6.4.0 to 7.0.0.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](actions/setup-node@48b55a0...8207627)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 24.10.15 to 26.1.1.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 26.1.1
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…478)

Bumps [@commitlint/cli](https://github.com/conventional-changelog/commitlint/tree/HEAD/@commitlint/cli) from 19.8.1 to 21.2.1.
- [Release notes](https://github.com/conventional-changelog/commitlint/releases)
- [Changelog](https://github.com/conventional-changelog/commitlint/blob/master/@commitlint/cli/CHANGELOG.md)
- [Commits](https://github.com/conventional-changelog/commitlint/commits/v21.2.1/@commitlint/cli)

---
updated-dependencies:
- dependency-name: "@commitlint/cli"
  dependency-version: 21.2.1
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [commander](https://github.com/tj/commander.js) from 12.1.0 to 15.0.0.
- [Release notes](https://github.com/tj/commander.js/releases)
- [Changelog](https://github.com/tj/commander.js/blob/master/CHANGELOG.md)
- [Commits](tj/commander.js@v12.1.0...v15.0.0)

---
updated-dependencies:
- dependency-name: commander
  dependency-version: 15.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [typescript](https://github.com/microsoft/TypeScript) from 5.9.3 to 7.0.2.
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Commits](https://github.com/microsoft/TypeScript/commits)

---
updated-dependencies:
- dependency-name: typescript
  dependency-version: 7.0.2
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
…egistry (#514)

* fix(cli): derive framework build's SUPPORTED_TARGETS from the build registry

framework.ts hand-copied a 5-name target list to validate --target, separate
from FRAMEWORK_BUILD_REGISTRY (deps.ts) — the real target:mode routing
source of truth already used one command down (createFrameworkBuildUseCase
returning undefined for an unknown combo). No drift exists today (both list
the same 5 targets), but nothing previously prevented one on the next
target/mode addition or removal.

deps.ts now exports SUPPORTED_BUILD_TARGETS, derived once from
FRAMEWORK_BUILD_REGISTRY's keys. framework.ts imports it instead of
maintaining its own copy.

Pure refactor, zero behavior change. 2049/2049 tests pass, tsc clean.

* fix(cli): move build target/mode list to domain, out of infrastructure

Review feedback on the prior commit: framework.ts (application/commands)
importing SUPPORTED_BUILD_TARGETS from infrastructure/deps.ts was backwards
— "which target/mode pairs exist" is a pure data fact, not something
requiring adapters or DI, so it belongs in domain and application should
depend inward on it, not sideways through infrastructure.

FRAMEWORK_BUILD_TARGET_MODES (+ derived SUPPORTED_BUILD_TARGETS) now lives
in domain/models/framework-build.ts. deps.ts's FRAMEWORK_BUILD_REGISTRY
keeps its own literal keys unchanged (the build-strategy wiring is
legitimately infrastructure's job) — a new test
(tests/infrastructure/framework-build-registry.unit.test.ts) instead
asserts the registry's runtime behavior matches the domain list exactly,
both directions, closing the drift risk between the two independently
-authored sources.

Confirmed via full grep audit: no other file under domain/ imports from
application/ or infrastructure/ anywhere in the codebase.

2059/2059 tests pass (2049 + 10 new), tsc clean.
blafourcade and others added 26 commits September 5, 2026 00:06
cli / Windows failed with every test passing - 39 files, 279 tests, 0
failures, exit 1 - on `[vitest-worker]: Timeout calling "onTaskUpdate"`.
Measured on five runs between 2026-09-02 and 2026-09-04, including three
plain merges to next and a re-run of the same commit, so it is neither a
branch nor a one-off.

Vitest transforms modules on its own main thread while every worker calls
back into it after each test, and that call has a fixed 60 s timeout no
configuration key exposes. On a 4 vCPU Windows runner whose e2e tests each
spawn node dist/cli.js, the default pool queued that thread past 60 s.

--max-workers=2 on that step alone. The Linux jobs keep the default: they
have never produced this.

The workflow also now lists itself in its own path filters. It did not, so
this fix's first push ran every workflow but the one it changes, and merging
it would have run none either - a change to how the suite runs, landing
unverified.

A red check that names no failing test is worse than a red check.


Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp

Co-authored-by: reference-week <reference-week@example.invalid>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both closers compared the two names with `===`. skill-detection.cjs has two
capture routes: Claude Code and Copilot write `aidd-dev:01-plan`, Cursor and
Codex write the bare directory name `01-plan`. The end is captured by neither
- it is read out of the text a skill echoes, and a skill always names itself
in full. So on Cursor and Codex a step opened as `01-plan` met an end named
`aidd-dev:01-plan`, matched nothing, and ran on as if the skill had never
said it was done.

One predicate now answers it for the step axis and the flow axis: equal
outright, or equal once a `plugin:` prefix comes off, and only when one side
carries no plugin at all. Two qualified names that disagree stay two skills.

The cost is stated where the function lives: an unqualified `01-plan` closes
whichever `01-plan` is open, whatever plugin it came from. The host threw the
plugin away before this code saw the line.


Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp

Co-authored-by: reference-week <reference-week@example.invalid>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
`by_flow` decided a record's flow from journal intervals and nothing else. A
record whose own transcript states the orchestrating skill it ran under was
reported as belonging to no flow, however plainly it said so.

Measured on this machine's sink, 30 days, 30,222 requests: `by_step` names
`aidd-orchestrator:01-sdlc` on 2,220 requests as `tool-stated`, and `by_flow`
placed 2,208 of them outside every flow. They come from a session whose journal
holds six `step_end` lines and no `step_start` at all — the shape a session
resumed after its context was compacted leaves behind, since nothing is invoked
again and no hook fires, while the transcript goes on stating the step on every
record it produces.

`flowKeyOf` gains one fallback: a record no interval covers, whose own
`step_attribution` is `tool-stated` and whose `step` names an orchestrating
skill, joins a row for that skill. The row is a distinct kind, keyed on the
name, carrying `attribution: "tool-stated"` and no `started_at` — a name is not
a run, so it can neither tell two runs apart nor claim a single opening moment.

An interval still wins where a record falls inside one, and the reason is
granularity, not strength: `withStepBackfill` prefers a tool-stated step over a
journal-interval one because there the question is *which skill*; here it is
*which run*, and only an interval can say. Twelve records show it in the real
data — tool-stated and inside the interval, they stay on the interval's row.

`by_flow` still reconciles: the unnamed row drops by exactly what the new row
gains. No envelope bump — the flow row gains a field a consumer may ignore, and
no existing field changes meaning.


Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp

Co-authored-by: reference-week <reference-week@example.invalid>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Four places called `by_prompt` "the one breakdown that is complete by
construction", three of them backed by one measurement: 1073 of 1073 records of
one real session carrying a `prompt_id`.

Measured the following day on this machine's own sink, 30,714 records: 845 carry
none, 2.75%, and every one of the nine sessions has at least one. Of those 845,
34 were written before the CLI stamped a version, 810 by one session's reader
before this resolution shipped, and exactly one by the current reader — an
assistant line whose `parentUuid` chain reaches no line naming a prompt. The
original measurement is also no longer reproducible: that transcript holds 230
assistant lines today where 1,073 records were read from it.

What is true is that `by_prompt` is the one breakdown no host limit can empty —
it waits on no journal, no identity file, no declaration, no host naming a
skill. That is not the same as complete, and the difference has a mechanism.

`storeNewCandidates` treats a candidate whose turn is already stored as a
correction only when it carries a larger counter. A field the stored record
lacks is not a correction, so a reader that later learns to resolve something an
earlier one could not names nothing already stored. A record's field set is
fixed the first time its turn is seen. Now stated where that code lives.

Left as it is, with the arithmetic: of the 811 measured against the transcripts
on disk, 720 name a request no transcript still holds and 90 would resolve
today. Roughly 90 records in 30,714 is the whole prize, and enriching would mean
appending a line whose counters equal one already stored, which
`collapseSupersededTurns` picks between by counter weight and then by serialized
content — a preference it would have to learn, for 0.3%.

Prose only. No behaviour moves, no envelope field, no version. The behaviour was
already guarded, correctly, in both directions.


Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp

Co-authored-by: reference-week <reference-week@example.invalid>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
`agentKeyOf` read `agent_name === undefined` as the main thread, whatever the
tool. Only Claude Code's reader ever sets that field — the Codex, Copilot and
OpenCode readers set it nowhere — so on those three tools every record was
reported as the main thread. Not a small error: 100% of the axis, asserted on no
evidence. The contract said so out loud and called it correct.

`claude-code-transcript.ts` already states the right rule twelve lines below,
for `step`: its absence yields no step at all rather than asserting no skill ran.

A record with no agent name now joins one of two rows, and only the tool's own
declaration says which: `main-thread` is a tool that names agents saying this
record belongs to none of them, `not-stated` is a tool whose route never names
one. `TelemetryRouteSupply` gains `agentName`, declared beside `toolStatedStep`
and checked against real captures by the same honesty guard — a route claiming
an agent name its reader never sets fails there. The declaration is read rather
than the record because the record cannot carry the absence: a tool that never
names an agent writes exactly what a main-thread line writes.

Still unfixed, and stated on the row instead: a line marked as a subagent's that
carries no agent name reads as the main thread. Measured across 1,852
transcripts, 157 of 122,637 subagent lines, 0.07%. Closing it needs a field no
already-stored record could ever gain.

`accumulate`'s per-record parameters became a `RecordContext` — nine positional
arguments in a fixed order is a call nobody checks by eye, and the linter's own
function-length rule made the point at the ninth.

Bundle budget raised 593 -> 596 KB, measured 593.8, the same 2.2 KB headroom the
raise before it left.


Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp

Co-authored-by: reference-week <reference-week@example.invalid>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Four cases in `smoke-tools.sh` damaged a file and proved nothing about it.

They did not know which file they damaged. Four places picked one with
`find … | head -1`, and `find` answers in directory order — not sorted, not the
same on two filesystems — so each run corrupted whichever file came back first.

And the three restore cases never checked the repair: each appended drift, ran
`restore --force`, and asserted exit 0. A `restore --force` that exits 0 having
restored nothing is precisely the failure #762 fixed inside the command, so the
case meant to cover it would have passed throughout. The `ide` case was worse:
its drift was a bare newline, which `grep` cannot see, so no check on that file
was possible at all.

`first_file` (`LC_ALL=C sort | head -1`) replaces every unsorted pick. One named
marker, `SMOKE_DRIFT`, is what the three cases append, and `repaired` fails when
the mark is still there after the restore, or when nothing was drifted to repair.

The harness is not run by CI, so the three guards read its text, the way
`smoke-harness-isolation.unit.test.ts` already guards the home sandbox. Each was
written first and failed for the reason it names; `bash -n` and
`shellcheck -S error` are both clean.


Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp

Co-authored-by: reference-week <reference-week@example.invalid>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
`architecture.md` has stated the rule since the per-tool distributions landed —
a skill never links outside itself, because the tree ships both flat and as a
marketplace and no relative path survives both — and nothing enforced it.

The one checker that looks at links cannot: `check-markdown-links.js` resolves
every path against this repository, where the target does exist. A link reaching
out of a skill is green there and dead in every installed copy.

Five had accumulated. Three point at `aidd_docs/product/cost-report-contract.md`,
which no plugin ships at all, so a reader of an installed `aidd-telemetry` was
sent to a file that was never in the package. A fourth pointed at the plugin's
own README, which is no more reachable than the repository once a tool has
installed the skill somewhere of its own choosing — the boundary is the skill,
not the plugin.

Each becomes a name in prose: a reader can search for a name, and a name cannot
rot into a broken link.

The guard reads every markdown under `plugins/<plugin>/skills/<skill>/`,
resolves each relative link, and fails on one landing outside that directory. It
ships with a second test that puts an escaping link in front of it and requires
it to be seen — a boundary check that never trips reads exactly like a clean
tree, which is the state the existing checker was already reporting.


Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp

Co-authored-by: reference-week <reference-week@example.invalid>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
`MarketplaceSyncSettingsUseCase` wrote `.claude/settings.json`, hashed what it
wrote, saved the manifest, and only then shelled out to `claude plugin
marketplace add` and `claude plugin enable` — which write into that same file.
Claude Code declares no separate `enabledPluginsSettingsPath`, so both halves
land there, and the file is hash-tracked precisely because the two paths
coincide.

Nothing re-read it. The tracked hash described content that stopped existing the
moment activation succeeded: `status` and `doctor` reported a file the person
never touched as drifted for as long as the manifest stood, and `restore` would
have undone the host's own registration to reach a state AIDD held for the
length of one function.

The first probe of this passed, and was vacuous — the fake activator wrote the
same key `mergeEnabledPlugins` already writes, so the content was byte-identical
and the hash matched by coincidence. Writing a key this code never writes, which
is what the host's own bookkeeping is, turned it red.

After activation the settings file is re-read and what is there is stored, for
the tools whose own CLI actually ran. `activateTool` now answers whether it drove
the CLI. Only those tools: a tool whose binary is absent wrote nothing, so a file
that differs for it differs because a person changed it — the drift `status`
exists to report — and re-hashing every installed tool would bless that as ours
and make the change permanent and invisible.


Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp

Co-authored-by: reference-week <reference-week@example.invalid>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
A re-read appends, so one session's lines sit in different orders on two
machines. The report is meant to be blind to that, and the existing check is
four records and a single reversal.

Four records cannot cover what has been added since. `by_flow` now keys an
interval-derived row on a `FlowInterval` object, a tool-stated one on the skill's
name, and the remainder on a symbol; `by_agent` keys two of its three rows on
symbols. Mixed key kinds in one Map are exactly where insertion order leaks into
output, since rows are ranked by size and ties broken on the row's own key —
and nothing failed when either tie-break was broken.

Verified against the real thing before writing it: 30,222 records of a live
sink, shuffled within every day file, produced a byte-identical report to the
unshuffled run. Three consecutive runs were byte-identical, and all eleven
`--axis` artefacts stable.

fast-check over 300 permutations of a fixture carrying every row kind once, plus
a second test asserting the fixture exercises all of them — a permutation of
records that produce one kind of row proves nothing about mixed keys.

The `pickDeterministically` mutation survived the first fixture, which had no two
records sharing a `billed_request_id`, so the function was never reached. Adding
the pair is what made that guard real.


Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp

Co-authored-by: reference-week <reference-week@example.invalid>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(cli): a step outlives the pause inside it

A `turn_end` closed a step interval, so a skill spanning several prompts was
credited with its first turn and nothing after. It is a pause, not the end of a
step, which is the rule `buildTaskIntervals` and `buildFlowIntervals` already
read from this very journal.

Measured on the one orchestrated session captured, 2026-09-04: four steps opened
across four hours of continuous work, every one closed by the next pause, the
last at 06:02:34 against a session that ran until 09:27:21. Of its 1,073
records, 69 fell inside a step interval. The same journal gave the flow axis
1,052 records and this axis 1 — two walks over identical evidence disagreeing by
three orders of magnitude.

`buildStepIntervals` now runs through `buildClosedIntervals`, the shared walk the
other two already use, over the same three-array merge. A `step_end` naming the
step's own skill or the next `step_start` closes an interval; nothing else does.
On a 30-day report holding 30,222 requests, `journal-interval` goes from 2 to
974, and all ten axes still reconcile to the period total.

Two consequences, both stated in code rather than discovered later:

- An unclosed step is capped at the journal's own last witnessed moment instead
  of left open. The premise for leaving it open — that this walk saw no later
  moments to cap at — is false now it reads `filesWritten` and `taskDeclarations`
  like the other two.
- A journal whose only line is the opener witnesses no later moment, so the step
  it opened covers nothing, and `records-join` can read fail where it read ok.
  Found by running it. Failing there is the honest answer: nothing in such a
  journal says the step was still running.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp

* test(cli): pin what capping an unclosed step costs

Two guards for consequences the change states but nothing asserted.

`check` on a journal whose only line is the opener: the step covers nothing, so
a record carrying no step of its own joins nothing and `records-join` reads
fail. That is a real host — Copilot fires no stop event — and `check` is the
command whose job is saying measurement is broken, so the verdict belongs in a
test rather than in a doc comment alone.

A step whose end shares its start's moment covers nothing. The journal stamps
`nowIso()`, whose resolution is the second, so two lines sharing a moment is the
common case; the shared walk orders them by a stable sort, which for two
`boundaries` entries is file order. Pinned because that ordering is inherited
from the sort now rather than written out.

Mutation run: an unclosed step left open again turns both red.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp

---------

Co-authored-by: reference-week <reference-week@example.invalid>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…er reads it (#776)

The hook stamps `schema_version` on every `session_start` it writes
(`record.cjs`'s own `SCHEMA_VERSION`, 2). The reader dropped the field on the
floor: `parseSessionStart` never looked at it, and `RunJournalSessionStart` had
nowhere to put it. So a journal written under a schema whose line shapes had
changed was read as if it were this one — a silent misreading, which is the one
thing a version field exists to prevent.

The reader now carries the stated schema and refuses a journal stating one it
does not read. Version 1 was a mutable record rather than this append-only line
log, so its lines are another shape entirely, and a later version can change any
line the same way; reading either would mean guessing that whatever lines this
parser still recognises mean what they used to.

Absence is not a disagreement. Every journal written before this reader looked
at the field states no schema, and refusing those would drop attribution this
reader has always given — "an unknown is never a zero", applied to the reader
rather than to a figure.

A refusal must not cost the fact that the file is there. Dropped from `list()`
alone, a refused journal leaves `check` to fall through to a branch that is
false about it: "the recorder is declared nowhere" claims no file exists, and
"none carry a readable session_start" blames a torn write for a version
disagreement. `RunJournalReader.listForeignSchemas` carries the stated versions
across, and `hook-fired` gains `journal-in-another-schema`, read ahead of the
anchorless one — a build that cannot read a journal's schema cannot tell a
missing `session_start` from one shaped differently.

Mutations run, all killed: the reader no longer refusing, absence read as a
disagreement, and the new reason ordered after the anchorless one.

Bundle budget 596 -> 598 KB, measured 594.3 -> 595.8, recorded beside the three
raises before it.


Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp

Co-authored-by: reference-week <reference-week@example.invalid>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* ci: install under pnpm 12, which dropped --frozen-lockfile=false

`validate.yml` activates `pnpm@latest`, and pnpm 12.3.4 landed on the runners
today. It removed the `=false` value form of `--frozen-lockfile`, so the lefthook
job stopped at its own install step:

  error: unexpected value 'false' for '--frozen-lockfile' found; no more were expected
  Usage: pnpm install --frozen-lockfile

Every pull request fails that check from now on, whatever it changes: the job
never reaches the hooks it exists to run. The three merged today passed it hours
earlier, under pnpm 11.

`--no-frozen-lockfile` is the same instruction in the form both majors accept —
pnpm 11 documents the flag as `--[no-]frozen-lockfile`, and pnpm 12's own error
says the flag takes no value.

Not fixed here, and worth a decision: nine other workflow steps also activate
`pnpm@latest`, so the next major arrives the same way, unannounced. Pinning the
version is a choice about what the team runs locally, not a CI detail, so it is
named rather than made.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp

* ci: one pinned pnpm for the whole tree, and the overrides pnpm 12 stopped reading

`corepack prepare pnpm@latest --activate` ran in ten workflow steps, so the version
CI used was whatever pnpm had published that morning. `cli/` and `kanban/` pinned
`pnpm@10.14.0` through `packageManager` and the repository root pinned nothing at
all, so one run could resolve the root under 12 and `cli/` under 10.

`packageManager` now names `pnpm@12.3.4` in all three, and every workflow step
stops at `corepack enable` — corepack reads the pin rather than choosing. One
version, written down once, and the next major arrives when somebody edits that
line.

pnpm 12 stopped reading `package.json`'s own `pnpm` field, which is where `cli/`
kept four security floors:

  [WARN] The "pnpm" field in package.json is no longer read by pnpm.
         The following keys were ignored: "pnpm.overrides".

Ignored there while the lockfile still recorded them, which is the
ERR_PNPM_LOCKFILE_CONFIG_MISMATCH every `cli` install answered with under 12. They
move to `cli/pnpm-workspace.yaml`, their new home, unchanged: `fast-uri >=3.1.2`,
`picomatch >=4.0.4`, `postcss >=8.5.10`, `qs >=6.15.2`.

Each lockfile gains 101 lines and loses none — the `packageManagerDependencies`
block pnpm 12 records for the pinned version. Measured, all three: no dependency
re-resolved, nothing removed. A pin cannot be added without it, since
`--frozen-lockfile` refuses to write that block itself.

Verified by running the thing rather than reading about it. Every package,
`pnpm@12.3.4`, `--frozen-lockfile`:

  root    exit 0, lockfile stable
  cli     exit 0, lockfile stable
  kanban  exit 0, lockfile stable

3,471 CLI tests, 371 repository script tests, biome `ci` clean (2 pre-existing
warnings), typecheck clean, bundle 595.7 / 598 KB, 0 broken links in 798 files.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp

* fix(ci): name the builds pnpm 12 refuses to run unasked

Pinning the version was not enough. pnpm turns an unapproved install script into
`ERR_PNPM_IGNORED_BUILDS`, and under 12 every `cli` and `kanban` job stopped there
— eleven checks red on a change that touches no source at all.

The repository root already named `lefthook`. `cli/` and `kanban/` named nothing,
because under 10.14.0 the same builds went through.

Named from what pnpm itself printed, never guessed:

  cli     Ignored build scripts: esbuild@0.21.5, esbuild@0.27.3, lefthook@2.1.12
  kanban  Ignored build scripts: esbuild@0.28.1

Verified after, `pnpm@12.3.4` with `--frozen-lockfile` and scripts enabled, each
package in its own clean directory: root, `cli/` and `kanban/` all exit 0 with the
lockfile unchanged.

One measurement to distrust and the reason it is not here: `cli/` first failed with
`ERR_PNPM_EXECUTOR_LIFECYCLE_SCRIPT_FAILED` in a bare directory, which is
`lefthook install` finding no `.git` — the sandbox's fault, not the tree's. Run
again in a directory with one, it passes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp

* fix(ci): stop discarding the file that allows kanban's builds

The lefthook hook installed kanban with `--ignore-workspace`, added so pnpm would
not resolve that folder to the repository root, find it lists no members, report
"Already up to date" and install nothing.

`kanban/pnpm-workspace.yaml` now stops that upward search by itself — the whole
reason the file exists, as its own comment says — and the flag had turned harmful:
it discards that same file, and with it the `allowBuilds` entry the commit before
this one added. So the one job that runs the hooks failed on the build pnpm 12
refuses to run unasked, while the standalone kanban job passed.

Measured in the real layout rather than reasoned about: a root workspace above,
kanban's own file present, `pnpm@12.3.4`, `--frozen-lockfile`.

  without --ignore-workspace   exit 0
  with    --ignore-workspace   exit 1, ERR_PNPM_IGNORED_BUILDS

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp

* fix(scripts): a YAML file may hold more than one document

`validate-yaml.mjs` called `load`, which refuses a stream carrying several
documents: "expected a single document in the stream, but found more". pnpm 12
writes exactly that shape — a lockfile whose first document carries
`packageManagerDependencies` and whose second carries the lockfile itself — so the
hook reported all three regenerated lockfiles as broken YAML. They are not: a
multi-document stream is valid YAML, and `load` was the wrong function for a check
whose whole job is syntax.

`loadAll` accepts them and still refuses what is actually malformed — verified both
ways: the three lockfiles pass, and `a: [1,` still fails on its own indentation.

Found only in CI, and the reason is worth writing down: run with no arguments, this
script validates nothing and prints "passed for 0 file(s)". The hook hands it the
staged files; a local run without them is green whatever the tree holds, which is
how three broken-looking lockfiles reached a pull request after a local check said
yes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp

---------

Co-authored-by: reference-week <reference-week@example.invalid>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ed it (#777)

* fix(cli): an invoked step does not close the orchestration that invoked it

Reading every `step_start` as the end of whatever was open assumes a session only
ever runs one skill after another. An orchestrating skill's whole job is to
invoke others, so that assumption cuts it short at its first child.

Measured on the one orchestrated session captured, 2026-09-04:
`aidd-orchestrator:01-sdlc` opened at 05:56:27 and `aidd-pm:04-spec` at 05:59:53,
so the orchestration read as 206 seconds against a session that ran until
09:27:21. `by_flow`, reading the same journal under the rule this adopts, named
1,052 records for that skill while `by_step` named 1.

`buildStepIntervals` is now two walks over the same lines. The orchestrating half
*is* `buildFlowIntervals` — a flow is an orchestrating step, and calling it
rather than restating it is what keeps the two axes from drifting apart again.
The other half opens on a non-orchestrating `step_start` and keeps the rule it
always had: any later `step_start` closes it, because a session that starts
orchestrating is no longer running the plain skill it was running before.

`attributeMoment` now answers with the innermost interval around a moment rather
than the first one in the array. Both claims are true of a record inside an
invoked step; the inner one says more, and the outer one goes on answering for
every moment the inner one does not cover. Array order decides nothing, which
matters now that two separate walks build the list.

Which skills orchestrate is `ORCHESTRATING_SKILLS`'s declaration, never inferred:
nesting and sequence produce the identical journal, so no rule read off the
boundaries alone separates them. The limit is stated in code — a skill that
invokes another without being declared an orchestrator is still cut short by its
own child.

**Measured effect on today's data: none.** `by_step` reads
`aidd-dev:01-plan=972, aidd-orchestrator:01-sdlc=1` before and after. The tail of
that session is owned by `aidd-dev:01-plan`, an invoked step nothing closed,
which runs to the journal's last witnessed moment and wins on having started
later. Telling an interval closed by evidence from one merely bounded is the next
change, and this two-walk shape is what it needs to fall back to.

Mutations run, both killed: the outermost interval winning, and a single walk
opening on every `step_start`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp

* fix(cli): a step nothing closed yields to the one that encloses it

An interval nothing ever closed ends at the journal's own last witnessed moment.
That end is a bound, not a measurement, and reading it as an extent is how a step
that opened shortly before a long session went on working comes to be credited
with all of it — purely for having opened later than the orchestration around it.

Measured on the one orchestrated session captured, 2026-09-04:
`aidd-dev:01-plan` opened at 06:00:50 inside an orchestration opened at 05:56:27.
Neither was ever closed, so both ran to 09:27:21 and the innermost-start rule gave
the invoked step every record in between.

`buildClosedIntervals` now tells its caller which of the two ended an interval —
a moment the journal witnessed, or the cap standing in for one it never did — and
`answersFor` reads it: an interval closed by the journal's end yields to one that
encloses it and was never closed either.

Two values and not three. "Closed by its own `step_end`" and "closed by a later
`step_start`" are different strengths of evidence and both were weighed; both are
nevertheless a moment the journal witnessed, and no caller separates them, so a
third value would be structure nothing reads.

The yielding is that narrow and no wider. Where the enclosing interval states its
own end, the inner one runs past it, nothing encloses it, and the innermost claim
stands — the same answer it gets when both ends are witnessed. An unclosed
interval nothing encloses still answers for its own moments: what is refused is
preferring a bound over a wider claim covering the same moment, never the bound.

No tie between two unclosed sibling steps can arise, and it is not this rule that
prevents it: any `step_start` closes whichever plain step was open, so at most one
invoked step is ever left unclosed at a time. The test says so by demonstration
rather than a comment.

Containment between two unclosed intervals is compared on the start alone. They
all end at the same cap, so comparing the ends would be a clause no input can
make false — the kind of default that hides a wrong number.

Measured on the real sink, 30 days, 31,025 requests, every axis reconciling:

  by_step  aidd-orchestrator:01-sdlc  journal-interval    1 -> 973
  by_step  aidd-dev:01-plan           journal-interval  972 -> 0

973 is the whole of what the two walks can agree on. 1,052 records fall inside
that flow interval; 79 of them carry a step their own tool stated (12
`aidd-orchestrator:01-sdlc`, 61 `aidd-dev:01-plan`, 6 `aidd-pm:04-spec`) and keep
that stronger claim on the step axis. 1,052 - 79 = 973, and `by_flow` reads 1,052
for the same interval.

This moves attribution within the 1,052 records the journal covers. It does not
move the 27,393 unattributed ones: those predate the journal, from one session
resumed since 2026-08-13, and no interval rule reaches them.

Mutations run, three killed by three distinct guards: never yielding, yielding to
any earlier start, and letting a closed interval yield too.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp

---------

Co-authored-by: reference-week <reference-week@example.invalid>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
)

The `by_backlog` axis rests on one file per task folder, `backlog-link.json`, and
nothing checked that the file was readable. Of the three this repository held, two
carried `writtenAt` and `writtenBy` while `task-backlog-adapter.ts` reads
`written_at` and `written_by`, so those two parsed as nothing at all.

Measured on the real sink before the fix, 30 days:

  {"backlog":"#694"}   4
  {"declaration":"unreadable"}              130
  {"declaration":"none"}                   2918

and after it, the same window and the same records:

  {"backlog":"#746"} 133
  {"backlog":"#694"}   4
  {"declaration":"none"}                   2918

The axis was never broken — it reported `unreadable` truthfully, on its own row,
exactly as `TaskBacklogDeclaration` requires of a file it cannot parse. What was
missing is anything that reads that row before a person does.

Both wrong files were written by `aidd-orchestrator:01-sdlc`, which
`01-frame.md` tells to carry the resolved ticket so that "whichever of Spec or
Plan first creates the delivery folder can declare it there". It wrote the file
itself instead, and took the field names from the TypeScript interface rather
than from what either skill teaches. The guard, not a fourth copy of the JSON
block, is the durable answer: a third skill inventing a fourth spelling now fails
here rather than in a report nobody reads.

The guard restates the reader's rule, because the reader is a `cli/` module and
this is a repository script test; the second case is what keeps that restatement
honest, asserting the same three names are the ones `aidd-pm:04-spec` and
`aidd-dev:01-plan` actually teach.

Mutations run, both killed: a declaration put back in camelCase, and a skill that
stops teaching one of the three fields.

373 repository script tests pass, 0 broken links in 798 files.


Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp

Co-authored-by: reference-week <reference-week@example.invalid>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…cript crossing it (#778)

* docs: state the line between a hook and the CLI, and guard the docs that name files

The telemetry pivot moved 4,355 lines of skill-owned scripts into the CLI on an
argument nobody wrote down. What is left outside is one skill script and the
bundled hooks, and the reason the hooks are not next is a measurement, not taste:
on one machine, 12 runs each, `hooks/journal.cjs` starts in a median 27 ms and
`cli/dist/cli.js` in 180 ms, and `PostToolUse` fires on every tool call a session
makes. A thousand tool calls is 153 seconds a person waits through.

So the line is not "plugin or CLI". It is what the code answers to: observing runs
on a tool event thousands of times a session and must not be felt, so it is plain
Node with no install behind it; answering runs once because a person or a skill
asked, so latency is nothing and it belongs in `aidd`. The section states both
consequences already paid for — a capability that answers moves even when a plugin
is what asks for it, and a skill needing an absent CLI must say so out loud — and
names the guard that already pins the second.

It also states the cost rather than arguing it away: a plugin that promised "no npm
install, no CLI, no account" now needs `node` to measure and `aidd` to answer.

The doc named `hooks/update_memory.cjs`. That file has always been `.js`, in the
tree and in `hooks.json`, so a reader following the architecture doc to the context
plugin's session hook found nothing.

`comments-name-files-that-exist.test.js` existed to catch exactly that and could
not see it: it scanned `cli/src`, `cli/tests`, `plugins` and `scripts` for `.ts`,
`.cjs` and `.js`, and no markdown at all. It now scans `docs/` markdown as well —
extended rather than paired with a second test, since a durable doc naming a file
makes the same promise a comment does.

Markdown anywhere else stays out, measured rather than assumed: scanning every
`.md` in the tree produced 17 findings and not one was a fault — a skill's own
asset and a `cli/tests/fixtures` template name illustrative paths on purpose. Of
the three findings in `docs/`, one is this bug and two name `INSTALL.md`, a seam
artefact one plugin writes into a reader's project for another to read back, now
listed in `NOT_A_REPOSITORY_FILE` for what it is.

Red before the fix on the one line it names, green after. 371 repository script
tests pass, the same count as before: the guard was extended, not duplicated.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
AIDD-Session-Id: 2c21d903-3a7e-47ac-83f8-d8b7ae3aa579

* feat(cli): aidd ai rules answers what the last plugin script used to

`plugins/aidd-context/skills/11-explore/scripts/list-rules.mjs` was the only script
left inside a skill, 198 lines, and its own header said it was "synced from
plugins/aidd-dev/scripts/list-rules.mjs. Keep in sync when the source changes" —
a directory that no longer exists. A hand-maintained copy whose source is gone, and
whose header is false.

It had drifted where it mattered. It carried its own table of four tool directories
and their extensions and stated "Codex CLI: rules not supported, skipped".
`plugin-content-translator.ts` routes a plugin's `rules/` into every tool whose
capability accepts them, and all five do, so a Codex project asking what rules it
had was answered "none" — silently, and wrongly. `05-rule-generate`'s own
`tool-paths.md` said the same of Codex and OpenCode; both rows are corrected here.

`RulesCapability.installedLocation()` is what removes the table rather than moving
it. It asks `buildInstallPath` with a sentinel name and reads its answer back, so
the directory and the extension come from the installer itself:

  claude    .claude/rules/          .md
  codex     .codex/rules/           .md
  copilot   .github/instructions/   .instructions.md
  cursor    .cursor/rules/          .mdc
  opencode  .opencode/rules/        .md

Written where the knowledge already is: three of those five are a path template, one
goes through `toMdc`, and Copilot's is a delegated handler. A caller splitting a path
string apart would have been a second copy, free to disagree the day a tool moves.
`registry-conformance` pins all five as a table, so a sixth tool, or a moved
directory, is read by whoever changes it.

The frontmatter parser goes too: `domain/formats/markdown.ts` already had
`parseFrontmatter`, and the script carried a fourth hand-rolled one. `hasRules` was
private to `plugin-content-translator.ts` and is now exported from `contracts.ts`
beside the type it guards, generic so that translator keeps the narrowing it had.

`--json` prints the array the script printed, field for field, so the skill reading
it did not change what it reads. The default output is for a person, and a project
with no rule says so rather than printing nothing: an empty answer and a command
that never ran look identical on a terminal, and only one is a fact.

The skill now runs `aidd ai rules --json` and, finding no command, stops and says the
CLI is required — never an empty inventory, which is the rule
`docs/ARCHITECTURE.md` states for a capability that answers.

Bundle 596.6 -> 599.0 KB, budget 598 -> 601, measured and recorded beside the three
raises before it.

Mutations run, three killed: `installedLocation` answering the source path instead of
the installed one (4 red), the scan dropping its extension filter (1), and a name
trimmed at the last dot rather than by the whole extension (1).

3,488 CLI tests, 371 repository script tests, typecheck, biome `ci`, knip, jscpd,
layering, 0 broken links in 798 files.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
AIDD-Session-Id: 2c21d903-3a7e-47ac-83f8-d8b7ae3aa579

* style(cli): order the imports biome's assist expects

Adding the use case to `deps.ts` and the guard to `plugin-content-translator.ts`
left both import blocks out of the order `assist/source/organizeImports` fixes to,
which `pnpm lint` fails on and `biome ci` alone does not report.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
AIDD-Session-Id: 2c21d903-3a7e-47ac-83f8-d8b7ae3aa579

* fix(scripts): scan docs with its own find, not one joined by a semicolon

Extending `comments-name-files-that-exist.test.js` to `docs/` markdown appended a
second `find` to the same command string, separated by `;`. `execSync` runs through
`cmd.exe` on Windows, where `;` separates nothing: the second `find` and its
arguments were handed to the first as arguments, and the walk came back with a list
that named files the repository does not hold.

Green on macOS, red on the Windows job — which is exactly what that job is there
for, and the reason the split is now two `execSync` calls with no shell operator
between them rather than a quoting fix that would have worked on one platform.

Line endings are split on either form while the calls are being touched: the same
output arrives with `\r\n` on Windows, and a trailing carriage return would have
made every path miss.

371 repository script tests pass.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
AIDD-Session-Id: 2c21d903-3a7e-47ac-83f8-d8b7ae3aa579

---------

Co-authored-by: reference-week <reference-week@example.invalid>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
`prompt-matched` read one source: a `step_start` the run journal wrote with the
prompt's own identifier. A session that ran before the hook was installed has no
such line, so 9 records of 31,225 were named that way.

The transcript holds the same fact. Every `Skill` call sits in a prompt, and the
reader already resolves each record's prompt by walking `parentUuid`. It now also
remembers, per prompt, the skill a `Skill` call started there, and stores it as
`prompt_skill`.

Measured on the real sink, 30 days, before and after:

  prompt-matched      9  ->  4,322
  by_step named   11.5%  ->  25.3%

An observation, never a judgement. Which step a record belongs to is derived fresh
on every report, from this and from the journal together; the journal wins where
both name a skill for the same prompt, since it was written by a hook the host
fired while this is read back afterwards.

It does not duplicate `step`. That one reads `attributionSkill`, which Claude Code
writes per message: exact where it appears and sparse where it does not. Measured
inside the window `aidd-dev:01-plan` demonstrably ran on 2026-09-04, 142 lines
carry counters and 20 carry that field — so its absence is not the tool saying no
skill ran, and this contradicts nothing it states. The doc comment claiming
otherwise is corrected.

Scoped to the transcript the record sits in, which is what the reader accumulates.
Claude Code writes a session's subagents to their own files, and 1,038 of 5,564
prompts on this machine appear in more than one. A subagent that invoked its own
skill did that work under that skill; merging files first would have to pick one
answer for both, and neither is true of both.

Verified against an independent recomputation over every transcript, main files
and subagent trees alike: of 6,215 stamped records, 6,136 match. The 79 that do
not are prompts spanning two files, where the check merged what the reader keeps
separate — the looser reading is the check's, not the code's.

Four cases written before the code, two red at the start. Mutations run, three
killed: the last skill of a prompt winning instead of the first, every `tool_use`
counting rather than only `Skill`, and the record outranking the journal.

3,477 CLI tests, 371 repository script tests, typecheck, biome, knip, jscpd,
layering, bundle 596.6 / 598 KB, 0 broken links in 798 files.


Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp
AIDD-Session-Id: 2c21d903-3a7e-47ac-83f8-d8b7ae3aa579

Co-authored-by: reference-week <reference-week@example.invalid>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
)

`aidd_docs/product/aidd-kanban.md` was written once and never referenced again: no
test asserts anything about it, no source file names it, and no other document
links to it. Its two neighbours in that folder are the opposite — `metrics-contract.md`
is held by three test files and `cost-report-contract.md` by four, which recompute
the figures those documents state and fail when a field is added or removed without
being written down.

A document nothing checks can drift without anyone finding out, and this one
describes a screen that has changed since. Read as current, it misleads; read as
history, it is not marked as such.

The 101 lines go. Nothing else moves: 373 repository script tests, 0 broken links
in 797 files, and the eight cases guarding the two contracts that remain still pass.


Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp

Co-authored-by: reference-week <reference-week@example.invalid>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
OpenCode loads every function-valued named export of a file in `plugin/` as
a plugin factory of its own. `opencode-plugin.js` exported `journalCallFor`
beside `AiddTelemetry` as a spawn-free test seam; OpenCode called it with one
argument, got `null`, and died reading `.auth` off it before any session
started. Measured against opencode 1.14.20 in a freshly installed project:

    export const Stub = async () => ({ event })          -> runs
    + export function helper(){ return null }            -> TypeError: null
                                     is not an object (evaluating 'S.auth')
    + export const helper = { a: 1 }                     -> runs

So installing this framework for OpenCode made the tool it measures
unusable. The seam now rides on the plugin function as a property, leaving
one export, and a test asserts that only `AiddTelemetry` is function-valued.

The same run showed the second half of the fault. OpenCode publishes
`session.created` on its own bus and never delivers it to a plugin's event
hook, and `opencode run` is always such a session, so the journal never
received a `session_start`, never created the run file the rest of the
session appends to, and dropped the `turn-end` and every task declaration
after it. `telemetryLocalRead` declared the tool covered while `aidd
telemetry read`, which reads only sessions the run journal knows, could
never find one. The first call a session produces now opens it, carrying the
directory that call was already going to use — never a new guess. What is
left is only what `session.created` alone could have said, and the README
says it: on a server serving more than one directory, a session it never
announced is journalled under the plugin's own init-time directory.

Verified end to end on a scratch repository, isolated sink: a real
`opencode run` wrote its first journal ever — `session_start`,
`task_declared`, `turn_end` — and `aidd telemetry read` answered
"OpenCode: read (3 new of 3)".


Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp

Co-authored-by: reference-week <reference-week@example.invalid>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two tools declared a limit that was true when it was written and is not any
more. Both are now measured, and the captures that settle them ship as
fixtures.

Copilot. `copilot.ts` said the exclusivity of `input` against `cache_read`
was unconfirmed, because the only capture held `cache_read: 0` — where an
inclusive and an exclusive `input` produce the same number. It named what
would close it: one session with a non-zero `cache_read`. Captured on 1.0.82:

    tokenDetails  input 9 · cache_read 42038 · cache_write 21404 · output 408
    usage.inputTokens                                                   63451

9 + 42038 + 21404 = 63451, and Copilot's own terminal line for that run read
`↑ 63.5k (42.0k cached, 21.4k written)`. An `input` that already counted the
cached prompt would read 63451, not 9. The four counters are disjoint, so the
report is right to add them; it would otherwise have over-counted that
session by 42038 of 63451.

OpenCode. `opencode-export.ts` said no capture ever put a large `cache.read`
beside `input` for a non-Anthropic provider — the one comparison that shows
`input` failing to shrink if it already counted the cached tokens. Captured
from `opencode export --sanitize`, providerID "opencode", modelID
"ling-3.0-flash-fin-free", three billed turns of one session:

    input  28242 → 269 → 196
    read     640 → 28928 → 29184
    total  29089 · 29356 · 29438, each == input + output + reasoning + cache

`input` falls as `cache.read` climbs, and OpenCode's own `total` holds the
identity on all three turns, which it could not if the counters overlapped.

What each tool still cannot say is narrower, and both limitation strings now
say that instead of the old one: for OpenCode, a provider reporting prompt
tokens inclusive of the cached ones has still never been captured here.

Both guards bite. Mutating `copilot-events.ts` to fold `cache_read` into
`input` turns exactly the new case red — `expected 42047 to be 9`.


Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp

Co-authored-by: reference-week <reference-week@example.invalid>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
`aidd marketplace list` died with

    Error: Cannot read properties of undefined (reading 'map')

on a `~/.config/aidd/marketplaces.json` holding `{"version":1}` — a real file,
found while clearing test residue out of a machine's own configuration. The
reader caught a missing file and nothing else: `JSON.parse` could throw, and
`parsed.marketplaces` could be undefined, so a present-but-malformed registry
reached `.map()` and produced a stack trace naming nothing a person can act
on. Invalid JSON gave the same shape of answer, in the parser's words rather
than the tool's.

Reading such a file as an empty registry would be worse than crashing.
`save()` reads this same list, appends to it and writes the whole file back,
so one silent empty read turns a file with five marketplaces into a file with
one. A missing file is a different answer and keeps its own: no file, no
marketplaces, nothing to lose.

So a file that exists but cannot supply the list is refused by name:

    Cannot read the marketplace registry at <path>: it carries no
    `marketplaces` list. Repair the file, or delete it to start from an empty
    registry.

Verified on the built binary across all four states — no file and an empty
`marketplaces: []` both answer "No marketplaces registered."; a missing list
and invalid JSON each name the file and the reason.

The bundle budget rises 601 -> 603 KB, measured 600.7 -> 601.2 KB. The
guard's own comment asks for exactly that: the budget makes growth visible
rather than walling it off, and a raise is what a reviewer sees. This one is
the smallest so far, and it is the raise that showed the budget had 0.3 KB of
headroom left — less than a correctness fix costs. It keeps the same 2.2 KB
headroom as the four raises before it.


Claude-Session: https://claude.ai/code/session_01VWNxk63AGKkqE8HRqHLjGp

Co-authored-by: reference-week <reference-week@example.invalid>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ssion telemetry (#795)

* refactor(cli): split architecture rules into self-contained invariants

`0-hexagonal.md` listed eleven directories, so it had to be rewritten every time
one moved. `0-layer-responsibilities.md` covered four topics at once, and its
"Shared Use Cases" section explicitly allowed promoting a use case as soon as a
second use case called it — the written policy that produced a 14-file `shared/`
directory where only two entries have callers in more than one area.

Both are replaced by one rule per topic, each scoped to the paths it governs and
carrying constraints rather than a map:

- 0-dependency-direction, 0-ports-adapters, 0-use-case, 0-domain-model
- 0-orchestration: an orchestrator depends on entry points, not on parts
- 0-shared-modules: sharing needs callers in two areas, and the test is `grep -rl`

Paths now live only in `aidd_docs/memory/codebase-map.md`, which already carried
a richer map than the deleted rule. Duplicated bullets are gone: ports and
adapters had two homes, and "Methods <= 20 lines" restated 6-method-size.md.

The no-re-export constraint joins `1-exports.md`, its own category, next to the
existing barrel-file rule.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011x4ms5qcGuZgYhCxfdHMUb

* test(cli): add architecture invariant tests and make guardrails blocking

The drift this repo accumulated happened while the rules existed: the `shared/`
policy was written down and followed faithfully into a dumping ground. A rule is
advice; a test is a barrier. Both detectors that could have caught it ran with
`continue-on-error: true`, so they reported and never blocked.

Five tests, in a dedicated `architecture` vitest project, 238ms for the set. They
read source as text and never import the code under test, so they cannot be
broken by wiring and stay cheap enough for pre-commit:

- earned-sharing: a shared module needs callers in two areas (7 known)
- orchestrator-deps: no use case injects more than four use cases (2 known)
- tool-addition-cost: a tool id appears only in its own profile (20 known)
- docs-do-not-lie: every command the docs present as available exists
- codebase-map: every directory under src/ appears in the map

Each carries a frozen baseline that may only shrink: a new violation fails
immediately, and fixing one without updating the list fails too. Verified by
introducing a deliberate violation and removing it again.

Biome gains four native rules — noBarrelFile, noReExportAll, noImportCycles,
noUnresolvedImports — plus an override forbidding the domain from importing
application or infrastructure, verified in both directions. One sanctioned
exception: tests/helpers/** keeps its barrel, imported by 78 test files.

Note for the record: noImportCycles does not flag the two cycles found by hand.
They close through `import type`, so there is no runtime cycle and Biome is right
to stay silent.

knip and jscpd now block. jscpd gets an explicit 3.5% threshold against a current
3.43% (71 clones, 772 duplicated lines of 22507), so any increase fails.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011x4ms5qcGuZgYhCxfdHMUb

* docs(cli): correct three stale claims and record the file ownership regimes

Found by the architecture tests added in the previous commit, which is the point
of them.

`ARCHITECTURE.md` presented `aidd sync` in its command surface. No such command
is declared anywhere, and none ever was — the README documents it as removed.
The line is gone. It also described manifest v6 as carrying `marketplaces`, while
`manifest.ts:142` states the registry moved to `.aidd/marketplaces.json`.

`codebase-map.md` was missing six real directories: display, translator, auth,
git, http, and `use-cases/framework/` with its `strategies/` — 1819 lines, the
largest use-case directory, absent from the map that is supposed to be the single
place describing where things live.

`memory/architecture.md` gains a File Ownership section. Two regimes share the
disk and confusing them is the main source of accidental complexity: files the
CLI generates are gitignored and disposable, so drift is answered by regenerating
them; files co-owned with the user (settings.json, .mcp.json, .vscode/) are
legitimately edited, so drift is answered by merging and reporting. Hash tracking
on the first is over-engineering; blind rewriting of the second destroys work.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011x4ms5qcGuZgYhCxfdHMUb

* refactor(cli): drop the re-export that only served a test

`doctor-use-case.ts` re-exported `extractAtReferences` and
`extractMarkdownLinkTargets` from `domain/formats/markdown-references.js`. No
production code went through it: `doctor-references-use-case.ts` already imports
them from the domain directly. The only consumer was the unit test, which reached
domain functions through the use case.

A test shaping production code, and a use case turned into a hub for a domain
module. The test now imports the source; the re-export is gone.

Surfaced by biome's noBarrelFile, enabled in the previous commits.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011x4ms5qcGuZgYhCxfdHMUb

* docs(cli): persist the context refactor plan

Eight documents from a scoping session backed by measurements on the code. Every
figure is reproducible.

The CLI's own value is translation: a user on Claude Code can register the
marketplace themselves, but cannot convert one content tree into Cursor's .mdc,
Codex's TOML and Copilot's .github/instructions. That reorders everything —
translate becomes the core, framework one of its clients.

Four contexts in a chain: framework -> translate -> tools -> kernel, plus
framework -> distribution. Command grammar taken from Claude Code and Codex,
where it holds without exception: a bare verb performs an action, a noun then a
verb manages a resource. translate absorbs framework build (measured identical:
same source, same output dir, same flat/marketplace modes), sync replaces
restore, doctor absorbs status, ai and ide become a --tool flag.

Acceptance test for the whole refactor: adding a sixth tool must touch one file.
It touches eight today, and a test now measures it.

The migration plan's rule is that a move and a scope change never share a commit:
a neutral batch passes golden and e2e untouched, a scope batch recaptures the
snapshot and its diff is the review.

Corrections made along the way are kept, because they show where the reasoning
slipped: materialization is not the cause of half the CLI (3 tools of 5 already
point rather than copy), and cutting test volume is dropped — the suite runs in
25s for 2158 tests, no subject is tested at two levels, and one file of 139 is
heavily doubled.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011x4ms5qcGuZgYhCxfdHMUb

* docs(cli): plan phase 0, extending the golden net

Reading the golden test changed what this phase is. `captureMatrix` runs commands
sequentially in one project directory: it is a scenario where state accumulates,
not a list of independent invocations, and `clean --force` is terminal. Extending
it is a scenario design, not commands appended to a list.

Three things the reading settled. The fixture marketplace offers `aidd-test` from
a local path, so plugin commands stay offline and deterministic. `framework build`
already has its own golden over the nine target/mode cells, so it is not
duplicated here. And the current snapshot never captures a modified tracked file
— yet drift detection is what `status` and `doctor` share, and what the refactor
touches most. That is the real gap, not the missing command count.

The phase is a scope batch by the plan's own rule, since it recaptures the
baseline. It should also be the only one whose diff is pure addition: if an
existing entry changes, the capture is not deterministic.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011x4ms5qcGuZgYhCxfdHMUb

* docs(cli): resolve a contradiction about framework build's golden

`migration.md` asked phase 0 to add `framework build` to the baseline matrix.
`phase-0.md`, written after actually reading the golden test, says the opposite:
it already has its own golden over the nine target/mode cells. The second is
right; the first predates opening the file.

Also states plainly what the plan implied but never wrote: the command is not
removed, and not yet renamed. The surface change is phase 12, last, because the
e2e net invokes the CLI and renaming breaks it exactly when it is most needed. The
behavior is never removed at all — `translate` absorbs it, same engine, new name.

So its golden is touched twice, at two moments: phase 1 drops flat mode for the
four native tools, taking nine cells to five; phase 12 changes the invocation,
keeping the old spelling as an alias until the tests have moved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011x4ms5qcGuZgYhCxfdHMUb

* docs(cli): turn the scoping into an executable plan of 17 phases

`migration.md` was a scoping note in thirteen prose phases. This is the plan in
the repo's own format: `plan.md` plus one phase file each, with a projection, a
user journey, a test scope, tasks and acceptance criteria.

Sizing changed the count. `migration.md`'s phase 1 held four deletions of very
different risk — dead code touches nothing, dropping the manifest migrations
changes what the CLI accepts, and removing four flat build cells rewrites the
build golden. They are four phases now, so each ships and is reviewed on its own.

Two things the plan makes explicit that the note left implied. Phase 4 opens with
a check before it removes anything: it is the only step that can refuse a project
that used to load. And phase 16 orders the surface change internally — `sync`
first because it replaces nothing, then `doctor` enriched, then `translate` before
`framework build` retires.

Every phase's acceptance table ends the same way for a neutral batch: golden and
e2e pass unmodified. That is the plan's rule, restated where it is enforced.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011x4ms5qcGuZgYhCxfdHMUb

* docs(cli): add three nets and split the oversized phase

The plan was reported at 7 of 10 with three named weaknesses. Each is answered by
something that fails on its own rather than by a promise to be careful.

Eleven relocation phases had no proof that the user-visible surface stayed put:
the golden covers a scenario, not the command tree. Phase 1 now freezes a `--help`
snapshot of every command and subcommand — no fixture, no network, and a moved
flag or a changed description fails immediately, naming the command.

Phase 17 was the one phase changing the net and the subject at once: recapturing
the golden cannot tell a successful rename from a behavior change, because the
command string moved too. Its net is now equivalence, not the snapshot — while
both spellings exist, each pair runs on two identical fresh projects and must
produce the same exit code, files, manifest and output. The test dies with the
aliases, and its passing run is what licenses their removal.

Extracting the framework context and redesigning `Manifest` in one pass could not
both be reviewed, so they are two phases. The split gains its own net: load every
manifest fixture, write it back, assert the bytes are unchanged — before and after.

Also promoted the chain to a test rather than a lint pattern (`context-graph`),
and moved the manifest-migration removal from second to fourteenth. It is the only
step that can refuse a project that used to load, and nothing depends on it, so it
now sits where postponing it costs nothing — after the aggregate split, on a
smaller file, with the round-trip test available.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011x4ms5qcGuZgYhCxfdHMUb

* docs(cli): add a smoke phase, after running the suite that nobody runs

`scripts/smoke-tools.sh` drives the built binary with real arguments in throwaway
projects and reports 100% leaf command coverage, 37 of 37, measured by itself. It
has no CI job and no lefthook entry, and its last commit is the repository move.

Running it settled three things a reading could not.

It is red. 73 pass, 4 fail, 7 min 11 s. The four are one scenario: it sets up with
`--plugins recommended`, corrupts the cached catalog, then expects
`plugin install aidd-dev` to fail with a message naming `marketplace refresh
--force`. It gets "already installed", because `aidd-dev` is in the recommended
set. The scenario stopped testing what it claims the day that plugin was promoted,
and nobody saw it because nobody ran it. A test defect, not a product one.

It needs the network. Seven invocations use `--source remote` and fetch the really
published framework — which is why one injected corrupt shape is
`{"message":"API rate limit exceeded"}`. A net gated by a rate limit cannot block
a build, so the phase splits a hermetic subset from a remote one.

And the real gap is options, not commands: 11 of 24 declared flags have never been
passed once, including `--flat`, which phase 5 removes for four tools, and
`--scope`, which decides where files land.

Two of my earlier figures were wrong before execution — 23 of 27 commands and 44
invocations — because the regex missed loop invocations. Corrected in the README,
kept visible rather than quietly replaced.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011x4ms5qcGuZgYhCxfdHMUb

* docs(cli): a fourth dormant net, broken by a TypeScript upgrade

`stryker.conf.json` mutates exactly one file: `src/domain/models/manifest.ts`,
break threshold 50. That is the strongest available evidence that this aggregate's
tests would notice a change — and `manifest.ts` is precisely what phase 14
redesigns.

It does not run. `stryker run` crashes on
`TypeError: ts.parseConfigFileTextToJson is not a function`: Stryker 9.6.1's
TSConfig preprocessor calls an API that TypeScript 7.0.2, the native port, no
longer exposes. No CI job, no hook, so nobody saw the upgrade break it.

It becomes task 0 of phase 14, before the split rather than after: a mutation
score taken after a redesign proves nothing about the redesign. If Stryker cannot
be made to work with TypeScript 7, the phase records that and names the round-trip
test as the weaker stand-in.

Also deleted `migration.md`. It kept its own thirteen-phase numbering against the
plan's nineteen, and two numbering schemes in one folder are the kind of trap this
whole effort exists to remove. Its two sections that lived nowhere else — how tests
behave during the migration, and what can run in parallel — moved to the README.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011x4ms5qcGuZgYhCxfdHMUb

* docs(contributing): name the pnpm 11 first-install stop

pnpm 11 will not run a dependency's build script until it is allowed, so the first
root install ends on `ERR_PNPM_IGNORED_BUILDS: lefthook` — and lefthook's build
script is what installs the git hooks, so nothing works until it clears.

Two ways out are documented. `pnpm install --ignore-scripts && pnpm exec lefthook
install` leaves nothing behind, both steps verified. `pnpm approve-builds lefthook`
writes a `pnpm-workspace.yaml` at the root, which is deliberately not committed:
with that file present, `pnpm --version` inside `kanban/` reports 11.20.0 instead
of 10.14.0. The effect reproduces; its cause was not found — no `packageManager`
field, no `.npmrc`, no `.tool-versions`, and pnpm's workspace-state files record no
version. It is stated as observed, not explained.

CI is unaffected either way: its only root install, in validate.yml, already passes
`--ignore-scripts`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011x4ms5qcGuZgYhCxfdHMUb

* test(cli): extend the golden net with drift, errors and a help surface

Phase 1 of the context refactor. The baseline held five invocations while its own
docstring claimed "each public CLI command"; the eighteen phases that move code
had almost nothing to check themselves against.

The scenario now runs 15 invocations and a second project holds 7 error paths,
prefixed so both share one snapshot: 22 entries, 6 of them non-zero exits. It
covers doctor, marketplace list, plugin list/install/remove and a second tool, none
of which were captured before.

A new help-surface golden walks the command tree from the root and freezes `--help`
for all 44 nodes. No fixture, no network, 13 seconds. It is the only net that
catches a moved flag or a reworded description while files travel between contexts,
and it was verified by changing one description and watching it name the
invocation.

Two invocations I wrote were wrong and the capture said so, which is the point:
`plugin remove --yes` recorded `error: unknown option`, not a removal. Worth noting
while fixing it — `plugin install` accepts `--yes` silently and `plugin remove`
rejects it, though neither declares it and it is not a global option.

The docstring now names what it does not cover: the network, the interactive paths,
`framework build` and the help shape, each already covered elsewhere or excluded on
purpose.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011x4ms5qcGuZgYhCxfdHMUb

* fix(cli): make `aidd restore --force` actually force

The flag was inert. `restore.ts` folded it into one boolean —
`interactive = !force && isTTY` — and `RestoreAllUseCase.execute` only ever took
`interactive`, so `runConfigRestore` passed `force: interactive`. In a non-TTY run
that is always `false`, whatever the user typed.

A modified tracked file therefore raised `InputRequiredError`, which the caller
swallowed into a warning reading "Use --force to overwrite modified files in
non-interactive mode" — addressed to someone who had just passed `--force`. With
nothing restored, the command then reported "Nothing to restore — all files are
unmodified" while `status` reported the same file modified. Two commands
disagreeing about one file, and neither wrong from its own point of view.

`force` is now threaded from the command to the decision. The error no longer
aborts the loop either, so a second tool is reached: the golden goes from
"Nothing to restore" to "Checking claude... Checking cursor... Restored 1 file(s)".

`ai restore` never had the defect — it passes `force` and `interactive` separately.
Only the global command folded them.

Found by the golden net extended in the previous commit, which is what it is for.
Two regression tests pin it, verified by reinstating the defect and watching the
force case fail.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011x4ms5qcGuZgYhCxfdHMUb

* test(cli): recapture the golden after the restore fix, close phase 1

The baseline moves from "Nothing to restore — all files are unmodified" to
"Checking claude... Checking cursor... Restored 1 file(s), kept 0 file(s)", and the
status that follows returns to "All files are in sync". That diff is the review of
the fix in the previous commit.

Phase 1 closes with every criterion met. One needed rewording rather than a claim:
it demanded a pure-addition diff on the reasoning that any changed entry proves the
capture non-deterministic. An entry did change, for a reviewed reason, so the
criterion now allows that and requires the reason to be recorded. Determinism is
still asserted directly — two captures byte-identical, two verification runs green,
and the snapshot carries no absolute path, version string or timestamp.

The phase file records what extending the net turned up: the inert `--force`, two
invocations this phase wrote wrong that the capture caught, and the fact that every
tracked file in this scenario is co-owned — with claude and cursor installed the
manifest tracks one `settings.json` each and `plugin install` writes no tracked file
at all, since both tools get a registered marketplace rather than copies. Exercising
the CLI-owned regeneration regime needs a flat-mode tool.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011x4ms5qcGuZgYhCxfdHMUb

* docs(cli): record that telemetry lands in the current structure

It is being built in parallel with this refactor and will follow today's
conventions, so its files move with their layer rather than arriving in the target
shape. That keeps one structure at a time, at the cost of one more migration.

Three consequences kept where they will be read. The phase projections name files
one by one and know nothing of telemetry, so whatever it adds has to be folded into
the projection of the phase that moves its layer. "Enable telemetry for a given
tool" reads state that `framework` owns, which the "only framework imports another
context" invariant does not allow — the fragility flagged when that rule was
written, now arriving.

And `docs/FAQ.md:44` currently promises the opposite: "there is no AIDD server,
account, or telemetry." That is the only place in the repository carrying the
promise — the README does not — so touching the README alone would leave it false
for a release.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011x4ms5qcGuZgYhCxfdHMUb

* docs(cli): replan phase 2 after measuring what the smoke suite covers

The phase was written from reading the script. Running it, and then counting what
the token gate hides, changed what the work is.

Line 106 falls back to `gh auth token`, and everything substantial sits behind
`if [[ -z "$TOKEN" ]]`. Counted statically: 11 invocations are hermetic, 30 are
gated — the setup matrix, the global read-only commands, restore, the per-tool AI
and IDE commands, the plugin commands, the update conflict guard and the fault
injection. On a machine where `gh` is logged in the suite covers 41 invocations and
reports 100% leaf coverage; where it is not, it covers 11. Same command, same
repository, two different nets. That, not the option gap, is why it cannot gate a
build.

So the phase's real work is moving those 30 onto the local fixture — including
swapping `aidd-dev`, a really published plugin, for the fixture's `aidd-test` and
rechecking every assertion that depends on plugin content.

A second run also had `plugin update (all)` exceed the script's own 180s ceiling and
get killed. Seen once, not diagnosed, and now task 0: reproduce it, keep a ceiling
so one hang cannot stall a run, and fix it outside this phase if it is a product
defect — a net phase does not change behavior.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011x4ms5qcGuZgYhCxfdHMUb

* test(cli): make the smoke suite hermetic and put it in CI

It reported 100% leaf command coverage and ran nowhere. Worse, that number depended
on the machine: line 106 fell back to `gh auth token`, and 30 of its 41 invocations
sat behind `if [[ -z "$TOKEN" ]]`. Where `gh` was logged in it covered everything;
where it was not, eleven invocations. Same command, two different nets — which is
why it could not gate a build.

Every setup now uses the local framework fixture. Coverage is 37/37 with or without
a token, the run takes 92s instead of 7 min 11 s, and a new blocking `cli / Smoke`
job runs it. One opt-in section (`SMOKE_REMOTE=1`) keeps what a fixture cannot
prove: that fetching from a real remote source works.

A third token dependency turned up on the way: the coverage threshold itself only
fired when a token was present. It is unconditional now.

The four failures are fixed. The fault-injection scenario set up with `--plugins
recommended` and then tried to install a plugin that was therefore already there,
so it never read the corrupt catalog it had just written. It also cannot be
hermetic — it corrupts the fetched catalog cache, which only a remote source
populates — so it moved into the opt-in section.

Once it finally reached its own code path, its expectation proved obsolete: a
corrupt fetched catalog no longer blocks the install. Recovering silently may be
right, since a fetched catalog is a cache. That question is now reported rather
than asserted, and it is this phase's one open decision.

All 24 declared options are exercised, up from 13. `--dry-run` is asserted to write
nothing by comparing the tree before and after; the two `--scope` values are
asserted to write to different registries.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011x4ms5qcGuZgYhCxfdHMUb

* test(cli): pin the corrupt-catalog recovery instead of demanding an error

Phase 2 left one question: a corrupt fetched catalog no longer blocks
`plugin install`. It is the better behavior — a fetched catalog is a cache, and the
rule for CLI-owned files is to regenerate rather than error — so the check now pins
the recovery rather than reporting the question.

Pinning it took two attempts, and the first is worth recording. Asserting that the
cache file came back valid failed on one shape of four: three make the CLI re-fetch,
`{ truncated` does not. That is an internal difference with no user-visible
consequence, and an assertion on it would flap for no reason. The check now asserts
what a user sees — the install succeeds and the CLI still works with the corrupt
catalog on disk.

Both modes now run with no skipped check: 99 hermetic, 108 with SMOKE_REMOTE=1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011x4ms5qcGuZgYhCxfdHMUb

* docs(cli): re-verify phase 3's targets and size them

Every deletion target checked again after phases 1 and 2 moved code around them,
and each now carries its evidence and its size rather than an assertion.

Roughly 700 lines. The foreign-catalog branch is 4 parsers, `normalized-plugin.ts`,
a port method and 5 of the adapter's 123 lines — `NormalizedPlugin` appears in seven
source files and no test. `marketplace-entry.ts` is still the only file unreachable
from `src/cli.ts`, 103 lines plus a 157-line test, and `knip.json` still names it to
stay quiet. And `mcp-exclusion.ts` loses 134 of its 186 lines.

The telling part is the middle: `mcp-exclusion`'s four exports and
`buildMergeFileEntries` each appear once in `src` — their own definition — and once
in tests. Live tests guarding dead behavior. They pass, they prove nothing, and
without this phase they would ride through eleven relocation phases.

Two test files the phase had missed are now in its projection: the merge-entry unit
test and the catalog-repository integration test.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011x4ms5qcGuZgYhCxfdHMUb

* refactor(cli): delete the dead code before moving anything

1871 lines across 23 files, one inserted. The foreign-catalog branch (`loadForeign`,
four parsers, `normalized-plugin.ts`, a port method, five adapter methods),
`domain/models/marketplace-entry.ts` with the `knip.json` entry that kept it quiet,
and six exports that each appeared once in `src` — their own definition — and once
in a test. Live tests guarding dead behavior, which would otherwise have ridden
through eleven relocation phases.

The nets were not touched. Golden, help surface, smoke and e2e all pass on files
this commit does not modify, which is what makes a deletion reviewable: unit tests
drop from 1522 to 1399 and integration from 510 to 482 because dead tests left with
the dead code, while behavior stayed put.

Two things the plan had not foreseen. The compiler named four whole test files for
the deleted parsers — 595 lines — and a 180-line `loadForeign` block in the adapter's
integration test, none of them in the projection: reading a codebase is not
compiling it. And deleting exposed more dead code — `ForeignSchemaValidationError`
existed only for the path that just went. Each removal uncovers the next, which is
the argument for doing this before the moves.

`tool-addition-cost` refused to stay silent on six now-obsolete entries and named
them; its baseline is 20 to 14. Duplication fell 3.43% to 3.17%, so the jscpd
threshold moved 3.5 to 3.2 rather than leaving a third of a percent of slack.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011x4ms5qcGuZgYhCxfdHMUb

* docs(cli): correct phase 4 before executing it, not during

Phase 3 discovered half its own scope while running, because its projection was
read rather than compiled. This one was checked first, and it was wrong in four
places.

It claimed `snapshots/phase0` would lose a help line. That snapshot holds 22 command
invocations and captures no help at all. The snapshot that changes is the
help-surface golden, built in phase 1 and therefore invisible to a fiche written
before it. The smoke suite was ignored entirely — it has a `plugin create` section,
an `ALL_COMMANDS` entry, and a coverage report that must go from 37 leaf commands to
36 and stay at 100%. And it listed one test file where there are five.

It also missed a cascade: `parsePluginComponentKind` has exactly one production
caller, the `--type` option of the removed subcommand, so
`plugin-component-kind.ts` and `InvalidPluginComponentKindError` fall with it. That
is the pattern phase 3 hit with `ForeignSchemaValidationError` — written into the
plan this time rather than discovered mid-flight.

Checked and safe: the schema integration test dies with the scaffold, but the two
adapters it exercises are covered by eight other tests, so nothing that survives
loses coverage.

Roughly 750 lines, 520 of them tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011x4ms5qcGuZgYhCxfdHMUb

* refactor(cli): drop plugin scaffolding

921 lines deleted, 2 inserted, across 15 files. `aidd plugin create` was exposed in
`--help` and documented nowhere: `docs/CREATE_PLUGIN.md`, the contribution guide,
describes an entirely manual flow. Nobody writes third-party plugins today and the
command was never on a contributor's path.

Correcting the fiche before executing paid off. The four mistakes it carried —
naming the wrong snapshot, ignoring the smoke suite, listing one test file where
there were five, and missing a cascade — were fixed as plan edits rather than
discovered as surprises. The predicted cascade then happened exactly as written:
`plugin-component-kind.ts` and `InvalidPluginComponentKindError` lost their last
caller with the `--type` option.

It went one level deeper than predicted, and `knip` is what found it:
`domain/formats/marketplace-json.ts` was imported only by the deleted use case. Its
test followed, 97 lines in all. Third phase running where deletion uncovers
deletion, second where tooling saw what reading had not.

The nets behaved as intended. The smoke suite reports 36 of 36 leaf commands and
still 100%; the help-surface diff removes exactly one entry and one line from
`aidd plugin`'s own help; `snapshots/phase0`, the build golden and every surviving
e2e file pass unmodified.

One flaw surfaced, in a ratchet added two phases ago. Phase 3 tightened jscpd to
3.2% after duplication fell to 3.17%. Deleting 921 lines of NON-duplicated code
pushed the ratio to 3.22% — the same 66 clones and 694 lines over a smaller
codebase. A percentage ratchet punishes deletion. Threshold moved to 3.3; phase 5
should expect the same, and ratcheting the clone count instead would be the fix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011x4ms5qcGuZgYhCxfdHMUb

* test(cli): explain the coverage exclusion and unblock stryker's first failure

Two gaps in the regression net, both measured rather than assumed.

`cli.ts` and `commands/**` were excluded from coverage with no reason recorded.
Including them drops the total from 91.3% to 82.0% and reports `cli.ts` at 0% and
`commands/` at 0.69% — while 126 e2e tests and 98 smoke checks exercise them. Both
spawn `dist/cli.js` as a subprocess and v8 coverage does not cross a process
boundary, so including them produces a false zero rather than a measurement. The
exclusion stays, with that reasoning now in the config where the next reader will
find it.

Stryker's crash is fixed: `tsconfigFile: ""` avoids its TSConfig preprocessor,
which called `ts.parseConfigFileTextToJson` — an API TypeScript 7 no longer exposes.
It now reaches its initial test run and fails further along, for a different and
named reason: its runner launches vitest, which picks up `vitest.workspace.ts` and
therefore runs e2e, and the build golden does not survive Stryker's sandbox where
absolute paths differ. `vitest.dir`, `vitest.related` and a dedicated config file
were each tried; none narrows the initial run. Unblocking it means stopping Stryker
from using the workspace, which is not done here.

Phase 14 still wants it, now with an obstacle named instead of "broken".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011x4ms5qcGuZgYhCxfdHMUb

* docs(cli): cancel phase 5, its premise does not hold

It was going to remove flat build mode for claude, cursor, copilot and codex,
keeping it only for OpenCode, on the grounds that their flat cells duplicated their
native mode. Checking that before executing is what caught it.

Two axes were conflated. `PluginsCapability.mode` describes how a plugin is
installed into a tool — four of five declare `native`. `FrameworkBuildMode`
describes how the framework is built for a target. The first measurement said
nothing about the second.

The build golden settles it: for claude, marketplace mode produces 198 files under
`.claude-plugin/` and `plugins/`, flat mode produces 189 under `.claude/agents/`,
`.claude/skills/` and `.claude/hooks/`. One is a distributable marketplace tree, the
other puts the framework straight into the tool's config directory. And `--flat` is
documented in `cli/README.md` in four places, including the clause that names the
second use case: "or when you want files on disk in the project".

So the nine build cells stay, the 831 lines of flat-specific code stay, and the plan
loses a deletion phase. Nothing downstream depended on it.

One finding survives and phase 6 already carries it: the plugin materializer
re-derives "flat" from `toolId === "opencode"` instead of reading the profile. That
is about plugin materialization — the axis this phase confused with build mode — and
it is a real defect either way.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011x4ms5qcGuZgYhCxfdHMUb

* docs(cli): record the three ways a marketplace gets registered

The same operation is done three different ways. Copilot and codex drive the tool's
own CLI through `nativeActivation`. Claude writes `.claude/settings.json` by hand —
`extraKnownMarketplaces` and `enabledPlugins` — while `claude plugin marketplace
add` exists and is the officially supported path. Cursor needs no registration at
all.

That file is co-owned: it is the only tracked file in the Claude profile, the one
whose drift showed up in phase 1. So the CLI hand-edits another tool's private
configuration when that tool publishes a command for it.

The Copilot profile documents why it drives the CLI — "Copilot treats enabledPlugins
in settings.json as a recommendation, not an auto-install (copilot-cli#2249)" —
which means the CLI path was taken because the file was not enough, not on
principle. The Claude profile carries no comment at all: nobody questioned it.

The tradeoff is real and not mine to settle: writing the file works whether or not
the tool is installed, while driving its CLI requires the binary but rests on a
public contract instead of a private file format that can change without notice.
Two of four tools already accept that dependency.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011x4ms5qcGuZgYhCxfdHMUb

* refactor(cli): one data-driven plugin CLI adapter instead of three

Three subclasses differed by two verbs. `NativePluginCliAdapter` takes the binary
and both verbs from the tool profile, so a tool's name no longer appears outside
its own profile: the `tool-addition-cost` ratchet drops from 14 entries to 11, and
the driven-tools registry in `deps.ts` is now derived from the profiles rather than
hand-listed.

The change started as an attempt to uniformize Claude onto its own CLI, and that
part was reverted after measuring it. `claude plugin marketplace add` exists and
takes a local path, but it writes `.claude/settings.json` itself — after this CLI
wrote that file and recorded its hash. Two writers, one recorder: the golden showed
`status` reporting the file modified forever after. Without `--scope project` it is
worse still, since the command defaults to user scope and would register the
marketplace globally for every project on the machine.

Both the profile and the `NativeActivation` type now carry that reasoning, so the
next reader does not retry it.

Cursor was checked too: `cursor-agent plugin marketplace add` now exists but takes a
git URL and indexes per account, so it cannot register a locally built marketplace.
Cursor's plugin-local materialization stays.

What this uncovers is bigger than the case: `.claude/settings.json` is co-owned with
the *tool*, not the user. Tracking the hash of a file another program legitimately
rewrites manufactures false drift — a third regime beside CLI-owned and user-co-owned,
and one nothing in the plan decides yet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011x4ms5qcGuZgYhCxfdHMUb

* docs(cli): phase 5 becomes letting each tool own its configuration

The slot held a cancelled phase; it now holds the change that replaces it, with the
cancellation kept as its first section so the wrong premise stays visible.

The CLI hand-writes `.claude/settings.json` and then records that file's hash in its
own manifest. The first attempt at fixing that drove `claude plugin marketplace add`
*in addition* to writing the file, and the golden showed the result: two writers,
one recorder, so `status` reports the file modified forever. The fix is not to add a
second writer, it is to stop being one. Write through the tool's command, verify
through `claude plugin marketplace list --json`, track nothing the tool owns.

One decision gates it, and the phase says so rather than assuming: setup currently
works when Claude Code is not installed, because writing the file leaves a
registration that takes effect later. Driving a command cannot. Require the binary,
fall back to the file when it is absent, or wait for hosted marketplaces.

That last option is why the remote direction matters here. The built marketplaces
are local paths, which is the only reason Cursor cannot be driven at all — verified
against the installed CLI, `cursor-agent plugin marketplace add` takes a git URL and
indexes per account. Host them and claude, codex, copilot and cursor all accept a
URL, leaving the four profiles differing by paths and formats only. That is the
shape phase 10's acceptance test is asking for.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011x4ms5qcGuZgYhCxfdHMUb

* docs(cli): plan the hosted per-tool marketplaces

Written after checking what already exists, and most of it does.

`ci.yml` already builds the nine tool/mode cells on every release and attaches
`aidd-framework-<tool>-<mode>-<version>.zip` to the GitHub release. The CLI does not
consume them: `--release` picks a framework *source* release and then rebuilds
locally into `.aidd/cache/built/`. The same build runs twice, once to publish and
once per user to install. And `docs/FAQ.md:42` already promises the direction —
"public-marketplace publishing is on the way".

What is missing is the form. Zips are published; the tools' commands want a git URL.
Verified against the installed CLIs: claude takes a URL, path or GitHub repo, codex
and copilot take a marketplace snapshot, and cursor takes a git repository URL
indexed per account — which is the single reason cursor cannot be driven at all
today. Superpowers solves the same problem by rsyncing into a registry repo and
opening a PR.

Three shapes are laid out to choose between: a repo per tool, branches of one repo,
or keeping zips and not driving the commands. The last one unblocks nothing.

What it would unblock is named: phase 5's open decision disappears, since with a URL
there is no local path to point at; cursor becomes drivable; the double build goes
away; and phase 10's acceptance test becomes real.

What it costs is named too: no offline install, content public by construction so a
private framework needs a second path, revocation moving into the tool's config, and
an unanswered question about who publishes what and when.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011x4ms5qcGuZgYhCxfdHMUb

* docs(cli): park phase 5 until the hosting shape is decided

It waits on a product decision that is not being taken now: neither the offline
behaviour nor the hosting form. Marked blocked rather than pending so the plan's
state stays true.

It blocks nothing. No other phase depends on it, and the refactor continues at
phase 6.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011x4ms5qcGuZgYhCxfdHMUb

* docs(cli): record the per-tool scopes and what the gitignore exposes

Checked against the four installed CLIs: only Claude offers scopes at all — `user`
(its default), `project` and `local`. Codex is user-global by design, copilot has no
`--scope`, cursor indexes per account. So "allow every scope where available" means
AIDD's model moves from one fixed scope per tool to the list a tool supports plus a
default, and exposes `--scope` only where there is a choice.

The gitignore settles which default. AIDD adds one line, `.aidd/cache/`, leaving
`.aidd/manifest.json`, `.aidd/marketplaces.json` and `.claude/settings.json`
committed. But that settings file holds the marketplace registration, and the path
it registers is absolute and points into `.aidd/cache/built/` — the ignored
directory. Verified on a fresh project: a teammate cloning the repo gets a pointer
to something that cannot exist until they run setup themselves.

That is a latent defect in today's design, independent of the rest, and it decides
the split: AIDD's runtime config is genuinely shareable and belongs in
`.claude/settings.json` at project scope, while the registration can only ever be
machine-local and belongs at `local` scope — which writes a separate file,
`.claude/settings.local.json`, removing the hash collision that sank the first
attempt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011x4ms5qcGuZgYhCxfdHMUb

* refactor(cli): import every symbol from where it is defined

Four untanglings, no file moved and no behaviour changed, so the golden snapshots
and the 126 e2e cases pass exactly as they were.

The two design cycles are gone. `UserFileSection` and `UserFileSectionKey` lived in
`tools/contracts.ts` while `formats/command.ts` was the only place building them, so
they move to `formats/`, and the three `AI_TOOL_IDS` imports in `capabilities/` now
point at `models/tool-ids.ts` rather than travelling through the tool registry.
Neither cycle was a runtime cycle — both closed through `import type`, which is why
`noImportCycles` never said anything about them.

Six re-exports are gone with them: the registry stopped re-publishing eight
identifiers it had imported from `models/tool-ids.ts`, and `setup-use-case.ts` and
`update-all-use-case.ts` stopped standing in for the modules that define
`SetupToolsResult`, `ToolInstallResult` and `GlobalExecutionError`.

The criterion for that last part named Biome as the judge, but Biome cannot deliver
that verdict: `noBarrelFile` only sees files that do nothing but re-export, and
`noReExportAll` only sees `export *`, while the form that had actually accumulated
here is narrower than both — a module importing a symbol and exporting it again. A
criterion checked by a tool blind to what it checks is the failure this refactor
exists to correct, so it becomes a ratchet with an empty baseline, proven by
injection: putting a re-export back makes it fail.

`MarketplaceSettings` and its entry types leave `plugins-capability.ts` for their own
file. They are read by marketplace settings synchronisation alone, while
`PluginsCapability` is read by every tool profile, and none of the five profiles pulls
them in any more.

Finally, the framework build mode is read off the tool's profile instead of being
re-derived from its name. `frameworkBuildModeFor` sits beside `nativeActivationOf`,
which already reads the profile the same way, and the translator that used to ask
whether the tool was called "opencode" no longer names a tool at all — one entry off
the tool-addition ratchet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011x4ms5qcGuZgYhCxfdHMUb

* docs(cli): refresh the index's net table after phase 6

The smoke row still described the suite as running nowhere and red, which phase 2
fixed: it is hermetic, wired into CI, and up from 77 checks to 98. And the
non-re-export ratchet phase 6 added is a net in its own right, worth listing next to
the others precisely because Biome cannot see the form it catches.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011x4ms5qcGuZgYhCxfdHMUb

* docs(cli): settle phase 5 — the offline question was not the blocker

Measured, and two of the beliefs the phase rested on were wrong.

The offline precedent already exists and costs less than the phase assumed. `aidd
setup --ai codex` lays down exactly one file, `.codex/config.toml`, holding `model`
and `approval_policy` and no registration at all: codex's marketplace exists only
through its command, so codex without its binary is already a setup that succeeds and
registers nothing. Claude lays down exactly one file too. Driving the command would
not introduce a new silent failure — it is already in place for one tool in two.

But what AIDD registers for itself cannot be shared, under either source mode. With
`--source remote` it writes an absolute path into `.aidd/cache/built/`, the directory
the gitignore excludes; with `--source local` it writes the absolute path of the
source repo. What gets registered is never the upstream repo, it is the built tree —
the Claude-shaped marketplace — so the path is machine-local by construction until
those trees are hosted.

And the same file carries entries of opposite natures: a third-party marketplace
declared as a GitHub repo writes `{source:"github", repo:"…"}`, which is perfectly
shareable and correct to commit. So a committed file mixes what can only belong to one
machine with what should belong to the team. That contrast, not the path on its own,
is what decides the phase.

So the phase splits. 5a separates the two natures — AIDD's own registration moves to
`.claude/settings.local.json`, gitignored, while third-party entries stay in the
committed file — which needs no hosting, no CLI driving, keeps AIDD the sole writer of
both files so no hash collides, and fixes a real defect: today a teammate cloning
inherits a pointer to a directory that cannot exist for them. 5b, driving the tool's
own command, stays blocked and is attached to the hosting note, because at project
scope it recreates the collision that sank the first attempt, at local scope it only
matches what writing the file already achieves, and cursor cannot be driven at all
since its command takes a git URL.

Two of the phase's own criteria were false and are corrected in place: `claude plugin
marketplace list` has no `--json`, and `configOutputPaths` keeps AIDD writing
`.claude/settings.json` for `respectGitignore` and `permissions`, so "no file under
`.claude/` in the manifest" was never reachable — only the machine-local entry leaves.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011x4ms5qcGuZgYhCxfdHMUb

* test(cli): keep the smoke suite out of the real home directory

The suite already isolated `AIDD_USER_CONFIG_DIR`, which covers what this CLI writes,
but not what it makes the tools write. `marketplace add --scope user` lands in the
tool's home settings, and native activation shells out to `codex` and `copilot`, which
register marketplaces in their own home store. Pointed at the real home, every run left
a registration behind naming a temp directory the script then deleted.

That is not theoretical: `~/.claude/settings.json` and copilot's global store each
carried an `aidd-framework` entry pointing into a deleted `aidd-smoke-tools-*` directory
on a developer machine. It also shadowed a real project's registration by name, which is
how it was found.

Moving HOME into the run's own temp root fixes both. The token is read first, since `gh`
looks for its credentials under the real home. Verified by counting the leftover entries
in both tools around a full run: unchanged, where before each run added one. The suite
still passes 98 checks across 36 of 36 leaf commands.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011x4ms5qcGuZgYhCxfdHMUb

* feat(cli): keep the machine-local registration out of the committed settings

`.claude/settings.json` is committed, and it carried the marketplace this CLI
registers for itself. That registration names the tree built under `.aidd/cache/` by
absolute path — and `.aidd/cache/` is the one line the CLI adds to `.gitignore`. So a
teammate cloning the repository inherited a pointer to a directory that could not exist
for them, and would not exist until they ran setup themselves.

It is not a matter of which source was chosen. `builtSourcesForTool` replaces every
successfully built marketplace with the built directory, and `resolveSourceForSettings`
makes that absolute, so the declared source is never what lands in the file. Both
`--source remote` and `--source local` produce a path belonging to one machine.

The sibling key is the opposite. `enabledPlugins` holds `plugin@marketplace` names, no
paths at all, and is worth sharing. So the cut is between the two keys, not between
tools or between entries: `MarketplaceSettings` gains `marketplacesSettingsPath`,
mirroring the `enabledPluginsSettingsPath` it already had, and the Claude profile sends
its registrations to `.claude/settings.local.json` — the file Claude itself writes when
told `--scope local`, and which the CLI now gitignores, since Claude does not.

That file is written and never hashed: recording an absolute path in the manifest would
make every other machine read as drift. Two consequences had to be handled.

`status` began reporting the new file as *added*, which is the same lie in a different
shape. `detectAddedFiles` already excluded `.backup` files for exactly this reason, and
files a profile declares as machine-local now follow that precedent.

And an untracked file announces nothing when it breaks — deleted by hand, `doctor` still
called the installation healthy. `DoctorRegistrationUseCase` closes that blind spot by
comparing the registry against what the file declares, and the command it suggests does
fix it.

Projects installed before this keep the key in their committed file, where it holds a
path that is wrong for everyone but its author, so the sync takes it out and re-hashes.

The golden diff shows a file appearing and one hash changing, and no command output
moving at all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011x4ms5qcGuZgYhCxfdHMUb

* test(cli): pin the two things the split could quietly get wrong

Both were reachable, and neither was covered by what shipped with the split.

The shared settings file is written twice in one sync: once to take out the
registration an older install left there, once to merge the enabled plugins. Each write
loads the file itself, so a wrong order would let the second resurrect the key the first
removed. It does not — the eviction writes before the plugins branch reads — but the
tests proved it by accident: the split case had a plugin and no stale key, the migration
case had a stale key and no plugin, so the two writes never met. Asserting the plugin
landed in the migration case is what makes them meet.

The second is why `status` skips these files. It compares the path the profile declares
against the one it rebuilds from the tool's directory, so a profile declaring
`settings.local.json` rather than `.claude/settings.local.json` would silently stop being
skipped and the false "added" would come back. A test around `status` cannot catch that:
the file would land outside the scanned directory, drift nothing, and pass for the wrong
reason — confirmed by injecting exactly that path. So the convention becomes an invariant
checked across every profile in the conformance suite, where injecting it does fail.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011x4ms5qcGuZgYhCxfdHMUb

* feat(cli): give each tool the answer its own architecture allows

Claude offers a machine-local project file, so the registration moved there. Copilot
does not, and treating the two the same would have been worse than doing nothing.

VS Code reads `.github/copilot/settings.json`, not the `copilot` CLI — measured:
`copilot plugin marketplace add` writes `~/.copilot/settings.json` and leaves the
project file untouched. And the documentation VS Code publishes says what that file is
for: "Projects can recommend plugins for team members by configuring plugin settings in
the workspace settings". It is a committed, shared recommendation. A marketplace entry
naming an absolute path on whoever ran the install is not a recommendation to anyone,
and copilot has no local sibling to move it to — `chat.plugins.marketplaces` cannot
stand in either, having application scope, which VS Code refuses in workspace settings.

So the capability gained a third answer rather than a second: registrations go in the
shared file, in a machine-local file, or nowhere. Copilot writes `enabledPlugins` and
nothing else, and learns its marketplaces through its own CLI, which it was already
being driven to do.

Two defects found while checking the rest.

`FileAdapter.listDirectory` returned whatever `relative()` gave it, so backslashes on
Windows — while every caller compares those paths against ones written with `/` in
profiles and manifests. No test could catch it: the in-memory adapter has always
produced `/`, so the two implementations diverged exactly where nobody was looking. The
port now states its shape and the real adapter holds to it.

And `loadSettings` parsed without a guard. That file is genuinely co-owned now —
Claude writes it too, and it is untracked and gitignored, which is precisely the kind of
file people hand-edit. A trailing comma took the whole sync down; it now warns and lets
the merge restore the keys this CLI owns.

The smoke suite earned its keep: `null` slipped past a guard that only rejected
`undefined` and reached `join(root, null)`, which threw and took `plugin doctor` with
it. Pinned by a test that fails when the guard is loosened again.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011x4ms5qcGuZgYhCxfdHMUb

* feat(cli): let the tool write its own configuration where it offers a command

A tool that publishes a command for writing its configuration writes it better than we
do: in its own format, at its own scope, and it will keep doing so when that format
changes. So this CLI now writes only what no command covers.

Claude registers its own marketplaces. An earlier attempt drove the command at project
scope, where it rewrites `.claude/settings.json` after this CLI hashed it — two writers,
one recorder, drift reported forever. `--scope local` writes
`.claude/settings.local.json` instead, which this CLI neither writes nor tracks, so
nothing collides. Verified end to end, including that the file appears only when the
binary is reachable.

What resisted the rule is `enabledPlugins`. Measured rather than assumed: `claude plugin
install --scope project` writes exactly `{"<plugin>@<marketplace>": true}` into the
shared settings file, character for character what this CLI already writes there.
Driving it would be a second way of doing the same thing, and would hand a hash-tracked
file a second writer. It stays ours.

The capability now separates the two axes it had been conflating: `marketplaceSettings`
says *where* the file is, which the gitignore and `status` still need, and
`nativeActivation` says *who* writes it.

Offline follows from that: with the registration driven, an unreachable binary means no
registration, exactly as for codex and copilot. That is the literal reading of the rule
— the file when the tool offers no command, not when the binary is missing.

Driving a command also makes the observable output depend on what the machine has
installed, which turned the golden green here and red without the binary — useless as a
gate. Sandboxed runs now strip from PATH every directory holding a drivable CLI and
reach node through `process.execPath`; filtering by directory rather than naming
directories to keep is what holds where `node` and `copilot` share `/opt/homebrew/bin`.

Under that filter the golden caught two real regressions. The built tree vanished,
because building had been a side effect of registering — while building is this CLI's
job whoever registers, and the tree is what any registration points at. And `doctor`
exited 1 to report that an uninstalled tool had not declared its marketplace, which is
reporting that absent software is misconfigured.

Four dead imports also go, left behind by the re-export removal two commits ago. They
survived because the lint output reaching this session was being filtered; the checks
here now run the binaries directly.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011x4ms5qcGuZgYhCxfdHMUb

* feat(cli): carry the scope through to the tool, and take a name back only when nobody holds it

A marketplace AIDD remembers at user scope was being declared in the project's tool
settings, which is neither what the user asked for nor where it belongs. The scope now
travels: one per-scope argument mapping on the profile, used by `add` and `remove`
alike so the two cannot drift — a remove that omitted the scope its add used would, for
Claude, delete the declaration from every scope at once.

A project-scoped marketplace maps to Claude's *local* scope, not its project one. That
looks like a mismatch and is not: the registration names the built tree by absolute
path, so it belongs to one machine, while Claude's project scope writes the shared,
committed file where such a path is wrong for everyone else. Local scope is
project-bound and machine-bound at once, which is what the content actually is. Verified
with an isolated home: project registrations land beside the project, user ones in
`~/.claude/settings.json`, neither in the other.

The copilot collision is fixed the only way that does not make it worse. Its registry is
global and keyed by name, so a name can be held by a project that no longer exists — and
it then breaks every other project's plugin installs, measured. Removing and re-adding
unconditionally would have had two live projects steal the name from each other on every
sync, uninstalling each other's plugins with `--force`. So the name is taken back only
when the tool reports its source is gone: copilot's `plugin marketplace update` exits 1
on a missing local path and 0 otherwise. Codex's `upgrade` refuses every local
marketplace alike and Claude's reports success on a path that does not exist, so neither
declares the probe, and "cannot tell" reads as "leave it alone". Verified against a real
stale entry on a developer machine, which now points at a live project again.

The smoke suite is the only net that runs real tools — the e2e ones strip the binaries
from PATH on purpose — so it grew the assertions that matter: where each scope's
declaration actually reached the tool. Its own scope checks had been reading AIDD's
registry, which a wrong `--scope` argument would have sailed straight past.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011x4ms5qcGuZgYhCxfdHMUb

* fix(cli): build a user-scope marketplace outside the project that registered it

A marketplace added at user scope is declared once and meant for every project, but its
built tree was written under whichever project happened to register it, and that is the
path the tool's global declaration named. Delete that project and the declaration points
at nothing — the same disease as a global registry holding a project-local path, one
level down.

It now builds under this CLI's own user directory. That directory is already the user's
`.aidd`, so the layout below it repeats the project one without repeating the `.aidd`
segment, and the shape lives beside its project-scoped sibling rather than being spelled
out at the call site.

The user directory itself was being computed from scratch in three places with the same
expression, and this made a fourth. One function now answers it, which also keeps the
`AIDD_USER_CONFIG_DIR` override — the thing that keeps the test suites out of a real home
— in a single place.

Verified on a real project with an isolated home: the user marketplace builds under the
config directory and `~/.claude/settings.json` points there, while the project one builds
in the project and is declared beside it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011x4ms5qcGuZgYhCxfdHMUb

* fix(cli): rebuild when the source can have changed, and tell the tools after an update

Two of the three limits noted while handling scopes; the third is a deliberate
non-fix.

A built tree was considered fresh when the CLI version and the marketplace's catalog
version both matched. For a published source that holds: different content carries a
different version. For a directory on this machine it does not — someone edits a file
and the version stays put, which is the whole of framework development. So `refresh`
re-read the source, reported ok, and left the old build in place; only deleting
`.aidd/cache/built/<name>` by hand got the change through. A local source's version is
no longer believed, nor is any cached answer when a refresh was explicitly asked for. A
real build is 434 files in 0.4s including node startup, so the safe answer is also the
cheap one.

That made the build's own diagnostics surface on every command, since almost all of
them bring the cache up to date. Those lines belong to `aidd framework build`, where a
build is what was asked for; the cache rebuild now traces them at debug, so `--verbose`
still shows them. The golden diff is exactly that: two commands stop repeating what
they skip.

And `update` refreshed the marketplace cache without ever telling the tools about it,
so the command a user reaches for to put a project back in order left a drifted
registration exactly as it found it — `doctor` reported it, `update` did not fix it,
only `marketplace refresh` did. The two now travel together there as they already do in
that command.

The third limit stays open by decision. Two marketplaces pointing at one source produce
built trees declaring the same name, and tools key their registry by that name rather
than by ours, so the second silently overwrites the first — measured, the declaration
ends up naming one marketplace and pointing at the other's content. A warning for it was
written and then removed: it reads the catalog name from the marketplace cache, which
only exists for remote sources, so it would have stayed silent precisely in the local
case where the collision happens. A rule that lies by omission is worse than no rule.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011x4ms5qcGuZgYhCxfdHMUb

* test(cli): stop the golden from depending on what has been released

The golden failed on a warning nobody wrote into it: "CLI update available: v5.2.1 →
v5.2.2". The update check asks GitH…
…ocess environment (#802)

`plugin install --token` and `marketplace add --token` hand the flag to `createDeps`, which
composes it into the auth reader every fetcher consults, ahead of `AIDD_TOKEN` and the stored
credentials. Nothing writes the environment any more: the install use case loses its
`Environment` and the port loses `set`. Two reads of the user's own `AIDD_TOKEN` remain, both
in `runtime/auth`, both reads.

Red first: the auth reader's explicit-token test failed with `expected 'env-token' to be
'flag-token'`, the marketplace wiring test with `expected "spy" to be called with arguments`.

Closes #797


Claude-Session: https://claude.ai/code/session_011x4ms5qcGuZgYhCxfdHMUb
AIDD-Session-Id: 4acc9a1c-19bc-4468-b8b6-e86644bcba60

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
… Claude Code does not warn (#803)

`claude plugin validate` warned `Unknown field 'recommended'` on our source catalog and on
every claude build, and CI now runs that validation on each pull request. Measured against
Claude Code's validator: a plugin entry's `metadata` object is the one place a catalog may
carry its own field without a warning, while `category`, `tags` and `keywords` are official
and mean something else. The field moves there: the source catalog, the fixtures, the schema,
the repository validator, the parser (which reads no other spelling) and the claude-style
catalog every build writes. The nine-cell golden is re-baselined for claude and cursor, whose
catalogs now carry `metadata.recommended`.

Red first: the parser test failed with `expected false to be true`, the catalog builder test
with `expected undefined to strictly equal { recommended: false }`.

Closes #800


Claude-Session: https://claude.ai/code/session_011x4ms5qcGuZgYhCxfdHMUb
AIDD-Session-Id: 4acc9a1c-19bc-4468-b8b6-e86644bcba60

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
* test(cli): kill the surviving mutants of the tool capabilities, formats and registry

Tests written for the survivors stryker named in the tools scope's domain
layer: the mcp json-to-toml transform and user-prime merge, the command
frontmatter and path builders, the agents, rules, skills, commands, settings,
hooks and plugins capabilities, the registry's signal scan and activation
queries, the flat hooks merges, the marketplace catalog builders, the host
registration answers and the marketplace source conflict. Every test was
watched red under a hand-applied mutation before the source was restored.

Scope measured 80.2 before this pass (1358 killed, 67 timed out, 232 survived,
120 uncovered of 1777) and 96.8 after this lot and the infrastructure lot
(1687 killed, 34 timed out, 50 survived, 6 uncovered).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011x4ms5qcGuZgYhCxfdHMUb
AIDD-Session-Id: 4acc9a1c-19bc-4468-b8b6-e86644bcba60

* test(cli): kill the surviving mutants of the tool host readers and plugin CLI adapter

Tests written for the survivors stryker named in the tools scope's
infrastructure: the plugin CLI adapter's marketplace removal, source check,
declared verbs, failure messages and batch-shim spawn; the PATH lookup's
empty segments, quoting and host description; Claude's, Codex's and
Copilot's plugin registries at the edges of their shapes; and Claude's
marketplace registry on documents that are not objects, entries without an
install location and a dead path. Every test was watched red under a
hand-applied mutation before the source was restored.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011x4ms5qcGuZgYhCxfdHMUb
AIDD-Session-Id: 4acc9a1c-19bc-4468-b8b6-e86644bcba60

* test(cli): raise the tools mutation floor to 94

Measured 96.8 under run-mutation.mjs tools --force after the two test lots;
the floor sits two under the measurement, as the other scopes' floors do.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011x4ms5qcGuZgYhCxfdHMUb
AIDD-Session-Id: 4acc9a1c-19bc-4468-b8b6-e86644bcba60

* test(cli): build the expected signal paths with the platform separator

hasToolSignals joins its answers with node's own path.join, so on Windows the
registry test compared a backslash path against a slash literal and failed
there alone. The expected list is now built the same way the source builds
it; the intention of the test is unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011x4ms5qcGuZgYhCxfdHMUb
AIDD-Session-Id: 4acc9a1c-19bc-4468-b8b6-e86644bcba60

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…#808)

The coverage job's `apt-get update` fails a few times a day on the Google Chrome source the
runner image ships, whose mirror serves a stale index ("Hash Sum mismatch"), and the whole
job goes red before a test runs; it hit #804 and #807 within an hour. `expect` comes from
ubuntu's own archive, so the chrome source is removed before the update.


Claude-Session: https://claude.ai/code/session_011x4ms5qcGuZgYhCxfdHMUb
AIDD-Session-Id: 4acc9a1c-19bc-4468-b8b6-e86644bcba60

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
* test(cli): add the test doubles the framework survivors need

Recording, choosing and checkbox prompters, a faulting file adapter, a stub asset provider and a stub AI tool, plus a save counter on the in-memory manifest repository and select and input recorders on the scripted prompter. Every double is consumed by the tests in the following commits.

Framework mutation score before this series: 72.2.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011x4ms5qcGuZgYhCxfdHMUb
AIDD-Session-Id: 4acc9a1c-19bc-4468-b8b6-e86644bcba60

* test(cli): kill the surviving mutants of the framework domain

Manifest, tool entries, tracked files, native registrations, mcp exclusions, serialization, setup flow, config capability, project context, tool recommendations, installed plugins and rules, markdown references, install scope, marketplace source drift and the plugin source resolver: 91 tests, each shown red first against the mutant it names.

Framework mutation score: 72.2 before the series, 95.4 after it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011x4ms5qcGuZgYhCxfdHMUb
AIDD-Session-Id: 4acc9a1c-19bc-4468-b8b6-e86644bcba60

* test(cli): kill the surviving mutants of the framework adapters

Plugin distribution reader, user source references, manifest and user manifest repositories, manifest file io and the environment adapter: 34 tests, each shown red first against the mutant it names.

Framework mutation score: 72.2 before the series, 95.4 after it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011x4ms5qcGuZgYhCxfdHMUb
AIDD-Session-Id: 4acc9a1c-19bc-4468-b8b6-e86644bcba60

* test(cli): kill the surviving mutants of the install and setup use cases

Runtime and IDE config install, AI and IDE tool install, config install, content sections, gitignore, init, setup and its tools, machine scope, marketplace source and project context steps, update decisions and one-tool update: 63 tests, each shown red first against the mutant it names.

Framework mutation score: 72.2 before the series, 95.4 after it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011x4ms5qcGuZgYhCxfdHMUb
AIDD-Session-Id: 4acc9a1c-19bc-4468-b8b6-e86644bcba60

* test(cli): kill the surviving mutants of the plugin use cases

Plugin add, install, install from marketplace, remove, update, search, target resolution and helpers: 68 tests, each shown red first against the mutant it names.

Framework mutation score: 72.2 before the series, 95.4 after it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011x4ms5qcGuZgYhCxfdHMUb
AIDD-Session-Id: 4acc9a1c-19bc-4468-b8b6-e86644bcba60

* test(cli): kill the surviving mutants of the clean use cases

Clean and clean user scope, including the shared reference guard: 61 tests, each shown red first against the mutant it names.

Framework mutation score: 72.2 before the series, 95.4 after it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011x4ms5qcGuZgYhCxfdHMUb
AIDD-Session-Id: 4acc9a1c-19bc-4468-b8b6-e86644bcba60

* test(cli): kill the surviving mutants of the status and doctor use cases

Status, status all, doctor, doctor all, registration, references, tracked files, merge files, layout and plugin checks: 65 tests, each shown red first against the mutant it names.

Framework mutation score: 72.2 before the series, 95.4 after it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011x4ms5qcGuZgYhCxfdHMUb
AIDD-Session-Id: 4acc9a1c-19bc-4468-b8b6-e86644bcba60

* test(cli): kill the surviving mutants of the restore and uninstall use cases

Restore, restore all, restore all plugins, tool files, merge and regular files, restore decisions, tool distribution generation, uninstall, uninstall tools, ide, plugin and mcp exclusion: 76 tests, each shown red first against the mutant it names. The mcp exclusion use case had no test before.

Framework mutation score: 72.2 before the series, 95.4 after it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011x4ms5qcGuZgYhCxfdHMUb
AIDD-Session-Id: 4acc9a1c-19bc-4468-b8b6-e86644bcba60

* test(cli): kill the surviving mutants of the marketplace flows

Marketplace sync settings, remove and check: 60 tests, each shown red first against the mutant it names.

Framework mutation score: 72.2 before the series, 95.4 after it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011x4ms5qcGuZgYhCxfdHMUb
AIDD-Session-Id: 4acc9a1c-19bc-4468-b8b6-e86644bcba60

* test(cli): kill the surviving mutants of the translators and shared steps

Built tree, flat and marketplace translators, project hooks materializer, ensure built marketplace, catalog identity, marketplace registration, project hooks removal, user scope files, cache purges, shared source references, plugin drift, plugin files and native calls: 66 tests, each shown red first against the mutant it names.

Framework mutation score: 72.2 before the series, 95.4 after it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011x4ms5qcGuZgYhCxfdHMUb
AIDD-Session-Id: 4acc9a1c-19bc-4468-b8b6-e86644bcba60

* test(cli): raise the framework mutation floor to 93

Measured 95.4 after the survivor series (5388 killed, 225 survived, 36 uncovered of 5649), up from 72.2 (4029 killed, 50 timed out, 1186 survived, 384 uncovered). The floor is the measured score minus two.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011x4ms5qcGuZgYhCxfdHMUb
AIDD-Session-Id: 4acc9a1c-19bc-4468-b8b6-e86644bcba60

* test(cli): make two framework survivor tests separator-agnostic

The in-memory file adapter normalises every key to forward slashes, so a listing is compared to the normalised form rather than to a platform join. The user source references adapter joins its own path, so the expected error messages are built with the same join and the unparsable-JSON reason is matched as a substring rather than a slash-specific regex. Both failed only on Windows.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011x4ms5qcGuZgYhCxfdHMUb
AIDD-Session-Id: 4acc9a1c-19bc-4468-b8b6-e86644bcba60

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
@aidd-bot
aidd-bot Bot requested a review from a team as a code owner September 9, 2026 19:32
@aidd-bot
aidd-bot Bot enabled auto-merge September 9, 2026 19:32
@aidd-bot
aidd-bot Bot merged commit c523368 into main Sep 9, 2026
64 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants