ktg-plugin-marketplace/docs/marketplace-polyrepo-migration/brief.md
Kjell Tore Guttormsen f2d41c8781 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>
2026-06-17 11:02:09 +02:00

302 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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._