Skip to content

feat(cli): one shared framework source per machine, user scope and session telemetry - #795

Merged
blafourcade merged 179 commits into
nextfrom
refactor/telemetry-into-contexts
Sep 9, 2026
Merged

feat(cli): one shared framework source per machine, user scope and session telemetry#795
blafourcade merged 179 commits into
nextfrom
refactor/telemetry-into-contexts

Conversation

@blafourcade

@blafourcade blafourcade commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

🎯 What & why

The aidd CLI 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), clean in one project could disable plugins for every other project, and nothing measured what a session cost.

🛠️ How it works

  • Five contexts under 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 under cli/tests/architecture/ and scripts/check-cli-type-honesty.mjs.
  • The aidd-framework marketplace is registered once per machine at userConfigDir()/marketplaces.json, built once per CLI version under cache/built/<version>/, and references.json records which projects claim it. setup, doctor, sync and clean take --scope user; sync migrates a pre-existing per-project install; clean never removes a shared registration and leaves a machine-global plugin enabled while another project still needs it; clean --scope user purges the machine-scope state behind a realpath-contained whitelist.
  • Native activation drives each tool's own CLI at the right scope, refuses to overwrite a different catalog registered under the same name, and reclaims only the reserved framework name at codex and copilot.
  • aidd telemetry on | off | read | report | check | forget | identity measure a session from the files each tool already writes, stored per machine, nothing uploaded; a prepare-commit-msg delegate ties cost to commits, and lefthook or husky owners get the job to add printed instead of a silent no-op.
  • marketplace add and plugin install --from activate 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:real on a machine with claude, codex and copilot on PATH — 88 checks against the hosts' own registries, HOME real, AIDD_USER_CONFIG_DIR relocated; leaves the registries byte-identical apart from claude's own lastUpdated.
  • 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.

⚠️ Heads-up

  • About 145 commits, each green on its own gates; read by commit rather than by the whole diff.
  • The manifest moves to v8 (scope per plugin, nativeRegistrations per tool). An older manifest is refused with the repair named, no migration chain.
  • The nine release archives change shape. build-per-tool used to call @ai-driven-dev/cli@5.1.1, four minors behind; it now builds the CLI from the released checkout and runs translate. Measured across the nine cells against the old build: cursor gains the telemetry hooks in .cursor/hooks.json, codex's hook table is hooks (was codex_hooks), opencode's skill tree is nested per plugin, claude's plugin.json no longer carries a hooks key the source never declared, and codex SKILL.md frontmatter keeps only name, description, allowed_tools. This belongs in the release note.
  • The branch rulesets on main and next were applied from .github/rulesets/*.json during this PR: cli / gate is now a required check. It was named in the files but never applied, which is how two red PRs merged before.
  • CodeQL: 45 alerts on the first run, 43 fixed in code; the two left are one by-design flow (http-client fetches 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.
  • Windows was the one gate that could not be run locally; its failures (no git identity on runners, CRLF checkouts, POSIX literals in assertions, an install order) were fixed from the job logs.

🔗 Linked issue

None; scoped and reviewed in-session, roadmap board to follow.

✅ I certify

  • I DO CERTIFY I READ EACH LINE OF THE PULL REQUEST BECAUSE I AM A SOFTWARE ENGINEER, NOT A AI PUPPY.

🤖 Generated with Claude Code

https://claude.ai/code/session_011x4ms5qcGuZgYhCxfdHMUb

Test and others added 30 commits August 21, 2026 05:40
`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
blafourcade and others added 2 commits September 9, 2026 10:42
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
blafourcade force-pushed the refactor/telemetry-into-contexts branch from 9dafc35 to 78981b3 Compare September 9, 2026 08:46
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
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
Comment thread cli/scripts/run-mutation.mjs Fixed
blafourcade and others added 12 commits September 9, 2026 11:09
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
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.

2 participants