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>
21 KiB
Marketplace Polyrepo Migration · Brief
Voyage
/trekbrief-style task brief. Foundation document for splitting thektg-plugin-marketplacemonorepo 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-annotationDECISIONblocks.
- Written: 2026-06-17
- Status: RATIFIED 2026-06-17 — all
DECISIONblocks D1–D8 ratified as recommended (see each block). Next: run/trekplanin 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.jsonread; (2) official Claude Code marketplace docs verified viaclaude-code-guideagent (plugin-marketplaces, plugins-reference); (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:
-
A Claude Code marketplace does NOT require a monorepo. Verified against official docs: a plugin entry's
sourcecan be an external git repo (github/url/git-subdir/npm), a singlemarketplace.jsoncan 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 aref(tag/branch) orsha. The monorepo is a packaging convenience, not an architectural necessity (Explore verdict). -
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-styleandclaude-designare new). A single working tree shares one.git/indexand oneHEAD, 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.mdmerge pressure (the three-doc gate forces every feature to touch the root README); noisygit 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 noshared/— onlymarketplace.json(external sources), the landingREADME.md,GOVERNANCE.md, and marketplace-wide conventions. /plugin marketplace add <same URL>+/plugin install <plugin>@ktg-plugin-marketplaceworks 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.gitthen/plugin install <plugin>@ktg-plugin-marketplacesucceeds 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 itstests//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.jsonschema check + install smoke after each plugin graduates. - SC4 — Catalog is thin. After completion the catalog repo contains no
plugins/and noshared/. 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.jsonSHA-256 matches). Verify: run sync, diff manifest. - SC6 — History per D2. Each new repo's
git logreflects the ratified history decision (filter-repo preserved, or hard-cut with the pre-migration monorepo archived). Verify:git logper repo. - SC7 — Continuity intact. Each plugin repo's
.gitignore(prefix re-rooted) ignores its local state; a session started in the repo injects itsSTATE.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-marketplacestays — existing user installs keep their update path. - NOT GitHub. Forgejo only (
git.fromaitochitta.com); neverghCLI. - 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):
{ "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/xsource before its external repo is pushed and verified. - R2 — History loss. Mitigation:
filter-repoon 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.jsonSHA 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-studioresolver fallback misfires. The../../../../fallback instorage.ts:48assumes the monorepo depth. Mitigation: verify the marker-based primary path finds.claude-plugin/plugin.jsonat 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/.gitleakspath-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-repoinvocation; whether to also preserveshared/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 whetherclaude-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
CHANGELOGstrategy (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/.gitleaksre-rooting,marketplace.jsonrewrites, 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
- Per-plugin standalone: clone each new repo to
/tmp, run its own validate/test suite → SC2. - Install smoke:
/plugin marketplace add <URL>+ install each plugin in a fresh session, at the final state and after each graduation → SC1, SC3, SC8. - Catalog thinness:
lsthe catalog — noplugins/, noshared/→ SC4. - DS parity: change a DS token in the DS repo, re-sync a consumer, diff
MANIFEST.jsonSHA → SC5. - History:
git logper new repo matches D2 → SC6. - Continuity: start a session in each repo; confirm its
STATE.mdinjects and hooks run;git statusshows no committed local-state files → SC7.
12. References (tool-verified anchors, 2026-06-17)
.claude-plugin/marketplace.json— 10 plugins, allsource: "./plugins/<name>".scripts/sync-design-system.mjs:26-27—MARKETPLACE_ROOTrelative resolution + vendoring target.plugins/linkedin-studio/scripts/analytics/src/utils/storage.ts:18-33,48— marker-basedfindPluginRoot+../../../../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,
plugins-reference — external/mixed sources, thin catalog,
ref/shapinning 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.