docs(migration): ratify polyrepo-migration brief D1-D8

Flip status DRAFT -> RATIFIED. All eight decision blocks ratified as
recommended on 2026-06-17:
- D1 bare slug open/<plugin>
- D2 filter-repo preserve on a fresh clone + pre-polyrepo-archive tag
- D3 own DS repo, consumers keep vendoring
- D4 tag-pin (ref vX.Y.Z)
- D5 DS first -> high-churn -> low-churn -> thin catalog last
- D6 catalog keeps json/README/GOVERNANCE/conventions; shared+sync leave
- D7 per-repo .gitignore re-rooted; local state stays uncommitted
- D8 no push until one authorized weekend window

Next: /trekplan --brief in a fresh session at repo root.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-06-17 11:02:09 +02:00
commit f2d41c8781

View file

@ -0,0 +1,302 @@
# Marketplace Polyrepo Migration · Brief
> **Voyage `/trekbrief`-style task brief.** Foundation document for splitting the
> `ktg-plugin-marketplace` monorepo into **one git repo per plugin + a thin catalog repo**.
> This brief defines *what the migration is and how we verify it* — it does **not** execute it.
> Decisions are surfaced as ratify-by-annotation `DECISION` blocks.
>
> - **Written:** 2026-06-17
> - **Status:** RATIFIED 2026-06-17 — all `DECISION` blocks D1D8 ratified as recommended (see each block). Next: run `/trekplan` in a fresh session at the repo root.
> - **Workstream:** Infrastructure (marketplace-wide, root-scoped). This is the first root-level workstream; all prior briefs were plugin-scoped.
> - **Grounded by:** (1) `.claude-plugin/marketplace.json` read; (2) official Claude Code marketplace docs verified via `claude-code-guide` agent ([plugin-marketplaces](https://code.claude.com/docs/en/plugin-marketplaces.md), [plugins-reference](https://code.claude.com/docs/en/plugins-reference.md)); (3) a read-only coupling inventory (Explore) of root infra + cross-references + per-plugin standalone-readiness, all on 2026-06-17. Counts/paths below are tool-verified, not recalled.
---
## 1. Context & motivation
The marketplace is a **monorepo**: 10 plugins under `plugins/`, a shared design system under `shared/`,
and a catalog (`marketplace.json` + landing `README.md`) at the root. Two findings make the monorepo the
wrong fit for how this repo is actually developed:
1. **A Claude Code marketplace does NOT require a monorepo.** Verified against official docs: a plugin entry's
`source` can be an external git repo (`github` / `url` / `git-subdir` / `npm`), a single `marketplace.json`
can **mix** in-repo and external sources, the "thin catalog that points to many plugin repos" is an
**officially supported pattern**, and each entry can **pin** to a `ref` (tag/branch) or `sha`. The monorepo
is a *packaging convenience, not an architectural necessity* (Explore verdict).
2. **The operator works on multiple plugins concurrently as the permanent norm, and plugin count is growing.**
10 plugins today (up from the ~8 the root CLAUDE.md still documents — `human-friendly-style` and
`claude-design` are new). A single working tree shares **one `.git/index` and one `HEAD`**, so concurrent
sessions race on global mutable git-state. Observed symptoms: cross-plugin commit bleed
(`f460814` "WIP across plugins", `69610d4` "roll up in-progress changes across plugins"); root-`README.md`
merge pressure (the three-doc gate forces *every* feature to touch the root README); noisy `git status`.
**The cost grows with each new plugin.**
The plugins are already **independent units in a monorepo costume**: each versions independently
(v1.15.0 / v4.1.0 / v7.7.2 …), each is self-contained (own `plugin.json` / `README` / `CLAUDE.md`), the design
system is **vendored, not live-linked**, and there are **no cross-plugin runtime imports**. Splitting aligns the
structure with that reality and removes the concurrency pain structurally rather than mitigating it.
---
## 2. Problem statement (grounded)
**Inventory (tool-verified 2026-06-17):**
| Surface | Finding |
|---|---|
| Plugins | **10**, `marketplace.json``plugins/` dirs are a **perfect match** (no drift) |
| Forgejo remote | `ssh://git@git.fromaitochitta.com/open/ktg-plugin-marketplace.git` (org `open/`) |
| Root catalog | `.claude-plugin/marketplace.json` (all 10 `source: "./plugins/<name>"`) |
| Root shared infra | `README.md` (landing, hand-maintained version badges), `GOVERNANCE.md`, `CLAUDE.md` (marketplace-wide conventions + a stale plugin overview), `.gitignore`, `.gitleaks.toml`, `.gitleaksignore`, `.mailmap`, `STATE.md` (gitignored) |
| Shared code | `shared/playground-design-system/` + `shared/playground-examples/` + `shared/PLAYGROUND-MAINTENANCE.md`; `scripts/sync-design-system.mjs` (the **only** root script) |
| Local state | 13 gitignored `STATE.md` / `*.local.md` files across plugins |
| Root CI/cron | **NONE** (no `.github/`, no `.forgejo/` at root; `voyage` has its own `.forgejo/`) |
**The coupling is minimal and resolvable — there are no live cross-plugin imports.** The only break-points a
split must address:
| # | Break-point | Detail |
|---|---|---|
| C1 | DS sync reaches the monorepo root | `scripts/sync-design-system.mjs:26-27` resolves `MARKETPLACE_ROOT/shared/playground-design-system` via `path.resolve(__dirname, '..')`; vendors into `plugins/<name>/playground/vendor/`. Runtime is fully decoupled — plugins load CSS from their own `vendor/`, **never** from `shared/`. Sync is a dev-time tool only. |
| C2 | `marketplace.json` source paths | All 10 are `./plugins/<name>`; must become external repo URLs. |
| C3 | `.gitignore` / `.gitleaks.*` per-plugin rewrite | Root `.gitignore` uses `plugins/*/` prefixes (e.g. `plugins/*/.claude/`, `plugins/*/reports/*-beskrivelse.*`) that must be re-rooted per plugin; `.gitleaks.toml`/`.gitleaksignore` carry per-plugin false-positive paths. |
| C4 | Root README version badges | Hand-maintained (`llm-security v7.7.2`, etc.); become cross-repo references in the thin catalog. |
| C5 | `shared/` relocation | DS + examples need a home that isn't a plugin and isn't the catalog's `plugins/`. |
| C6 | `linkedin-studio` storage resolver | `scripts/analytics/src/utils/storage.ts:18-33,48` — marker-based `findPluginRoot` (walks up to `.claude-plugin/plugin.json`) with a hardcoded `../../../../` **fallback**. Primary path is resilient across the repo boundary; the fallback must be re-checked at the new repo root. |
| C7 | `voyage` test helper | `tests/helpers/hook-helper.mjs:4` is a **verbatim copy** of llm-security's, with a provenance comment — not a live import; stays as a copy. |
---
## 3. Goal (definition of done)
**Each of the 10 plugins lives in its own Forgejo repo; the design system lives in its own versioned repo;
and `ktg-plugin-marketplace` becomes a thin catalog repo at the same URL users already added — installable and
live at every step of the migration, with no plugin functionality or version changed.**
After the migration:
- Each plugin is its own repo with its own history (per D2), `.gitignore`, `.gitleaks.*`, and continuity
(`STATE.md` + session hooks).
- The catalog repo has **no `plugins/` subdir** and **no `shared/`** — only `marketplace.json` (external
sources), the landing `README.md`, `GOVERNANCE.md`, and marketplace-wide conventions.
- `/plugin marketplace add <same URL>` + `/plugin install <plugin>@ktg-plugin-marketplace` works for all 10.
- Concurrent multi-plugin work is **structurally isolated** — separate repos, separate `index`/`HEAD`, zero
shared-state contention.
- The design system has a single source repo; consumers vendor from it via the (re-homed) sync script.
---
## 4. Success criteria (testable)
> Each is a concrete check; the planning phase turns these into commands.
- **SC1 — Install works post-migration.** `/plugin marketplace add ssh://…/open/ktg-plugin-marketplace.git`
then `/plugin install <plugin>@ktg-plugin-marketplace` succeeds for all 10. Verify: fresh-session install
smoke test per plugin.
- **SC2 — Each plugin repo is standalone.** A clean clone of each new repo (no marketplace parent) passes that
plugin's own validation/test suite. Verify: clone to `/tmp`, run its `tests/`/`validate`/`test-runner`.
- **SC3 — Catalog resolves externally at every step.** No `./plugins/…` source remains at completion; every
*intermediate* mixed-source state also resolves. Verify: `marketplace.json` schema check + install smoke
after each plugin graduates.
- **SC4 — Catalog is thin.** After completion the catalog repo contains **no** `plugins/` and **no** `shared/`.
Verify: `ls`.
- **SC5 — Design-system parity.** A DS change in the DS repo + re-sync produces byte-identical vendored output
in a consumer (sync script rewired to the DS-repo source; `MANIFEST.json` SHA-256 matches). Verify: run sync,
diff manifest.
- **SC6 — History per D2.** Each new repo's `git log` reflects the ratified history decision (filter-repo
preserved, or hard-cut with the pre-migration monorepo archived). Verify: `git log` per repo.
- **SC7 — Continuity intact.** Each plugin repo's `.gitignore` (prefix re-rooted) ignores its local state; a
session started in the repo injects *its* `STATE.md`; hooks run via `${CLAUDE_PLUGIN_ROOT}`. Verify: session
start in each repo.
- **SC8 — Marketplace never goes dark.** Install works at **every** intermediate commit (mixed sources). Verify:
install smoke after each graduation.
---
## 5. Non-goals (scope fence)
- **NOT changing plugin code, functionality, or versions.** Pure relocation + rewiring.
- **NOT changing the marketplace URL.** `open/ktg-plugin-marketplace` stays — existing user installs keep their
update path.
- **NOT GitHub.** Forgejo only (`git.fromaitochitta.com`); never `gh` CLI.
- **NOT touching content repos** (`~/repos/from-ai-to-chitta/…`).
- **NOT building new CI.** None exists at root; per-plugin CI (`voyage/.forgejo/`) moves with its plugin
unchanged.
- **NOT merging, splitting, or renaming plugin functionality.**
- **NOT a design-system redesign**`shared/` is relocated, not rewritten.
---
## 6. Target architecture
```
open/ktg-plugin-marketplace ← THIN CATALOG (same URL)
.claude-plugin/marketplace.json → external source + ref-pin per plugin
README.md GOVERNANCE.md CONVENTIONS .mailmap .gitleaks (baseline)
open/playground-design-system ← DS repo (versioned)
playground-design-system/ playground-examples/ PLAYGROUND-MAINTENANCE.md
scripts/sync-design-system.mjs (re-homed; --source points here)
open/<plugin> × 10 ← one repo each
.claude-plugin/plugin.json README CLAUDE CHANGELOG
.gitignore (re-rooted) .gitleaks.* (per-plugin)
hooks/ tests/ scripts/ playground/ vendor/playground-design-system/ …
```
`marketplace.json` entry shape (per D4 pinning):
```json
{ "name": "linkedin-studio",
"source": { "source": "url", "url": "ssh://git@git.fromaitochitta.com/open/linkedin-studio.git", "ref": "v0.4.0" },
"description": "…" }
```
---
## 7. Decisions (ratify by annotation)
> Each block: **recommendation** + rationale + alternatives. **All eight ratified as recommended on 2026-06-17** — D1, D2, D4, D8 via explicit operator selection; D3, D5, D6, D7 ratified-as-recommended.
### D1 — Forgejo org + repo naming
**✓ Ratified 2026-06-17 — bare plugin slug under `open/` (`open/<plugin>`).**
**Recommendation:** Org `open/` (matches the catalog); repo name = the **bare plugin slug** (`open/linkedin-studio`,
`open/ms-ai-architect`, …). *Rationale:* the marketplace already namespaces; a `ktg-`/`plugin-` prefix is
redundant. *Alternatives:* (a) prefix `open/ktg-<slug>`; (b) a dedicated org (e.g. `plugins/`).
### D2 — History preservation
**✓ Ratified 2026-06-17 — preserve per-plugin history via `git filter-repo` on a fresh clone; tag the pre-migration monorepo `pre-polyrepo-archive` regardless.**
**Recommendation:** Preserve per-plugin history with `git filter-repo --path plugins/<name>/ --path-rename plugins/<name>/:`
run on a **fresh clone** (never the working repo), keeping blame intact. Regardless of choice, tag the
pre-migration monorepo (`pre-polyrepo-archive`) and keep it as a read-only archive. *Alternatives:* (a) **hard-cut**
`git init` a new repo from the current tree, lose granular history but keep it in the archived monorepo (fast);
(b) hybrid — filter-repo the high-value plugins, hard-cut the rest. *This decision is the single biggest effort lever.*
### D3 — Design-system distribution
**✓ Ratified 2026-06-17 (as recommended) — DS becomes `open/playground-design-system`; consumers keep vendoring; `sync-design-system.mjs` re-homed with a `--source`/env pointer.**
**Recommendation:** DS becomes its own repo `open/playground-design-system`; `sync-design-system.mjs` moves there
(or to the catalog) and takes a `--source`/env pointing at the DS checkout; **consumers keep vendoring** (runtime
model unchanged — plugins load from `vendor/`). Pin the DS version per consumer in the vendored `MANIFEST.json`.
*Rationale:* single source of truth, minimal change to a proven sync. *Alternatives:* (a) `git-subdir` source in
each plugin (rejected — DS isn't a plugin); (b) npm package (adds a publish step); (c) git submodule per plugin
(rejected — submodule friction; vendoring already works). **Note for planning:** only ~5 plugins consume DS and
only **2** (`ms-ai-architect`, `llm-security`) currently have a `vendor/` dir — enumerate true consumers first.
### D4 — Marketplace pinning strategy
**✓ Ratified 2026-06-17 — pin each plugin to a release tag (`ref: "vX.Y.Z"`).**
**Recommendation:** Pin each plugin to a release **tag** (`ref: "vX.Y.Z"`) — reproducible installs, explicit
upgrades (bump the tag in `marketplace.json` to release). *Alternative:* floating (track default branch — simpler,
but installs move under users without a catalog change).
### D5 — Migration order + live strategy
**✓ Ratified 2026-06-17 (as recommended) — DS repo first → high-churn plugins → low-churn → thin the catalog last; mixed sources keep the marketplace live throughout.**
**Recommendation:** (0) Stand up the **DS repo first** (it's the spine 5 plugins vendor from). (1) Graduate
**high-churn** plugins first — `linkedin-studio`, `voyage`, `ms-ai-architect`, `llm-security` — each as one
reversible unit: create repo → push → flip its `marketplace.json` entry to external. (2) Mixed sources keep the
marketplace live throughout. (3) Low-churn plugins next (`okr`, `config-audit`, `graceful-handoff`,
`ai-psychosis`, `human-friendly-style`, `claude-design`). (4) **Thin the catalog last** — remove `plugins/` and
`shared/`, rewrite the landing README. *Alternative:* big-bang all-at-once (rejected — no reversible checkpoints,
marketplace dark during the cutover).
### D6 — What stays in the catalog repo
**✓ Ratified 2026-06-17 (as recommended) — catalog keeps `marketplace.json`, landing README, `GOVERNANCE.md`, `.mailmap`, a `.gitleaks` baseline, and the extracted marketplace-wide conventions; `shared/` + `sync-design-system.mjs` leave to the DS repo.**
**Recommendation:** `marketplace.json`, landing `README.md`, `GOVERNANCE.md`, `.mailmap`, a shared `.gitleaks`
baseline, and the **marketplace-wide conventions** extracted from the root `CLAUDE.md` (move them to `GOVERNANCE.md`
or a new `CONVENTIONS.md`). `shared/` + `scripts/sync-design-system.mjs` leave to the DS repo. The catalog
`CLAUDE.md` becomes thin (catalog-maintenance only). *Alternative:* keep `shared/` in the catalog and skip a
separate DS repo (rejected — re-creates a mini-monorepo and the cross-cutting README pressure).
### D7 — Gitignored state + continuity
**✓ Ratified 2026-06-17 (as recommended) — per-repo `.gitignore` with `plugins/*/` prefixes stripped; the 13 local state files stay uncommitted; `STATE.md` regenerates per repo; zero hook changes.**
**Recommendation:** Each new repo gets its own `.gitignore` with the `plugins/*/` prefixes stripped
(`.claude/`, `reports/*-beskrivelse.*`, `STATE.md`, `*.local.md`, OS files). The 13 local state files stay
**local and uncommitted**; `STATE.md` regenerates per repo (it is untracked, so `filter-repo`/clone won't carry
it — that is fine). The global `~/.claude` session-start hook already injects the nearest `STATE.md` from cwd, so
continuity works in any repo with zero hook changes. *Alternative:* seed an empty `STATE.md` template per repo.
### D8 — Push timing + irreversibility gate
**✓ Ratified 2026-06-17 — all extraction + validation stays local with NO push; creating Forgejo repos + the first push + the live `marketplace.json` rewrite are batched into one explicitly-authorized window (the weekend).**
**Recommendation:** All extraction + local validation happens with **NO push** (honoring "ikke push før helgen").
The outward, irreversible steps — **creating Forgejo repos, the first push, and rewriting the live
`marketplace.json`** — are **batched into one explicitly-authorized window** (the weekend). Forgejo repo creation
is operator-run (API/UI). *Rationale:* the marketplace is live; half-pushed external sources would break installs.
*Alternative:* graduate-and-push incrementally during the week (rejected — violates push policy + risks a
half-migrated public marketplace).
---
## 8. Risks
- **R1 — Marketplace goes dark mid-migration.** *Mitigation:* mixed sources (D5) + install smoke after each step
(SC8); never remove a `./plugins/x` source before its external repo is pushed and verified.
- **R2 — History loss.** *Mitigation:* `filter-repo` on a **clone** (D2); archive the pre-migration monorepo as a
tag/read-only repo regardless of D2.
- **R3 — DS drift across consumers.** *Mitigation:* single DS repo (D3); `MANIFEST.json` SHA verification (SC5).
- **R4 — Concurrent-session corruption during destructive git surgery.** The thinning + any history rewrite mutate
the working tree/`.git`; a second session committing there races on the shared index. *Mitigation:* run the
migration **from a dedicated fresh clone**, and quiesce other writers in the original checkout during the
destructive phase (see §10 + the session-hygiene answer).
- **R5 — `linkedin-studio` resolver fallback misfires.** The `../../../../` fallback in `storage.ts:48` assumes
the monorepo depth. *Mitigation:* verify the marker-based primary path finds `.claude-plugin/plugin.json` at the
new repo root (it will — the manifest sits at the standalone repo root); add a regression test.
- **R6 — Forgejo repo-creation irreversibility.** *Mitigation:* operator-gated, batched, one authorized window (D8).
- **R7 — `.gitignore`/`.gitleaks` path-rewrite errors.** *Mitigation:* generate from a template; verify per repo
(a committed local-state file is a failure).
- **R8 — Root README version badges go stale.** *Mitigation:* the thin catalog README references plugin repos;
a version-fetch helper is noted but out of scope.
---
## 9. Open questions for the planning phase
- Exact `filter-repo` invocation; whether to also preserve `shared/` history into the DS repo.
- True DS consumers — only 2 have `vendor/` dirs today vs 5 named; enumerate before standing up the DS repo, and
decide whether `claude-design` / `human-friendly-style` (no playground) need DS at all.
- Forgejo repo **visibility** per repo (the marketplace is under `open/` — confirm public vs private).
- Does `shared/playground-examples/` belong in the DS repo or stay referenced from the catalog?
- Per-repo `CHANGELOG` strategy (carry the plugin's existing history vs start fresh).
---
## 10. Execution model (for the planning phase)
- **Run from a dedicated fresh clone** of `ktg-plugin-marketplace`, not the operator's working checkout — so other
sessions' working trees are untouched and the destructive steps (filter-repo, thinning) can't race a concurrent
committer (R4).
- **Operator-run steps:** Forgejo repo creation (API/UI), the authorized push window (D8).
- **Claude-run steps:** filter-repo/extraction scripting, `.gitignore`/`.gitleaks` re-rooting, `marketplace.json`
rewrites, the DS-repo standup, validation per SC.
- **Reversible until push:** everything before D8's window is local and discardable; the migration is a sequence of
per-plugin units, each independently verifiable (SC2) and revertible.
---
## 11. Verification plan
1. **Per-plugin standalone:** clone each new repo to `/tmp`, run its own validate/test suite → **SC2**.
2. **Install smoke:** `/plugin marketplace add <URL>` + install each plugin in a fresh session, at the final state
**and** after each graduation → **SC1, SC3, SC8**.
3. **Catalog thinness:** `ls` the catalog — no `plugins/`, no `shared/`**SC4**.
4. **DS parity:** change a DS token in the DS repo, re-sync a consumer, diff `MANIFEST.json` SHA → **SC5**.
5. **History:** `git log` per new repo matches D2 → **SC6**.
6. **Continuity:** start a session in each repo; confirm its `STATE.md` injects and hooks run; `git status` shows
no committed local-state files → **SC7**.
---
## 12. References (tool-verified anchors, 2026-06-17)
- `.claude-plugin/marketplace.json` — 10 plugins, all `source: "./plugins/<name>"`.
- `scripts/sync-design-system.mjs:26-27``MARKETPLACE_ROOT` relative resolution + vendoring target.
- `plugins/linkedin-studio/scripts/analytics/src/utils/storage.ts:18-33,48` — marker-based `findPluginRoot` +
`../../../../` fallback.
- `plugins/voyage/tests/helpers/hook-helper.mjs:4` — verbatim-copy provenance comment.
- Root `.gitignore``plugins/*/` prefixed patterns to re-root.
- Forgejo remote: `ssh://git@git.fromaitochitta.com/open/ktg-plugin-marketplace.git`.
- Docs: [plugin-marketplaces](https://code.claude.com/docs/en/plugin-marketplaces.md),
[plugins-reference](https://code.claude.com/docs/en/plugins-reference.md) — external/mixed sources, thin catalog,
`ref`/`sha` pinning all officially supported.
---
_Counts and paths verified by direct codebase mapping + official-doc check on 2026-06-17, not recalled.
The root CLAUDE.md's plugin overview (~8 plugins) is stale — there are 10._