feat(cli): one shared framework source per machine, user scope and session telemetry - #795
Merged
Conversation
`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
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
…egimes 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
`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
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
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
`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
`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
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
`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
`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
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
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
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
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
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
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
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
…rror
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
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
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
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
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
… 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
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
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
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
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
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
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
framework, marketplace, plugin, auth, update, translate and the menu, in the same shape as the four before: the command wires, the display prints, each display proven line by line. The banner drops the newline output.print already adds, and the menu builds its output before printing it. A refusal decided before any use case, and its exit, stays in the command, as the process rule allows. 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
…he prompts The telemetry command already only wired; an integration test drives it through commander with the graph substituted and reads the bytes it writes. Every survivor the report listed in the cost-report, telemetry and prompt modules gets the case that observes it: the exact line, the other branch, the empty list. The ones left are equivalent mutants, each named in the test file with why. 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
blafourcade
force-pushed
the
refactor/telemetry-into-contexts
branch
from
September 9, 2026 08:46
9dafc35 to
78981b3
Compare
Measured 74.0 after the three display lots, from 30.3 before them, on a forced run of every mutant. 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
9 tasks
Stryker reuses an incremental result unless the mutant's file or a test that covered it changed. A test written after the fact covers nothing the file knew about, so a mutant recorded as survived, uncovered or static kept its old verdict: presentation measured 74.0 on a full run and 67.5 in CI against the previous run's file, under its floor. The runner now prunes the file to kills before each run; the pruned local run reads 74.0 in 49 seconds. 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
A check followed by a read is a race CodeQL names; reading and treating absence as absence is not. 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
The workflows are markdown and need nothing; Node 22 or later is for the plugin that ships hooks, and the engines field is the one place the number lives. 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
…m a built tree strict is a marketplace entry field: whether the plugin's own manifest is authoritative. A plugin added with catalog metadata recorded it only on the metadata path; the built-tree path read it off plugin.json, which has no such field, and so wrote false whatever the catalog said. The distribution now carries the catalog's value, and the reader no longer invents one from the plugin manifest. 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
The fixture marketplace declares strict on its entries; the manifest now records it, so eight entries read true where the baseline held false. Nothing else in the snapshot moved. 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
A test that only checks for a describe title proves nothing about the probe inside it; the convention stays in coding-assertions.md. biome's noExportedImports and noTsIgnore now hold what a script and a ratchet held on their own; the one exported import, the application tests' alias of the silent prompter, becomes a binding of its own. 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
Domain and application no longer reach `process`. The two use cases that read a switch or published a token take an `Environment` port, the report use case warns through its logger, and a profile's `userSettingsPath` receives an environment reader from whoever composes the call, so codex's `CODEX_HOME` lookup needs no exclusion. Biome's `noRestrictedGlobals` denies `process` under every context's domain and application, and the guard test plants a file to prove it bites. Every moved site was watched red against its new test first. 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
One build test per profile calls the build contract's own functions with a minimal input and asserts the whole structure, `toStrictEqual` so an `undefined` key can no longer hide. Every test was watched red against the mutation it names before the source was restored. Measured with `run-mutation.mjs <scope> --force`, floors raised to the score minus two: cursor 51.3 -> 96.4 (floor 94), codex 55.8 -> 89.5 (87), claude 58.5 -> 94.5 (92), copilot 65.3 -> 82.2 (80), opencode 67.5 -> 83.0 (81). No source file changed. 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
Exact installed-path lists and whole `{path: content}` trees per target, a contract stated in
the test to reach the branches no shipped profile does, and the marketplace strategy, the
source-tree reader and the skill-tree writer each get their own suite. Every test was watched
red against the transformation it names before the source was restored.
Measured with `run-mutation.mjs translate`: 68.0 -> 87.0, floor 66 -> 85. No source changed.
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
One integration test per command drives commander with the runtime wiring mocked at `createDeps`, so the eleven command modules that no test reached are now covered branch by branch. A `Set` inside `expect.objectContaining` compares nothing, which let a narrowed tool set survive; the assertion reads the call argument directly. Measured with `run-mutation.mjs presentation`: 74.0 -> 97.3, floor 72 -> 95. No source changed. 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
`smoke:real` now reads the fixture plugin's skills, hook events, MCP servers and version and asks each host's own binary for them: `claude plugin details`, `codex plugin list`, `copilot plugin list`. A registry naming the plugin proved a path was recorded; a host listing its components proves it loaded the build. Cursor and opencode expose no inventory command, and Claude counts no agents, so neither is asserted. CI adds Claude Code's own `plugin validate` over a fresh claude build. The binary exits 0 whether validation passed or failed, so `check-claude-accepts-build.cjs` reads the verdict from its text, exit codes as the identifier probe's, and its test drives it with a fake host. 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
…runs it The test's `claude` stand-in was a `#!/bin/sh` file, which Windows cannot spawn, and `cli / Windows` refused the plugin suite. The host is now an argv, so the fake is a node script started through `process.execPath` on every platform. 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
…ndows agrees
The in-memory file adapter keys every write in posix form, while the two stub roots came
from `resolve("/tmp/…")`, backslashed on Windows, and the expectations concatenated on them.
The roots are posix now, and the one path the catalog receives as the strategy joins it is
joined the same way. `cli / Windows` refused fifteen tests for this.
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
This was referenced Sep 9, 2026
fix(cli): carry the catalog's recommended field under metadata, where Claude Code does not warn
#803
Merged
Merged
1 task
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🎯 What & why
The
aiddCLI reorganised by bounded context, with session telemetry folded in as its own context, and the framework installed from one shared source per machine instead of one copy per project. Before this, claude, codex and copilot disagreed on a second project on the same machine (codex and copilot refuse a second source under the same name, claude silently repoints the whole machine),cleanin one project could disable plugins for every other project, and nothing measured what a session cost.🛠️ How it works
cli/src/contexts/(tools,translate,distribution,framework,telemetry), a kernel, presentation and runtime; the allowed edges, the no-reach-inside rule, public-module surface, folder size and type honesty are ratchets undercli/tests/architecture/andscripts/check-cli-type-honesty.mjs.aidd-frameworkmarketplace is registered once per machine atuserConfigDir()/marketplaces.json, built once per CLI version undercache/built/<version>/, andreferences.jsonrecords which projects claim it.setup,doctor,syncandcleantake--scope user;syncmigrates a pre-existing per-project install;cleannever removes a shared registration and leaves a machine-global plugin enabled while another project still needs it;clean --scope userpurges the machine-scope state behind a realpath-contained whitelist.aidd telemetry on | off | read | report | check | forget | identitymeasure a session from the files each tool already writes, stored per machine, nothing uploaded; aprepare-commit-msgdelegate ties cost to commits, and lefthook or husky owners get the job to add printed instead of a silent no-op.marketplace addandplugin install --fromactivate only the marketplace they acted on.🧪 How to verify
cd cli && pnpm typecheck && pnpm lint && pnpm test:arch && pnpm knip && pnpm jscpd && pnpm build && pnpm test— 372 files / 3893 tests, bundle 639.6 KB under the 641 KB budget.cd cli && pnpm smoke— 33/33 leaf commands, hermetic.cd cli && pnpm smoke:realon a machine with claude, codex and copilot on PATH — 88 checks against the hosts' own registries,HOMEreal,AIDD_USER_CONFIG_DIRrelocated; leaves the registries byte-identical apart from claude's ownlastUpdated.pnpm test:coverage— 95.5 % lines / 90.2 % branches against the configured thresholds.node scripts/check-tests-leave-git-alone.js -- node --test 'scripts/__tests__/*.test.js'— 400 tests.scopeper plugin,nativeRegistrationsper tool). An older manifest is refused with the repair named, no migration chain.build-per-toolused to call@ai-driven-dev/cli@5.1.1, four minors behind; it now builds the CLI from the released checkout and runstranslate. Measured across the nine cells against the old build: cursor gains the telemetry hooks in.cursor/hooks.json, codex's hook table ishooks(wascodex_hooks), opencode's skill tree is nested per plugin, claude'splugin.jsonno longer carries ahookskey the source never declared, and codexSKILL.mdfrontmatter keeps onlyname,description,allowed_tools. This belongs in the release note.mainandnextwere applied from.github/rulesets/*.jsonduring this PR:cli / gateis now a required check. It was named in the files but never applied, which is how two red PRs merged before.http-clientfetches a marketplace URL read from config) and one pre-existing finding in a frozen fixture, both to be dismissed in the Security tab by a maintainer with the scope.🔗 Linked issue
None; scoped and reviewed in-session, roadmap board to follow.
✅ I certify
🤖 Generated with Claude Code
https://claude.ai/code/session_011x4ms5qcGuZgYhCxfdHMUb