Plan for splitting the monorepo into per-plugin Forgejo repos + a DS repo + a thin catalog. Built from the ratified brief via /trekplan: 6-agent exploration verified the brief and surfaced 6 under-scoped findings (F1-F6: plugin renames, ssh->https, 148MB blob bloat, config-audit snapshot path-coupling, tag namespace, README link rot). plan-critic REVISE on first draft (3 blockers + 8 majors) all addressed after re-verifying contested facts; scope-guardian ALIGNED. Local Claude half (Steps 1-11) is fully reversible NULL-push; operator window is a separate runbook. Held unpushed per D8. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
69 KiB
Marketplace Polyrepo Migration — Implementation Plan
Plan quality: B+ (85/100) — APPROVE_WITH_NOTES (revised after adversarial review — Phase 9)
Generated by trekplan v5.1.1 on 2026-06-17 —
plan_version: 1.7Brief:docs/marketplace-polyrepo-migration/brief.md(RATIFIED, D1–D8). Profile: premium (Opus).⚠ Brief corrections requiring operator ratification (the plan overrides two literal values in the RATIFIED brief — surface these for explicit annotation, do not absorb silently):
ssh://→https://(F2). Brief SC1 (§4) and the §6 entry shape literally specifyssh://git@…; D4 ratified only pin-to-tag, silent on scheme. Claude Code rejects SSH marketplace sources (issue #9740) → the plan useshttps://git.fromaitochitta.com/open/<repo>.giteverywhere. Correct engineering call, but it edits a ratified literal.- config-audit SC2 carve-out (F4). Two back-compat test files (
json-backcompat.test.mjs,raw-backcompat.test.mjs) assert byte-stability of a capture that embeds the original machine's absolute path + a sibling marketplace + deleted plugins; the plan excludes them by name from config-audit's SC2 gate (Step 7) — touches test fixtures, edges the §5 non-goal. Operator must accept the carve-out.
Context
The ktg-plugin-marketplace monorepo (10 plugins under plugins/, a shared design system under
shared/, a catalog at the root) is split into one Forgejo repo per plugin + a versioned
design-system repo + a thin catalog repo at the same URL. The plugins are already independent
units (independent versions, self-contained plugin.json/README/CLAUDE.md, no live
cross-plugin runtime imports — confirmed hard by exploration), so the split aligns structure with
reality and removes concurrent-session git contention structurally rather than mitigating it. The
marketplace must stay installable and live at every step (mixed in-repo + external sources).
This plan covers the Claude-run, local, reversible half: it builds the migration tooling, runs a
full local dry-run extraction with standalone validation, and stages (but never runs) the
outward steps — all with NULL push (honoring D8). The operator-run irreversible window
(Forgejo repo creation, first push, live marketplace.json cutover) is captured as a runbook
deliverable (Step 10) + the Execution Strategy, not as machine-verified steps, because those are not
local file edits.
Every decision below traces to the brief: the goal (§3), success criteria SC1–SC8 (§4), the scope fence (§5), decisions D1–D8 (§7), risks R1–R8 (§8), the five §9 open questions, and the execution model (§10). Exploration surfaced six findings the brief under-scoped (F1–F6 below); the plan absorbs all six.
Exploration deltas the brief did not have (load-bearing)
- F1 — Plugin renames split history.
voyage←ultraplan-local(136 commits),llm-security←llm-security-copilot(3),linkedin-studio←linkedin-thought-leadership(50). A single-path filter (--subdirectory-filter plugins/voyage) silently drops the pre-rename history. Renamed plugins MUST be extracted with multiple--patharguments (both names) → SC6. - F2 — HTTPS, not
ssh://. The brief's §6ssh://git@…urlsource is unverified/likely rejected (Claude Code issue #9740 showsmarketplace addrejecting SSH). HTTPS (https://git.fromaitochitta.com/open/<repo>.git) is the only form verified-accepted on both theurlsource schema andmarketplace add. The discriminator key issource(brief correct), nottype. Forgejo has no background auto-update token → public repos sidestep the gap → answers §9 visibility. - F3 —
ms-ai-architectis a 148 MB screenshot blob-bomb (86% of.git; 26 blobs already deleted from HEAD but alive in history). filter-repo resurrects them → blob-strip decision. - F4 —
config-auditsnapshot tests embed the absolute path (verified directly): thedefault-output/snapshots assert byte-equal output whose deep per-file paths (e.g.…/tests/fixtures/marketplace-medium/CLAUDE.md) are not normalized (normalizeScanOrchestratorscrubs onlymeta.target/timestamp/duration_ms), so they break at a new clone path; thev5.0.0/back-compat snapshots (loaded byjson-backcompat.test.mjs+raw-backcompat.test.mjs) additionally embed a sibling marketplace (ktg-privat) + deleted plugins from their original capture. SC2 blocker → real fix uses the existingUPDATE_SNAPSHOT=1seam for the rebasable suite + by-name exclusion of the 2 frozen back-compat tests (Step 7). - F5 — Tag namespace is inconsistent (3 schemes; bare
v5.0.0/v7.7.2carry no plugin identity;v3.4.1→deletedultraplan-local). filter-repo copies ALL tags into EACH extract → strip-all-then-re-tag. - F6 — README/metadata reference rot. Every plugin
README.md:7links../../README.md(AI-disclosure) → 404 standalone; root-only.mailmap/.gitleaks.*don't travel with--path plugins/x;ai-psychosis/plugin.jsonalready points at a standalone URL while others point at the monorepo (reconcile).
The five §9 open questions — RESOLVED with evidence
- Exact filter-repo invocation +
shared/history into DS. For non-renamed plugins:git filter-repo --path plugins/<name>/ --path-rename plugins/<name>/:(≡--subdirectory-filter plugins/<name>). For renamed plugins: explicit multi---pathwith a--path-renameper old+new name (F1). DS:git filter-repo --subdirectory-filter shared— yes, preserveshared/history into the DS repo (clean: 10/12shared/-touching commits are pure-shared, 2 are single-plugin-entangled but conflict-free). All extractions run on agit clone --no-localfresh clone (avoids filter-repo's spurious freshness abort on local clones; do not paper over with--force). - True DS consumers. Exactly 2 —
llm-security,ms-ai-architect(only two with aplayground/vendor/dir).claude-design+human-friendly-stylehave noplayground/→ don't need DS. The "6" inshared/PLAYGROUND-MAINTENANCE.md:5is stale (one entry,ultraplan-local, is the ghost pre-rename name forvoyage). DS-repo standup scope = 2. - Repo visibility. Public (recommended) — sidesteps the Forgejo no-auto-update-token gap
(F2), matches the existing
open/model, lets HTTPS clone work without credential setup. Operator confirms at the window. playground-examples/home. DS repo. It is DS-coupled (every HTML referencesplayground-design-system;PLAYGROUND-MAINTENANCE.mdanchorsros-lier-kommune.htmlto ms-ai-architect renderers). Travels with the DS repo, not the thin catalog.- Per-repo CHANGELOG. Carry existing — filter-repo preserves each plugin's
CHANGELOG.mdand its commit history (D2); tags are re-created cleanly per repo (F5).
Architecture Diagram
graph TD
subgraph BEFORE["Monorepo (today) — open/ktg-plugin-marketplace"]
MJ0[".claude-plugin/marketplace.json<br/>10x ./plugins/x"]
PLUGS["plugins/ x10"]
SH["shared/<br/>design-system + examples"]
SYNC["scripts/sync-design-system.mjs"]
SYNC -->|vendors| PLUGS
SH -->|source| SYNC
end
subgraph AFTER["Polyrepo (target) — same URL"]
CAT["open/ktg-plugin-marketplace<br/>THIN CATALOG<br/>marketplace.json (external+ref) · README · GOVERNANCE · CONVENTIONS · .mailmap · .gitleaks"]
DS["open/playground-design-system<br/>design-system + examples + sync-design-system.mjs(--source/--check)"]
R1["open/voyage (v5.1.1)"]
R2["open/llm-security (v7.7.2)"]
R3["open/linkedin-studio (v0.4.0)"]
Rn["open/<plugin> ... x10 total"]
CAT -.->|url + ref pin HTTPS| R1
CAT -.->|url + ref pin HTTPS| R2
CAT -.->|url + ref pin HTTPS| R3
CAT -.->|url + ref pin HTTPS| Rn
DS -->|vendor via re-homed sync| R2
DS -->|vendor via re-homed sync| Rn
end
BEFORE ==>|filter-repo extraction<br/>NULL push until window| AFTER
Codebase Analysis
- Tech stack: Node.js
.mjsscripts (zero npm deps house style,node:test), bash 3.2 hooks/ validators, one TypeScript analytics subtree (linkedin-studio/scripts/analytics, needsnpm install+tsx). Git on Forgejo (git.fromaitochitta.com, orgopen/). 1890 source files, 741 commits,.git144 MB. - Key patterns: Each plugin self-contained (
.claude-plugin/plugin.json+README+CLAUDE.md, optionalhooks/ tests/ scripts/ playground/). Hooks resolve via${CLAUDE_PLUGIN_ROOT}(relocation-invariant — confirmed). DS is vendored (playground/vendor/), never live-linked. - Relevant files (verified):
.claude-plugin/marketplace.json— 10 entries, allsource: "./plugins/<name>", entry shape{name, source, description}(noversion/reftoday).scripts/sync-design-system.mjs— SOURCE at:26-27(MARKETPLACE_ROOT/shared/…), target at:146(plugins/<name>/playground/vendor/…), SHA-256MANIFEST.json+ drift-guard at:118-127,151-156. No--sourceflag today (parseArgs:30-46)..gitignore—plugins/*/patterns at:9(plugins/*/.claude/) and:12(plugins/*/reports/*-beskrivelse.*); global patterns (STATE.md,*.local.md, …) stay..gitleaksignore— 2 plugin-prefixed fingerprints (:2llm-security,:5linkedin-studio);.gitleaks.tomlallowlist:13already plugin-relative.plugins/linkedin-studio/scripts/analytics/src/utils/storage.ts:18-33,48— marker resolver (standalone-safe primary) +../../../../fallback (latent depth assumption, dead branch).plugins/voyage/tests/helpers/hook-helper.mjs:4— verbatim copy (C7), stays a copy.
- Reusable code: the existing SHA-256
MANIFEST.json+detectDriftinsync-design-system.mjsalready implements SC5's hash mechanism — extend, don't rebuild. Thevalidate-plugin.shstructure-validator inclaude-design/tests/+ms-ai-architect/tests/is the pattern to port to the two test-less plugins (okr, human-friendly-style). - Recent git activity: HEAD
f2d41c8(ratified brief, 3 commits ahead of origin, unpushed). Onlymain, no feature branches. Cross-plugin bleed commits confirmed (f460814touches 10 plugins;69610d42) — ~1.8% of commits span 3+ plugins, mostly marketplace-wide doc sweeps.
Research Sources
| Topic | Source | Key findings | Confidence |
|---|---|---|---|
git filter-repo subdir extract |
git-filter-repo man page | --subdirectory-filter X ≡ --path X/ --path-rename X/:; removes origin; rewrites tags; aborts on non-fresh clone |
High |
| Local-clone freshness | filter-repo #38 | git clone --no-local from a local source (not --force) |
High |
| marketplace.json external/mixed/ref | plugin-marketplaces, plugins-reference | discriminator is source not type; {source:"url",url,ref}; mixed in-repo+external in one file supported; /plugin marketplace update refreshes a bumped ref |
High |
| SSH rejection | claude-code #9740 (open) | marketplace add rejects SSH URLs → use HTTPS |
High (use HTTPS) |
| Forgejo auto-update token | plugin-marketplaces token table | no FORGEJO_TOKEN for bg auto-update of private repos → prefer public |
Medium |
| Forgejo repo create | Forgejo api-usage | POST /api/v1/orgs/{org}/repos, header Authorization: token …, body {name,private,auto_init:false,default_branch} |
High |
Implementation Plan
Execution invariants (apply to every step):
- D8 — NULL PUSH. Every
git commithere is local only; nogit pushuntil the authorized operator window. Checkpoints are local commits held onmainahead of origin.- All extraction runs from a
git clone --no-localfresh clone in a throwaway workspace ($WORK, default/tmp/polyrepo-migration), never the working checkout (R4). The working repo is read-only to every extraction.- Migration tooling lives under
docs/marketplace-polyrepo-migration/migration/so it sits beside the brief/plan and is removed/archived at thinning, never polluting the live catalog.- The single source of truth for per-plugin paths/versions/flags is
migration/plugin-map.json(Step 1); every later script reads it.
Step 1: Preflight, archive tag, and the plugin map
- Files:
docs/marketplace-polyrepo-migration/migration/00-preflight.sh,docs/marketplace-polyrepo-migration/migration/plugin-map.json,docs/marketplace-polyrepo-migration/migration/00-preflight.test.mjs - Changes: (new files)
00-preflight.shasserts tooling and repo hygiene:git filter-repo --versionreturns a real version,python3 --version≥ 3.6, HEAD isf2d41c8or a descendant onmain,origin/mainis behind (unpushed state expected). Hygiene check is relaxed and idempotent (M12 fix): it aborts only ifgit status --porcelain plugins shared .claude-plugin/marketplace.json scriptsis non-empty (uncommitted change to a migration-sensitive surface) — committedmigration/files and unrelated untracked docs are tolerated, so re-running preflight mid-migration does not abort. It creates the local annotated tagpre-polyrepo-archiveat HEAD (D2; pushed later in the window) idempotently. It establishes the dedicated extraction source (R4 / M6 fix):git clone --no-local "$REPO_ROOT" "$WORK/_mirror"once, at the pinned HEAD —$MIRRORis the single source every per-target extraction clones from, so concurrent commits to the operator's working checkout can never affect any extraction (the working checkout is never read by filter-repo). It also enumerates blob-strip candidates (M4 fix): lists every blob >1 MB in ms-ai-architect history with its path, and emitsblob_strip_safe: trueintoplugin-map.jsoniff all >1 MB blobs are underplayground/screenshots/(elsefalse→ the surgical--path-globstrategy is selected instead of size-strip, decided deterministically, no runtime prompt).plugin-map.jsonencodes, per extraction target: the rename-aware--pathlist, the--path-renamemap, the release tag (fromplugin.jsonversion), the new HTTPS repo URL,has_vendor_ds(true only for llm-security + ms-ai-architect),blob_strip+blob_strip_safe(ms-ai-architect),test_cmd, andstandalone_caveat. Includes the DS target (shared/→open/playground-design-system) and an explicitdrop: ["plugins/ultra-cc-architect/"]note (removed plugin, already its own repo — its history is dropped, not extracted). - Reuses: version strings from each
plugins/<name>/.claude-plugin/plugin.json(task-finder table); churn order from git-historian (voyage→llm-security→linkedin-studio→ms-ai-architect). - Test first:
- File:
docs/marketplace-polyrepo-migration/migration/00-preflight.test.mjs(new) - Verifies:
plugin-map.jsonparses; has exactly 11 targets (10 plugins + DS); the 3 renamed plugins carry ≥2--pathentries each; every plugin'srepo_urlishttps://(notssh://); ms-ai-architect hasblob_strip: true; only llm-security + ms-ai-architect havehas_vendor_ds: true. - Pattern:
plugins/voyage/tests/validators/*.test.mjs(node:test style)
- File:
- Verify:
bash docs/marketplace-polyrepo-migration/migration/00-preflight.sh --dry-run→ expected: printsPREFLIGHT OKand the 11-target table, creates no tag in--dry-run; thennode --test docs/marketplace-polyrepo-migration/migration/00-preflight.test.mjs→ expected:pass. - On failure: escalate — if tooling is missing (
git filter-repoabsent), stop and instructbrew install git-filter-repo; do not proceed. - Checkpoint:
git commit -m "chore(migration): preflight script + rename-aware plugin map" - Manifest:
manifest: expected_paths: - docs/marketplace-polyrepo-migration/migration/00-preflight.sh - docs/marketplace-polyrepo-migration/migration/plugin-map.json - docs/marketplace-polyrepo-migration/migration/00-preflight.test.mjs min_file_count: 3 commit_message_pattern: "^chore\\(migration\\): preflight script \\+ rename-aware plugin map$" bash_syntax_check: - docs/marketplace-polyrepo-migration/migration/00-preflight.sh forbidden_paths: - .claude-plugin/marketplace.json - scripts/sync-design-system.mjs must_contain: - path: docs/marketplace-polyrepo-migration/migration/plugin-map.json pattern: "ultraplan-local" - path: docs/marketplace-polyrepo-migration/migration/plugin-map.json pattern: "https://git.fromaitochitta.com/open/"
Step 2: Re-home and extend sync-design-system.mjs (--source + --check)
- Files:
scripts/sync-design-system.mjs,scripts/sync-design-system.test.mjs - Changes: Add a
--source <dir>flag so the script vendors from an arbitrary DS checkout (the re-homed DS repo) instead of the hardcodedMARKETPLACE_ROOT/shared/playground-design-system(:26-27); keep the in-repo path as the default fallback so the script still works pre-migration. Add a--checkmode that re-hashes a target plugin's vendored tree against its committedMANIFEST.jsonand exits non-zero on drift, with no source required — making SC5 a single runnable command in a clean clone (D3). Resolve the plugin/target dir from a--target <dir>arg whenMARKETPLACE_ROOT/plugins/<name>no longer exists. This is migration rewiring, not a plugin-functionality change (non-goal §5 untouched). The modified script + test travel into the DS repo at standup (Step on the DS target) and are removed from the catalog at thinning (Step 9). - Reuses: existing
detectDrift+ SHA-256MANIFEST.jsonwriter (:118-127) —--checkreuses the same hashing. - Test first:
- File:
scripts/sync-design-system.test.mjs(new) - Verifies:
--source <tmpDS>vendors byte-identically to the default in-repo source (SC5 parity);--checkpasses on an unmodified vendored tree and fails (exit 2) after a tampered byte; omitting--sourcestill resolves the in-repo default. - Pattern:
plugins/voyage/tests/scripts/*.test.mjs
- File:
- Verify:
node --test scripts/sync-design-system.test.mjs→ expected:pass(all assertions); thennode scripts/sync-design-system.mjs --check ms-ai-architect→ expected:MANIFEST OKagainst the current vendored tree. - On failure: revert —
git checkout -- scripts/sync-design-system.mjs scripts/sync-design-system.test.mjs; the legacy in-repo sync still works, so this is non-blocking for the rest until retried. - Checkpoint:
git commit -m "feat(migration): sync-design-system --source + --check for DS re-home" - Manifest:
manifest: expected_paths: - scripts/sync-design-system.mjs - scripts/sync-design-system.test.mjs min_file_count: 2 commit_message_pattern: "^feat\\(migration\\): sync-design-system --source \\+ --check for DS re-home$" bash_syntax_check: [] forbidden_paths: - .claude-plugin/marketplace.json must_contain: - path: scripts/sync-design-system.mjs pattern: "--source" - path: scripts/sync-design-system.mjs pattern: "--check"
Step 3: Rename-aware extraction driver
- Files:
docs/marketplace-polyrepo-migration/migration/10-extract.sh,docs/marketplace-polyrepo-migration/migration/10-extract.test.mjs - Changes: (new files) Given a target key from
plugin-map.json, it:git clone --no-local "$WORK/_mirror" "$WORK/<key>"(clones from the dedicated mirror built in Step 1, never the working checkout — R4/M6); runs a singlegit filter-repoinvocation that composes all history rewrites in one pass (M5 fix — avoids the second-pass--forceabort): the rename-aware--path/--path-renameset from the map (single path for non-renamed; both names for voyage/llm-security/linkedin-studio — F1) plus, for ms-ai-architect, the blob strip selected deterministically fromblob_strip_safe(Step 1):--strip-blobs-bigger-than 1Mwhen all >1 MB blobs are screenshots, else--path-glob '!plugins/ms-ai-architect/playground/screenshots/*'— no runtime prompt (M4/F3). filter-repo composes--path,--path-rename, and--strip-blobs-bigger-thanin one invocation (verified against the man page). After the filter completes (so the tag points at the final rewritten HEAD — M5 ordering fix), it strips every tag filter-repo carried over and re-creates exactly one annotated tagv<version>at HEAD (F5). Idempotent: re-running wipes$WORK/<key>first. No push. - Reuses:
plugin-map.json(Step 1);git clone --no-localper research-scout. - Test first:
- File:
docs/marketplace-polyrepo-migration/migration/10-extract.test.mjs(new) - Verifies (runs the driver against the live monorepo into a temp
$WORK): thevoyageextract'sgit log --allcontains commits whose original paths were under bothplugins/voyage/andplugins/ultraplan-local/(≥150 commits, proving F1 handled); the extractgit taglists exactlyv5.1.1and nothing else; the extract has noplugins/prefix at root (contents at repo root);originremote is absent post-filter. - Pattern: integration test style in
plugins/voyage/tests/integration/*.test.mjs
- File:
- Verify (self-contained — M6):
bash docs/marketplace-polyrepo-migration/migration/00-preflight.sh && bash docs/marketplace-polyrepo-migration/migration/10-extract.sh voyage(preflight builds$WORK/_mirrorif absent; extract is self-healing) → expected:EXTRACT OK voyage → $WORK/voyage (257 commits, tag v5.1.1); thengit -C "$WORK/voyage" log --oneline | wc -l→ expected:≥ 250;git -C "$WORK/voyage" tag→ expected: exactlyv5.1.1. - On failure: retry — if filter-repo aborts on freshness, confirm the source is
$WORK/_mirror(a fresh--no-localclone); if a rename path is missing, the test catches the dropped-history count. Revert leaves no working-tree change (extract is in$WORK). - Checkpoint:
git commit -m "chore(migration): rename-aware filter-repo extraction driver" - Manifest:
manifest: expected_paths: - docs/marketplace-polyrepo-migration/migration/10-extract.sh - docs/marketplace-polyrepo-migration/migration/10-extract.test.mjs min_file_count: 2 commit_message_pattern: "^chore\\(migration\\): rename-aware filter-repo extraction driver$" bash_syntax_check: - docs/marketplace-polyrepo-migration/migration/10-extract.sh forbidden_paths: - .claude-plugin/marketplace.json - plugins must_contain: - path: docs/marketplace-polyrepo-migration/migration/10-extract.sh pattern: "no-local" - path: docs/marketplace-polyrepo-migration/migration/10-extract.sh pattern: "ultraplan-local"
Step 4: Per-repo config re-rooting (.gitignore, .gitleaks, .mailmap)
- Files:
docs/marketplace-polyrepo-migration/migration/20-rehome-config.sh,docs/marketplace-polyrepo-migration/migration/templates/gitignore.plugin.tmpl,docs/marketplace-polyrepo-migration/migration/20-rehome-config.test.mjs - Changes: (new files) Operates on an extracted repo in
$WORK/<key>: writes a re-rooted.gitignorefrom the template (theplugins/*/prefixes stripped →.claude/,reports/*-beskrivelse.*; the globalSTATE.md/*.local.md/OS patterns kept — F6/C3); generates a per-plugin.gitleaks.tomlfrom the root baseline and a.gitleaksignorewith the matching fingerprint re-rooted by dropping ONLY theplugins/<name>/path prefix and keeping the full:rule-id:linesuffix (M7 fix — the gitleaks fingerprint format ispath:rule-id:line, notpath:line; dropping the rule-id yields a fingerprint that no longer suppresses) — only for llm-security (examples/malicious-skill-demo/evil-project-health/lib/telemetry.mjs:generic-api-key:18) and linkedin-studio (hooks/prompts/content-quality-gate.md:linkedin-client-id:14); copies the root.mailmapinto every extract (F6 — it does not travel with--path). For the DS target it also copies the modifiedsync-design-system.mjs+ test (Step 2) andshared/PLAYGROUND-MAINTENANCE.md+playground-examples/into the DS repo root (§9 Q4). - Reuses: root
.gitignore(:9,:12),.gitleaks.toml(:13),.gitleaksignore(:2,:5),.mailmap. - Test first:
- File:
docs/marketplace-polyrepo-migration/migration/20-rehome-config.test.mjs(new) - Verifies (against a freshly extracted llm-security in
$WORK): the new.gitignorecontains.claude/but noplugins/*/prefix; the re-rooted.gitleaksignorefingerprint is exactlyexamples/malicious-skill-demo/evil-project-health/lib/telemetry.mjs:generic-api-key:18(noplugins/llm-security/prefix, rule-idgeneric-api-keyretained);.mailmapexists; a plugin with no fingerprint (graceful-handoff) gets a.gitleaks.tomlbut an empty/absent.gitleaksignore. - Pattern:
plugins/config-audit/tests/lib/*.test.mjs
- File:
- Verify:
bash …/20-rehome-config.sh llm-securitythengit -C /tmp/polyrepo-migration/llm-security check-ignore .claude/x→ expected:.claude/x(ignored);grep -c 'plugins/' /tmp/polyrepo-migration/llm-security/.gitleaksignore→ expected:0. - On failure: retry — config generation is idempotent; revert leaves working tree clean.
- Checkpoint:
git commit -m "chore(migration): per-repo gitignore/gitleaks/mailmap re-rooting" - Manifest:
manifest: expected_paths: - docs/marketplace-polyrepo-migration/migration/20-rehome-config.sh - docs/marketplace-polyrepo-migration/migration/templates/gitignore.plugin.tmpl - docs/marketplace-polyrepo-migration/migration/20-rehome-config.test.mjs min_file_count: 3 commit_message_pattern: "^chore\\(migration\\): per-repo gitignore/gitleaks/mailmap re-rooting$" bash_syntax_check: - docs/marketplace-polyrepo-migration/migration/20-rehome-config.sh forbidden_paths: - .gitignore - .gitleaks.toml - .gitleaksignore must_contain: - path: docs/marketplace-polyrepo-migration/migration/20-rehome-config.sh pattern: "mailmap"
Step 5: Reference-rot rewriter (README links + plugin.json/package.json URLs)
- Files:
docs/marketplace-polyrepo-migration/migration/30-fix-references.mjs,docs/marketplace-polyrepo-migration/migration/30-fix-references.test.mjs - Changes: (new files) Operates on an extracted repo in
$WORK/<key>: replaces the[Full disclosure →](../../README.md#ai-generated-code-disclosure)footnote at every pluginREADME.md:7with the inlined disclosure text (M13 — inline chosen over a cross-repo link: deterministic, self-contained, no dangling anchor); fixesgraceful-handoff/README.md:355and any other../../README.md/../../.claude-plugin/marketplace.jsonreference (e.g. linkedin-studio remediation docs); setsplugin.jsonrepositoryandpackage.jsonhomepage/repositoryto the new standaloneopen/<plugin>HTTPS URL. Scope note (M14):ai-psychosis/plugin.jsonalready points atopen/ai-psychosis(verified) — for it this is a verify-only no-op; the real reconcile work is the 9 other plugins still pointing at the monorepo URL/subpath (e.g. linkedin-studio, llm-securitypackage.jsonhomepage). Reads URLs fromplugin-map.json. Reports every file it rewrote. - Reuses:
plugin-map.jsonrepo URLs. - Test first:
- File:
docs/marketplace-polyrepo-migration/migration/30-fix-references.test.mjs(new) - Verifies (against an extracted graceful-handoff): no
../../README.mdsubstring remains in any*.md;plugin.json.repositoryends with/open/graceful-handoff; an idempotent second run makes zero changes. - Pattern:
plugins/linkedin-studio/render/__tests__/*.test.mjs
- File:
- Verify:
node …/30-fix-references.mjs graceful-handoffthengrep -rn "\.\./\.\./README.md" /tmp/polyrepo-migration/graceful-handoff→ expected: no matches (exit 1 from grep). - On failure: retry — rewriter is idempotent and reports diffs; revert leaves working tree clean.
- Checkpoint:
git commit -m "chore(migration): standalone reference-rot rewriter" - Manifest:
manifest: expected_paths: - docs/marketplace-polyrepo-migration/migration/30-fix-references.mjs - docs/marketplace-polyrepo-migration/migration/30-fix-references.test.mjs min_file_count: 2 commit_message_pattern: "^chore\\(migration\\): standalone reference-rot rewriter$" bash_syntax_check: [] forbidden_paths: - plugins must_contain: - path: docs/marketplace-polyrepo-migration/migration/30-fix-references.mjs pattern: "ai-generated-code-disclosure"
Step 6: Standalone validation harness (SC2 + SC7)
- Files:
docs/marketplace-polyrepo-migration/migration/40-validate-standalone.sh,docs/marketplace-polyrepo-migration/migration/40-validate-standalone.test.mjs - Changes: (new files) Per
plugin-map.jsontest_cmd: clones the extracted repo to a clean/tmp/claude-*dir (no marketplace parent) and runs its self-validate command in the runner the plugin itself declares —node --test 'tests/**/*.test.mjs'(quoted globstar, Node-≥21 native; config-audit/llm-security/voyage) ornode --test <dir>/*.test.mjsglob for plugins with a flat layout — nevernode --test <dir>(Node 25 gotcha). Per-plugin caveats baked in:ai-psychosisruns from repo root (relative-fixture cwd);linkedin-studioSC2 is explicitly two-tier (M11): the.mjscore (node --test hooks/scripts/__tests__/*.test.mjs render/__tests__/*.test.mjs agents/__tests__/*.test.mjs) is the hard gate (must pass with zero deps); the TS analytics suite (cd scripts/analytics && npm install && npm test) is reported as a separate ADVISORY line — network-required, NOT a clean-room hard gate (not silently waived, explicitly labelled non-blocking for SC2).claude-design+ms-ai-architectrunbash tests/validate-plugin.sh;okr+human-friendly-stylerun the portedvalidate-plugin.generic.shstructure check (Step 7 provides it) since they have no unit suite (H2).voyage/.forgejo/survival (M17): for voyage, assert the extract still contains.forgejo/ISSUE_TEMPLATE/(it travels under--path plugins/voyage/) and that no file in it references a monorepo-relative path (it holds only issue templates, no workflows — confirmed). SC7 assertions for every repo:git ls-files | grep -E 'STATE\.md|\.local\.md$'is empty except the intentional trackedokr/templates/okr.local.md.template; no../../README.mdremains;.gitignoreignores.claude/. Emits a per-repo PASS/FAIL table. - Reuses: the per-plugin command table (test-strategist);
validate-plugin.shpattern. - Test first:
- File: (the harness is itself the test surface) — assert via a smoke run that the harness exits
non-zero when fed a deliberately broken extract (a planted tracked
STATE.md) and zero on a clean one. Encoded asdocs/marketplace-polyrepo-migration/migration/40-validate-standalone.test.mjs. - Verifies: harness FAILs a repo with a tracked
*.local.md; PASSes a clean repo; uses glob test form (asserts the command string contains*.test.mjs, not a bare dir). - Pattern:
plugins/voyage/tests/synthetic/*.test.mjs
- File: (the harness is itself the test surface) — assert via a smoke run that the harness exits
non-zero when fed a deliberately broken extract (a planted tracked
- Verify:
bash …/40-validate-standalone.sh graceful-handoff→ expected:graceful-handoff: PASS (6 tests, standalone-safe);node --test …/40-validate-standalone.test.mjs→ expected:pass. - On failure: escalate per repo — a FAIL is a genuine SC2/SC7 blocker for that plugin (e.g. config-audit until Step 7); record it, continue other repos, do not mask.
- Checkpoint:
git commit -m "chore(migration): standalone SC2/SC7 validation harness" - Manifest:
manifest: expected_paths: - docs/marketplace-polyrepo-migration/migration/40-validate-standalone.sh - docs/marketplace-polyrepo-migration/migration/40-validate-standalone.test.mjs min_file_count: 2 commit_message_pattern: "^chore\\(migration\\): standalone SC2/SC7 validation harness$" bash_syntax_check: - docs/marketplace-polyrepo-migration/migration/40-validate-standalone.sh forbidden_paths: - plugins must_contain: - path: docs/marketplace-polyrepo-migration/migration/40-validate-standalone.sh pattern: "\\*.test.mjs"
Step 7: Resolve the config-audit SC2 blocker + port a structure-validator to okr/human-friendly-style
- Files:
docs/marketplace-polyrepo-migration/migration/50-config-audit-sc2.sh,docs/marketplace-polyrepo-migration/migration/templates/validate-plugin.generic.sh,docs/marketplace-polyrepo-migration/migration/50-config-audit-sc2.test.mjs - Changes: (new files) config-audit (F4 — corrected against verified test code): config-audit
declares its runner as
node --test 'tests/**/*.test.mjs'(52 files, CLAUDE.md:109). Two distinct portability defects exist (both verified directly): (a) The rebasable suitesnapshot-default-output.test.mjsasserts byte-equal CLI output whose deep per-file paths are not normalized (normalizeScanOrchestratorscrubs onlymeta.target/timestamp/duration_ms,normalizeTokenHotspotsonlyduration_ms), so it breaks at a new clone path. Fix: the script runsUPDATE_SNAPSHOT=1 node --test tests/snapshot-default-output.test.mjsinside the clean clone to re-seed those snapshots at the clone's own path — it then passes (the seam exists, line 33; this is the intended re-approval path). (b) The frozen back-compat testsjson-backcompat.test.mjs+raw-backcompat.test.mjsload thetests/snapshots/v5.0.0/fixtures, which embed the original capture machine's absolute path + a sibling marketplace (ktg-privat) + deleted plugins — regenerating them would defeat their byte-stability purpose. Fix: these two files are excluded by name from config-audit's SC2 gate, which runs over the file listtests/**/*.test.mjsminus those two (deterministic file enumeration, no invented env var). The exclusion + rationale is recorded in the config-audit repo's test README. This edges the §5 non-goal "NOT changing plugin code" (it re-seeds fixtures + names an exclusion; no functionality changes) — surfaced as a brief-correction requiring operator ratification (header) + Assumption 4. okr + human-friendly-style (H2):validate-plugin.generic.shis a ported, parameterized copy of the existingclaude-design/tests/validate-plugin.sh(validplugin.json, expected command/skill/agent files present, frontmatter parses) so SC2 has a runnable gate for the two test-less plugins. - Reuses:
plugins/claude-design/tests/validate-plugin.sh(the pattern); config-audit's ownUPDATE_SNAPSHOT=1seam (line 33) + declared runner (CLAUDE.md:109). - Test first:
- File:
docs/marketplace-polyrepo-migration/migration/50-config-audit-sc2.test.mjs(new) - Verifies: after the script runs against an extracted config-audit, the SC2 gate (full
tests/**/*.test.mjsminus json-backcompat + raw-backcompat) passes; the gate file list does NOT include the two excluded files; the generic validator PASSes okr and FAILs a plugin with a deletedplugin.json. - Pattern:
plugins/config-audit/tests/snapshot-default-output.test.mjs(the rebasable one)
- File:
- Verify:
bash …/50-config-audit-sc2.shthen in the extracted clone the emitted SC2 gate command (e.g.node --test $(ls tests/**/*.test.mjs | grep -vE 'json-backcompat|raw-backcompat')) → expected:pass;bash validate-plugin.generic.sh okr→ expected:okr: STRUCTURE OK. - On failure: escalate — surface to the operator the choice: accept config-audit SC2 as "full suite minus 2 frozen back-compat tests" (recommended), or invest in normalizing the v5.0.0 snapshots' embedded paths (config-audit's own backlog, out of migration scope).
- Checkpoint:
git commit -m "fix(migration): config-audit SC2 gate (re-seed + back-compat exclusion) + generic validator" - Manifest:
manifest: expected_paths: - docs/marketplace-polyrepo-migration/migration/50-config-audit-sc2.sh - docs/marketplace-polyrepo-migration/migration/templates/validate-plugin.generic.sh - docs/marketplace-polyrepo-migration/migration/50-config-audit-sc2.test.mjs min_file_count: 3 commit_message_pattern: "^fix\\(migration\\): config-audit SC2 gate \\(re-seed \\+ back-compat exclusion\\) \\+ generic validator$" bash_syntax_check: - docs/marketplace-polyrepo-migration/migration/50-config-audit-sc2.sh - docs/marketplace-polyrepo-migration/migration/templates/validate-plugin.generic.sh forbidden_paths: - plugins/config-audit must_contain: - path: docs/marketplace-polyrepo-migration/migration/50-config-audit-sc2.sh pattern: "UPDATE_SNAPSHOT" - path: docs/marketplace-polyrepo-migration/migration/50-config-audit-sc2.sh pattern: "json-backcompat"
Step 8: marketplace.json rewriter (mixed-source aware, HTTPS + ref pin)
- Files:
docs/marketplace-polyrepo-migration/migration/60-rewrite-marketplace.mjs,docs/marketplace-polyrepo-migration/migration/60-rewrite-marketplace.test.mjs - Changes: (new files — authored, not run against the live file until the window) Rewrites a
marketplace.jsonso a named subset of entries become external{"source":"url","url":"https://git.fromaitochitta.com/open/<name>.git","ref":"v<version>"}(F2 — HTTPS + discriminatorsource, ref = the plugin's release tag, D4) while the rest stay./plugins/<name>— enabling the mixed-source intermediate states that keep the marketplace live (SC3/SC8).--only <names>flips a subset;--allflips everything (final state). Reads versions fromplugin-map.json; validates output against the marketplace schema (each entry hasname+source+description; external entries have a validurl+ref). Writes to a--outpath by default (never the live file) so Step 11's dry-run exercises it without violating D8. - Reuses:
plugin-map.json; the current.claude-plugin/marketplace.jsonas the input shape. - Test first:
- File:
docs/marketplace-polyrepo-migration/migration/60-rewrite-marketplace.test.mjs(new) - Verifies:
--only voyageproduces a file wherevoyageis an externalurl+ref:v5.1.1entry and the other 9 stay./plugins/…(mixed-source proven); every externalurlstartshttps://(nossh://);--allleaves zero./plugins/sources; schema-validates. - Pattern:
plugins/voyage/tests/parsers/*.test.mjs
- File:
- Verify:
node …/60-rewrite-marketplace.mjs --only voyage --out /tmp/mp-voyage.jsonthengrep -c '"./plugins/' /tmp/mp-voyage.json→ expected:9;grep -c 'ssh://' /tmp/mp-voyage.json→ expected:0. - On failure: retry — pure transform, idempotent; revert leaves working tree clean.
- Checkpoint:
git commit -m "chore(migration): mixed-source marketplace.json rewriter (HTTPS+ref)" - Manifest:
manifest: expected_paths: - docs/marketplace-polyrepo-migration/migration/60-rewrite-marketplace.mjs - docs/marketplace-polyrepo-migration/migration/60-rewrite-marketplace.test.mjs min_file_count: 2 commit_message_pattern: "^chore\\(migration\\): mixed-source marketplace.json rewriter \\(HTTPS\\+ref\\)$" bash_syntax_check: [] forbidden_paths: - .claude-plugin/marketplace.json must_contain: - path: docs/marketplace-polyrepo-migration/migration/60-rewrite-marketplace.mjs pattern: "https://git.fromaitochitta.com/open/"
Step 9: Catalog-thinning script (staged for the window)
- Files:
docs/marketplace-polyrepo-migration/migration/70-thin-catalog.sh,docs/marketplace-polyrepo-migration/migration/70-thin-catalog.test.mjs - Changes: (new files — staged, not run until the window) Produces the thin-catalog state in a
workspace: removes
plugins/+shared/+scripts/sync-design-system.mjs(+ its test); extracts the marketplace-wide conventions from the rootCLAUDE.mdinto a newCONVENTIONS.md(D6); thins the catalogCLAUDE.mdto catalog-maintenance only; rewrites the landingREADME.mdto reference the external plugin repos. The landing-README rewrite is the place the stale version/count drift is corrected (M15 — coupled explicitly, not a side ride-along): D6 mandates the README rewrite, and a rewrite that re-states the plugin roster MUST state the verified counts/versions (10 plugins, not ~8; linkedin-studio 0.4.0 not "4.1.0"; config-audit 5.1.0; voyage 5.1.1; …) or it ships a known-false document. The correction is therefore part of the D6 README deliverable, coupled to it, not a separable doc-fix. Verifies the result:lsshows noplugins/, noshared/(SC4); the catalog retainsmarketplace.json,README.md,GOVERNANCE.md,CONVENTIONS.md,.mailmap, a.gitleaksbaseline. - Reuses: root
CLAUDE.mdconventions block;GOVERNANCE.mdas the conventions sibling. - Test first:
- File:
docs/marketplace-polyrepo-migration/migration/70-thin-catalog.test.mjs(new) - Verifies (against a workspace copy): post-thin tree has no
plugins/, noshared/, noscripts/sync-design-system.mjs;CONVENTIONS.mdexists and contains the Forgejo/conventional- commits/three-doc rules;marketplace.json+README.md+GOVERNANCE.mdremain. - Pattern:
plugins/config-audit/tests/*.test.mjs
- File:
- Verify:
bash …/70-thin-catalog.sh --workspace /tmp/thin-catalogthenls /tmp/thin-catalog→ expected: noplugins/sharedentries;test -f /tmp/thin-catalog/CONVENTIONS.md→ expected: exit 0. - On failure: revert — operates only in
--workspace; the live tree is untouched (forbidden path enforced). - Checkpoint:
git commit -m "chore(migration): catalog-thinning script (staged for window)" - Manifest:
manifest: expected_paths: - docs/marketplace-polyrepo-migration/migration/70-thin-catalog.sh - docs/marketplace-polyrepo-migration/migration/70-thin-catalog.test.mjs min_file_count: 2 commit_message_pattern: "^chore\\(migration\\): catalog-thinning script \\(staged for window\\)$" bash_syntax_check: - docs/marketplace-polyrepo-migration/migration/70-thin-catalog.sh forbidden_paths: - plugins - shared - CLAUDE.md - README.md must_contain: - path: docs/marketplace-polyrepo-migration/migration/70-thin-catalog.sh pattern: "CONVENTIONS.md"
Step 10: Operator-window runbook (pilot gate + batch order)
- Files:
docs/marketplace-polyrepo-migration/migration/RUNBOOK.md - Changes: (new file) The single document the operator follows in the authorized window. It
encodes: (0) Pilot gate (B3 ordering fix — the flip MUST precede the install-smoke, else the
test is vacuous). Recommend
graceful-handoff(low-churn, has tests, no DS, no blob bloat). Steps, in order: (0a) create it on Forgejo (POST /api/v1/orgs/open/reposwith{"name":"graceful-handoff","private":false,"auto_init":false,"default_branch":"main"}, headerAuthorization: token $FORGEJO_TOKEN); (0b)git remote add origin https://… && git push origin --all && git push origin --tags; (0c) flip graceful-handoff's entry in the catalogmarketplace.jsonto the externalurl+refform (60-rewrite-marketplace.mjs --only graceful-handoff) and push the catalog in its still-mixed state (9×./plugins/x+ 1× external;plugins/still present, thinning comes last); (0d) in a fresh Claude Code session/plugin marketplace add https://git.fromaitochitta.com/open/ktg-plugin-marketplace.git+/plugin install graceful-handoff@ktg-plugin-marketplace. **Because the entry is now external, the install resolves the Forgejo repo over HTTPS atref: v2.1.0— this is the actual test of the HTTPS + external-source- ref-pin + Forgejo chain** (F2 / Assumption 1, the highest-stakes unverified link). If 0d fails,
STOP — revert the flip (graceful-handoff back to
./plugins/graceful-handoff, marketplace stays live) and diagnose before touching the other 10. (1) DS repo first, (2) high-churn (voyage → llm-security → linkedin-studio → ms-ai-architect — corrected order), (3) low-churn (config-audit, okr, ai-psychosis, human-friendly-style, claude-design), each: create repo → push → flip itsmarketplace.jsonentry to external (--only) → push catalog → install-smoke (SC8) before the next. (4) Thin the catalog last (Step 9), push catalog, push thepre-polyrepo-archivetag. Includes the visibility decision (public — §9), theauto_init:falserequirement, the ref-update flow (/plugin marketplace update), the per-plugin install-smoke checklist ordered by surface (linkedin-studio 29 cmds first), and the rollback note (a flip is reversible until the./plugins/xsource is removed; never remove it before the external repo is pushed+verified — R1/SC8).
- ref-pin + Forgejo chain** (F2 / Assumption 1, the highest-stakes unverified link). If 0d fails,
STOP — revert the flip (graceful-handoff back to
- Reuses: all prior scripts (the runbook invokes them); research-scout's verified Forgejo/git commands.
- Test first: (documentation deliverable — no unit test; verified by review + the dry-run in Step 11 exercising every script the runbook references)
- Verify:
test -f docs/marketplace-polyrepo-migration/migration/RUNBOOK.md && grep -q "auto_init" docs/marketplace-polyrepo-migration/migration/RUNBOOK.md && grep -q "Pilot" docs/marketplace-polyrepo-migration/migration/RUNBOOK.md→ expected: exit 0. - On failure: revert —
git checkout -- docs/marketplace-polyrepo-migration/migration/RUNBOOK.md. - Checkpoint:
git commit -m "docs(migration): operator-window runbook (pilot gate + batch order)" - Manifest:
manifest: expected_paths: - docs/marketplace-polyrepo-migration/migration/RUNBOOK.md min_file_count: 1 commit_message_pattern: "^docs\\(migration\\): operator-window runbook \\(pilot gate \\+ batch order\\)$" bash_syntax_check: [] forbidden_paths: - .claude-plugin/marketplace.json must_contain: - path: docs/marketplace-polyrepo-migration/migration/RUNBOOK.md pattern: "auto_init" - path: docs/marketplace-polyrepo-migration/migration/RUNBOOK.md pattern: "Pilot"
Step 11: Full local dry-run (all 11 targets, NULL push)
- Files:
docs/marketplace-polyrepo-migration/migration/99-dryrun.sh,docs/marketplace-polyrepo-migration/migration/99-dryrun.test.mjs - Changes: (new files)
99-dryrun.shruns Steps 3→4→5→6 (+ Step 7 for config-audit/okr/ human-friendly-style) across all 11 targets (10 plugins + DS) into$WORK, then exercises Step 8's rewriter (--all→--out) and Step 9's thinning (--workspace), and writes a generateddryrun-report.md(an output artifact, not committed): per-target extract commit count (F1 history retention), tag list (F5 clean re-tag), standalone PASS/FAIL (SC2), SC7 state-file check, and the marketplace.json/catalog-thin previews — with zero pushes and zero live-file mutation. It also runs the cross-repo DS-vendor validation (M9 — the gap the pilot can't cover): with both the DS extract and the ms-ai-architect extract present in$WORK, it runs the re-homed syncnode scripts/sync-design-system.mjs ms-ai-architect --source "$WORK/playground-design-system" --target "$WORK/ms-ai-architect"then--checkagainst the committedMANIFEST.jsonand asserts byte-parity — exercising the actual two-extracted-repo--source/--targetinvocation (SC5) locally, before the window. Honest scope of what the dry-run proves (M8): it proves the local mechanics — extraction, rename-aware history retention, re-rooting, reference rewrites, standalone validation, marketplace/ catalog transforms, and the DS-vendor chain. It cannot prove the window-only steps: (i) Forgejo accepting the push of post-strip history, (ii)auto_init:falserepo creation via the API, (iii) Claude Code resolving an HTTPSurl+refsource from self-hosted Forgejo (Assumption 1 — covered instead by the Step 10 pilot, not the dry-run). The dry-run + the pilot together gate the batch; neither alone is sufficient. - Reuses: every prior script.
- Test first:
- File:
docs/marketplace-polyrepo-migration/migration/99-dryrun.test.mjs(new) - Verifies:
dryrun-report.mdlists all 11 targets; voyage shows ≥250 commits; config-audit shows PASS under its SC2 gate (full suite minusjson-backcompat/raw-backcompat, after re-seed); no target shows a tracked state-file leak; the previewed all-external marketplace.json has zero./plugins/and zerossh://. - Pattern:
plugins/voyage/tests/integration/*.test.mjs
- File:
- Verify:
bash docs/marketplace-polyrepo-migration/migration/99-dryrun.sh→ expected:DRY-RUN OK — 11/11 targets extracted + validated, 0 pushes;git -C $REPO_ROOT status→ expected: working tree shows only the committedmigration/additions, no change toplugins/,shared/, or.claude-plugin/marketplace.json(D8 held). - On failure: escalate — any target FAIL is a real blocker to surface before the window; the
dry-run is reversible (everything in
$WORK/--out/--workspace). - Checkpoint:
git commit -m "chore(migration): full local dry-run harness + report" - Manifest:
manifest: expected_paths: - docs/marketplace-polyrepo-migration/migration/99-dryrun.sh - docs/marketplace-polyrepo-migration/migration/99-dryrun.test.mjs min_file_count: 2 commit_message_pattern: "^chore\\(migration\\): full local dry-run harness \\+ report$" bash_syntax_check: - docs/marketplace-polyrepo-migration/migration/99-dryrun.sh forbidden_paths: - .claude-plugin/marketplace.json - plugins - shared must_contain: - path: docs/marketplace-polyrepo-migration/migration/99-dryrun.sh pattern: "no-local"
Manifest — objective completion predicate
Every step above carries a Manifest. The defining invariant across all of them:
forbidden_paths always includes the live .claude-plugin/marketplace.json (and, for
extraction/validation/thinning steps, plugins/ + shared/) — this is the machine-checkable
enforcement of D8 (NULL push / no live-marketplace mutation) during the entire Claude-run local
phase. The live catalog and the plugins source are mutated only in the operator window
(Step 10's runbook), which is outside trekexecute's verified surface by design.
Failure recovery rules
- revert — script-authoring steps:
git checkout -- <files>; extraction/validation operate in$WORKso reverting isrm -rf $WORK/<key>. - retry — idempotent transforms (Steps 3–9, 11) re-run cleanly.
- escalate — a standalone-validation FAIL (Step 6/7/11) or a missing tool (Step 1) stops and asks the operator; never mask an SC2/SC6 failure.
- Checkpoint — each step commits locally (D8: no push), so a later failure cannot corrupt completed tooling.
Alternatives Considered
| Approach | Pros | Cons | Why rejected |
|---|---|---|---|
--subdirectory-filter plugins/<name> for every plugin (brief's literal §9 form) |
Simplest one-flag invocation | Silently drops 136+50+3 commits of pre-rename history for voyage/llm-security/linkedin-studio (F1) | Violates SC6 for 3 high-churn plugins; multi---path is mandatory for renamed plugins |
ssh://git@… url sources (brief §6) |
Matches the operator's SSH push workflow | marketplace add rejects SSH (#9740); url schema names only https:///git@ |
F2 — HTTPS is the only verified-accepted form; SSH risks a dead marketplace |
Hard-cut history (git init from current tree) for all repos |
Fast; no filter-repo | Loses granular blame/history | D2 ratified filter-repo preservation; only the archived monorepo keeps full history under hard-cut |
| Big-bang all-at-once cutover | One window, less bookkeeping | No reversible checkpoints; marketplace dark during cutover | D5 ratified incremental mixed-source; R1 |
| Run extraction in the working checkout | No extra clone | Races concurrent sessions on .git/index (R4); destructive to live tree |
§10 mandates a dedicated --no-local fresh clone |
| Skip the pilot, push all 11 then test | Fewer window steps | If Forgejo HTTPS/external-source resolution fails, all 11 are already pushed | F2 — the Forgejo external-source chain is the highest-stakes unverified link; pilot de-risks it on 1 repo |
Test Strategy
- Framework: Node
node:test(glob formnode --test path/*.test.mjs— Node 25 gotcha), zero npm deps (house style); bashvalidate-plugin.shfor structure checks. - Existing patterns: per-plugin
tests/**/*.test.mjs;claude-design/ms-ai-architectvalidate-plugin.sh; config-auditUPDATE_SNAPSHOTrebasing seam; the SHA-256MANIFEST.json+detectDriftinsync-design-system.mjs. - New tests in this plan: one test per script step (Steps 1–9, 11), each asserting the script's
contract against the live monorepo or a freshly-extracted repo in
$WORK— so the tooling is proven before the dry-run, and the dry-run (Step 11) is the integration test over all 11 targets.
Tests to write
| Type | File | Verifies | Model test |
|---|---|---|---|
| Unit | migration/00-preflight.test.mjs |
plugin-map shape, rename-awareness, HTTPS URLs | voyage/tests/validators/*.test.mjs |
| Unit | scripts/sync-design-system.test.mjs |
--source parity, --check drift |
voyage/tests/scripts/*.test.mjs |
| Integration | migration/10-extract.test.mjs |
renamed-plugin history retained, clean re-tag | voyage/tests/integration/*.test.mjs |
| Unit | migration/20-rehome-config.test.mjs |
re-rooted gitignore/gitleaks, mailmap copied | config-audit/tests/lib/*.test.mjs |
| Unit | migration/30-fix-references.test.mjs |
no ../../README.md, URLs reconciled |
linkedin-studio/render/__tests__/*.test.mjs |
| Integration | migration/40-validate-standalone.test.mjs |
SC2/SC7 gate fails on planted leak | voyage/tests/synthetic/*.test.mjs |
| Unit | migration/50-config-audit-snapshots.test.mjs |
portable snapshots pass; generic validator | config-audit/tests/snapshot-default-output.test.mjs |
| Unit | migration/60-rewrite-marketplace.test.mjs |
mixed-source, HTTPS, ref pin | voyage/tests/parsers/*.test.mjs |
| Unit | migration/70-thin-catalog.test.mjs |
no plugins/shared, CONVENTIONS.md present | config-audit/tests/*.test.mjs |
| Integration | migration/99-dryrun.test.mjs |
11/11 targets, SC2/SC6/SC7, NULL push | voyage/tests/integration/*.test.mjs |
Risks and Mitigations
| Priority | Risk | Location | Impact | Mitigation |
|---|---|---|---|---|
| Critical | Renamed-plugin history silently dropped (F1) | 10-extract.sh |
SC6 fails; voyage/linkedin-studio lose 30–60% history | Multi---path per plugin-map.json; Step 3 test asserts commit count from both names |
| Critical | Forgejo HTTPS external-source resolution unverified (F2) | window / RUNBOOK.md |
Marketplace could go dark if url source can't resolve self-hosted Forgejo |
Pilot gate (Step 10) proves the chain on 1 repo before the other 10; HTTPS not SSH; public repos |
| High | ms-ai-architect 148 MB blob-bomb (F3) | 10-extract.sh blob_strip |
Extracted repo bloated; slow push | --strip-blobs-bigger-than 1M (default, operator-confirmed); decision surfaced in Step 3 |
| High | config-audit snapshots embed absolute path (F4) | Step 7 | SC2 blocker | UPDATE_SNAPSHOT=1 re-seed of the rebasable snapshot-default-output in-clone; by-name exclude the 2 frozen back-compat tests (json-backcompat/raw-backcompat) from the SC2 gate; flagged for operator ratification (edges §5) |
| High | Tag collision/leak across extracts (F5) | 10-extract.sh |
Wrong tags in each repo; ambiguous v5.0.0 |
Strip all tags, re-create exactly v<version>; Step 3 test asserts single tag |
| High | Marketplace goes dark mid-migration (R1/SC8) | window | Broken installs | Mixed sources (Step 8 --only); never remove ./plugins/x before external repo verified; install-smoke each step |
| Medium | README/metadata reference rot (F6) | 30-fix-references.mjs |
404 disclosure links, wrong repo URLs standalone | Rewriter inlines disclosure + reconciles repository/homepage; Step 5 test asserts none remain |
| Medium | Root-only .mailmap/.gitleaks.* don't travel (F6/M5) |
20-rehome-config.sh |
Lost author dedup; re-flagged false positives | Copy .mailmap; generate per-plugin .gitleaks with re-rooted fingerprints |
| Medium | Concurrent-session corruption during surgery (R4) | all extraction | Index race in working tree | git clone --no-local fresh clone; working checkout read-only; quiesce other writers in the window |
| Medium | Untracked non-gitignored files leak on blanket add (M2) | extraction | progress.json + 2 briefs enter an extract |
filter-repo is history-based (untracked files excluded); Step 6 SC7 check catches leaks |
| Low | okr.local.md.template is intentionally tracked |
Step 6 | False SC7 failure | Explicit allow-exception in the SC7 check |
| Low | Forgejo no auto-update token for private repos (F2) | window | Background update of a private marketplace unsupported | Recommend public repos (§9); manual /plugin marketplace update otherwise |
Assumptions
| # | Assumption | Why unverifiable | Impact if wrong |
|---|---|---|---|
| 1 | Claude Code resolves a self-hosted Forgejo url source over HTTPS with a pinned ref |
Not testable until a repo exists on Forgejo (push is operator-gated, D8) | If false, the whole external-source model fails — the Step 10 pilot is the explicit test that gates the rest |
| 2 | git filter-repo carries each plugin's CHANGELOG.md + history intact under multi---path |
filter-repo tag/history edge-cases ("tags not on path") lack a crisp documented rule | Verified empirically by Step 3/11 tests (commit counts, tag list) before the window |
| 3 | Stripping blobs >1 MB from ms-ai-architect loses only screenshots, not source | Depends on whether any non-screenshot blob exceeds 1 MB | Step 3 surfaces the strip list as a decision; operator confirms before the window |
| 4 | Excluding config-audit's 2 frozen back-compat tests (json-backcompat/raw-backcompat) from SC2, plus re-seeding snapshot-default-output in-clone, is acceptable (edges §5 non-goal) |
A judgment on "what SC2 means" for fixtures that embed the original capture machine's path + a sibling marketplace + deleted plugins | Flagged for operator ratification (header + Step 7); fallback = the config-audit-internal work of normalizing the v5.0.0 snapshot paths (out of migration scope) |
| 5 | Public visibility is acceptable for all open/ plugin repos |
Visibility is a policy choice, not technical | Operator confirms at the window; private repos lose background auto-update (F2) |
Verification
Per-step manifests run during execution (Steps 1–11). These are the cross-step, end-to-end checks mapping to the brief's SC1–SC8.
- SC6 (F1):
git -C /tmp/polyrepo-migration/voyage log --oneline | wc -l→ expected:≥ 250(bothvoyage+ultraplan-localhistory present). - SC6 (F5):
git -C /tmp/polyrepo-migration/voyage tag→ expected: exactlyv5.1.1. - SC2/SC7:
bash docs/marketplace-polyrepo-migration/migration/40-validate-standalone.sh --all→ expected: every targetPASS(config-audit gate = fulltests/**/*.test.mjsminusjson-backcompat+raw-backcompat, after the Step 7 re-seed; linkedin-studio.mjscore is the hard gate, TS analytics ADVISORY); no tracked*.local.md/STATE.mdexceptokr/templates/okr.local.md.template. - SC5 (two extracted repos): with the DS + ms-ai-architect extracts in
$WORK,node scripts/sync-design-system.mjs ms-ai-architect --source "$WORK/playground-design-system" --target "$WORK/ms-ai-architect"thennode scripts/sync-design-system.mjs --check ms-ai-architect→ expected:MANIFEST OK;git -C "$WORK/ms-ai-architect" diff --exit-code playground/vendor/→ expected: empty (byte-identical re-vendor). - SC3 (F2):
node …/60-rewrite-marketplace.mjs --all --out /tmp/mp-all.json && grep -c '"./plugins/' /tmp/mp-all.json→ expected:0;grep -c 'ssh://' /tmp/mp-all.json→0. - SC4:
bash …/70-thin-catalog.sh --workspace /tmp/thin && ls /tmp/thin→ expected: noplugins/, noshared/;CONVENTIONS.mdpresent. - D8 held: after the full dry-run,
git -C $REPO_ROOT status --porcelain .claude-plugin/marketplace.json plugins shared→ expected: no output (live surfaces unchanged);git log origin/main..main --onelineshows only localmigration/commits, none pushed. - SC1/SC8 (operator window, post-pilot): per
RUNBOOK.md,/plugin marketplace add https://…/open/ktg-plugin-marketplace.git+/plugin install <plugin>@ktg-plugin-marketplacesucceeds for the pilot, then after each graduation.
Estimated Scope
- Files to modify: 1 (
scripts/sync-design-system.mjs). - Files to create: ~24 (11 migration scripts/docs + 11 tests + 2 templates), all under
docs/marketplace-polyrepo-migration/migration/+ 1 root test. - Repos produced (local, unpushed): 11 (10 plugins + DS) in
$WORKduring the dry-run. - Complexity: high (rename-aware history surgery, mixed-source live-marketplace choreography, operator-gated irreversibility) — but the Claude-run local half is fully reversible (NULL push, throwaway clones); only Step 10's window is irreversible and operator-run.
Execution Strategy
Steps exceed 5 → grouped into sessions/waves. The local half (Sessions 1–5) is trekexecutable now;
the window (Session 6) is operator-run from RUNBOOK.md, not via trekexecute.
Session 1: Foundation
- Steps: 1, 2
- Wave: 1
- Depends on: none
- Scope fence:
- Touch:
docs/marketplace-polyrepo-migration/migration/,scripts/sync-design-system.mjs(.test) - Never touch:
.claude-plugin/marketplace.json,plugins/,shared/
- Touch:
Session 2: Extraction + per-repo prep scripts
- Steps: 3, 4, 5
- Wave: 2
- Depends on: Session 1 (reads
plugin-map.json) - Scope fence:
- Touch:
docs/marketplace-polyrepo-migration/migration/ - Never touch:
plugins/,shared/,.claude-plugin/marketplace.json(extraction operates in$WORK)
- Touch:
Session 3: Validation + blockers
- Steps: 6, 7
- Wave: 3
- Depends on: Session 2 (validates extracted repos)
- Scope fence:
- Touch:
docs/marketplace-polyrepo-migration/migration/ - Never touch:
plugins/config-audit/live (Step 7 operates on the extract in$WORK)
- Touch:
Session 4: Catalog + window staging
- Steps: 8, 9, 10
- Wave: 3 (parallel with Session 3 — independent scripts)
- Depends on: Session 1 (versions/URLs)
- Scope fence:
- Touch:
docs/marketplace-polyrepo-migration/migration/ - Never touch: live
.claude-plugin/marketplace.json,CLAUDE.md,README.md(rewriters write to--out/--workspace)
- Touch:
Session 5: Local dry-run (de-risk proof)
- Steps: 11
- Wave: 4
- Depends on: Sessions 2, 3, 4 (exercises every script)
- Scope fence:
- Touch:
docs/marketplace-polyrepo-migration/migration/,$WORK - Never touch: any live source/catalog surface (D8 asserted by the step's own verify)
- Touch:
Session 6: Operator window (NOT trekexecute — operator-run from RUNBOOK.md)
- Steps: runbook only (pilot → DS → high-churn → low-churn → thin catalog)
- Wave: 5 (authorized weekend window, D8)
- Depends on: Session 5 green dry-run + operator authorization
- Scope fence:
- Touch: Forgejo (repo creation + push), live
.claude-plugin/marketplace.json(mixed→external), catalog thinning - Never touch: anything before the pilot passes
- Touch: Forgejo (repo creation + push), live
Execution Order
- Wave 1: Session 1
- Wave 2: Session 2 (after Session 1)
- Wave 3: Sessions 3 + 4 (parallel, after Session 2 / Session 1 respectively)
- Wave 4: Session 5 (after Sessions 2–4)
- Wave 5: Session 6 — operator window, gated on the dry-run + explicit authorization
Grouping rules applied
- Steps sharing the
plugin-map.jsoncontract → Session 1 first. - Extraction/prep scripts (independent files) → Session 2; validation depends on them → Session 3.
- Catalog/window staging is file-independent → Session 4 parallel with Session 3.
- The dry-run gates everything before the irreversible window.
Plan Quality Score
| Dimension | Weight | Score | Notes |
|---|---|---|---|
| Structural integrity | 0.15 | 88 | $WORK lifecycle now defined via a dedicated mirror clone; blob/re-tag ordering fixed (single-pass filter-repo) |
| Step quality | 0.20 | 84 | Deterministic blob decision (no runtime prompt); config-audit step corrected to the verified UPDATE_SNAPSHOT=1 seam; gitleaks fingerprint format fixed |
| Coverage completeness | 0.20 | 86 | SC1–SC8 + D1–D8 + R1–R8 + §9 traced; F1–F6 absorbed; DS cross-repo chain + .forgejo survival now covered |
| Specification quality | 0.15 | 85 | filter-repo/Forgejo/marketplace commands verified; config-audit framing corrected to the real test behaviour |
| Risk & pre-mortem | 0.15 | 86 | Pilot now flips-then-installs (actually tests the ref-pin chain); dry-run honestly scoped to local mechanics |
| Headless readiness | 0.10 | 80 | Local steps deterministic & headless-ready; window operator-run by design; TS analytics explicitly advisory |
| Manifest quality | 0.05 | 85 | Every step has a checkable manifest; expected_paths ⊆ Files:; D8 enforced via forbidden_paths |
| Weighted total | 1.00 | 85 | Grade: B+ |
Adversarial review (Phase 9):
- Plan critic: REVISE on the first draft (63/C) — 3 blockers + 8 major + 6 minor. **All 3 blockers
- all 8 majors + the actionable minors addressed** (see Revisions). Contested config-audit facts re-verified directly against the test code before revising.
- Scope guardian: ALIGNED — 0 scope gaps; all SC1–SC8 / D1–D8 / §9 / R1–R8 covered; 4 creep items all ACCEPTABLE (2 forced by relocation, 2 operator-ratified); 1 DEVIATION (ssh→https) surfaced for operator ratification in the header.
Revisions
| # | Finding (reviewer) | Severity | Resolution |
|---|---|---|---|
| 1 | Step 7 misdescribed config-audit tests; invented SKIP_ENV_SNAPSHOTS; wrong-glob under-ran the suite (critic) |
blocker | Re-verified the real tests. Rewrote Step 7 to use the actual UPDATE_SNAPSHOT=1 seam (re-seed snapshot-default-output) + by-name exclusion of the 2 frozen back-compat tests + the declared runner node --test 'tests/**/*.test.mjs' |
| 2 | Pilot couldn't test the ref-pin chain — install resolved the in-repo source (critic) | blocker | Step 10 pilot reordered: create→push→flip the entry to external url+ref & push catalog→then install-smoke, so the HTTPS+ref chain is actually exercised |
| 3 | blob_strip left as a runtime prompt (non-headless) (critic) |
major | Step 1 preflight enumerates >1 MB blobs and emits blob_strip_safe; Step 3 picks size-strip vs surgical path-glob deterministically — no prompt |
| 4 | filter-repo second-pass + re-tag ordering hazard (critic) | major | Single filter-repo invocation composing --path/--path-rename/--strip-blobs-bigger-than; re-tag as the final op |
| 5 | $WORK/$REPO_ROOT undefined; concurrency race reintroduced (critic) |
major | Step 1 builds a dedicated $WORK/_mirror once; all extractions clone from it; per-step verifies self-contained |
| 6 | .gitleaksignore re-rooting used wrong format path:line (critic) |
major | Step 4 fixed to path:rule-id:line (keep generic-api-key/linkedin-client-id + line); test assertion corrected |
| 7 | DS cross-repo vendor chain never validated before window (critic) | major | Step 11 runs the re-homed sync --source/--target across the two extracted repos + --check parity (SC5) |
| 8 | Dry-run oversold as proving the migration (critic) | major | Step 11 reworded to local-mechanics only; window-only modes (push/create/resolve) explicitly listed as pilot-covered |
| 9 | expected_paths not ⊆ Files: in every step (critic) |
major | Added each .test.mjs to its step's Files: line |
| 10 | linkedin-studio TS suite silently waived for SC2 (critic) | major | Step 6 states two-tier SC2: .mjs core = hard gate; TS analytics = explicit ADVISORY (network-gated) |
| 11 | Preflight HEAD/status assertions go stale (critic) | minor | Relaxed to porcelain on plugins/ shared/ marketplace.json scripts only; idempotent |
| 12 | Step 5 inline-vs-link either/or; ai-psychosis mis-scoped (critic) | minor | Inline chosen; ai-psychosis noted verify-only, real work = 9 monorepo-pointing plugins |
| 13 | Step 9 doc-fix coupling; .forgejo CI uncovered; pre-claimed grade (critic) |
minor | Version correction coupled to the D6 README rewrite; Step 6 asserts .forgejo/ survives; grade set post-review |
| 14 | ssh→https overrides a ratified brief literal without annotation (guardian) | deviation | Surfaced as a brief correction requiring operator ratification in the header; kept HTTPS (correct per #9740) |