diff --git a/docs/marketplace-polyrepo-migration/brief.md b/docs/marketplace-polyrepo-migration/brief.md new file mode 100644 index 0000000..98c9f64 --- /dev/null +++ b/docs/marketplace-polyrepo-migration/brief.md @@ -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 D1–D8 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/"`) | +| 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//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/`; 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 ` + `/plugin install @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 @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/ × 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/`).** +**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-`; (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// --path-rename plugins//:` +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 ` + 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/"`. +- `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._