# Polyrepo migration — operator-window RUNBOOK The single document the operator follows in the authorized window. Everything before this (Steps 1–9, plus the Step 11 dry-run) was authored and verified locally with **NULL push (D8)**. This runbook is the **only** place live Forgejo repos are created and the live `marketplace.json` is mutated. ## Preconditions - **`git-filter-repo`** (a non-default git extension) and **`python3 >= 3.6`** on `PATH` — the extraction (`10-extract.sh`) and every map-driven script depend on them. `00-preflight.sh` and `10-extract.sh` both assert them, but install first: `brew install git-filter-repo python3`. - All migration scripts committed locally; the Step 11 dry-run is green (it exercises every script below). - `$FORGEJO_TOKEN` is exported (macOS Keychain → `~/.zshenv`) with `write:org` + `repo` scope. - The `pre-polyrepo-archive` tag exists locally (D2) — the rollback anchor for the whole operation. - **Push window (private-work policy):** this runbook pushes to Forgejo. Run it only inside the allowed push window — weekdays 20:00–23:00 (Europe/Oslo), or any time on weekends / Norwegian holidays. - `WORK=/tmp/polyrepo-migration` (the mirror + per-target extracts the scripts build). - Visibility: **public** (§9) — every repo is created `"private": false`. Paths below assume you are in the catalog repo root unless stated. Scripts live in `docs/marketplace-polyrepo-migration/migration/` (referred to as `MIG/`). --- ## (0) Pilot gate — `graceful-handoff` The pilot proves the highest-stakes unverified link: **HTTPS + external `source` + `ref` pin + Forgejo resolution** (F2 / Assumption 1). Pilot = `graceful-handoff` (low-churn, has tests, no design-system vendor, no blob bloat). > **B3 ordering — non-negotiable:** the catalog entry MUST be flipped to external *before* the > install-smoke. If you install while the entry is still `./plugins/graceful-handoff`, the install > resolves the **local** copy and the test is vacuous — it proves nothing about the Forgejo chain. **(0a) Create the repo on Forgejo** (`auto_init:false` so the first push defines history — an auto-initialised repo would create a divergent root commit the rename-aware extract cannot fast-forward): ```bash curl -fsS -X POST "https://git.fromaitochitta.com/api/v1/orgs/open/repos" \ -H "Authorization: token $FORGEJO_TOKEN" \ -H "Content-Type: application/json" \ -d '{"name":"graceful-handoff","private":false,"auto_init":false,"default_branch":"main"}' ``` **(0b) Push the extracted repo** (the extract was prepared + validated by the harness; re-run it to be sure it is green, then push all branches + tags): ```bash WORK=$WORK bash MIG/40-validate-standalone.sh graceful-handoff # must print: graceful-handoff: PASS (...) cd "$WORK/graceful-handoff" git remote add origin https://git.fromaitochitta.com/open/graceful-handoff.git git push origin --all git push origin --tags # publishes v2.1.0 — the ref the catalog will pin cd - # back to the catalog repo ``` **(0c) Flip the catalog entry to external and push the catalog in its still-mixed state** (9× `./plugins/x` + 1× external; `plugins/` is still present — thinning is last): ```bash node MIG/60-rewrite-marketplace.mjs --only graceful-handoff \ --in .claude-plugin/marketplace.json --out /tmp/mp.json # review /tmp/mp.json: graceful-handoff is now {source:"url", url:..., ref:"v2.1.0"}, the other 9 unchanged cp /tmp/mp.json .claude-plugin/marketplace.json git add .claude-plugin/marketplace.json git commit -m "chore(marketplace): externalise graceful-handoff (pilot)" git push origin main ``` **(0d) Install-smoke from a FRESH Claude Code session** — the actual Forgejo-chain test: ```text /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 at `ref: v2.1.0`**. Confirm the plugin's commands/skills load. > **If 0d fails: STOP.** Revert the flip (§ Rollback) — graceful-handoff goes back to > `./plugins/graceful-handoff`, the marketplace stays live — and diagnose the HTTPS/ref/Forgejo chain > before touching any of the other 10. Do not proceed past a failed pilot. --- ## (1)–(3) Roll out the rest, one repo at a time Order (lower risk last so problems surface while the blast radius is small): 1. **Design-system first** — `playground-design-system` (the 2 consumers, llm-security + ms-ai-architect, vendor it; standing it up first means their vendored copies have an upstream to point at). 2. **High-churn** — `voyage` → `llm-security` → `linkedin-studio` → `ms-ai-architect`. - `ms-ai-architect` carries the 148 MB screenshot blob-bomb (F3); the extract strips >1 MB blobs when `blob_strip_safe` (verified true), so its push is a normal size. Confirm the push completes. 3. **Low-churn** — `config-audit` → `okr` → `ai-psychosis` → `human-friendly-style` → `claude-design`. **Per-repo procedure (identical for every target):** ```bash KEY= # e.g. voyage # a. extract + validate standalone (config-audit uses its dedicated SC2 gate). # SC2 is REGRESSION-RELATIVE (the contract the Step-11 dry-run validated): 41 runs 40 strict, then on # failure passes iff the standalone failing-test set is a SUBSET of the live in-repo set — so pre-existing # in-repo red (e.g. voyage's 2 doc-consistency drifts, ai-psychosis's 1) does NOT STOP the rollout, while # a genuine extraction-introduced regression still does. (Strict 40 alone would STOP on that blessed red.) if [ "$KEY" = "config-audit" ]; then WORK=$WORK bash MIG/50-config-audit-sc2.sh # config-audit: SC2 PASS (...) else WORK=$WORK bash MIG/41-validate-or-regression.sh "$KEY" # : PASS (standalone strict | N pre-existing) fi # b. create the Forgejo repo (auto_init:false, public) curl -fsS -X POST "https://git.fromaitochitta.com/api/v1/orgs/open/repos" \ -H "Authorization: token $FORGEJO_TOKEN" -H "Content-Type: application/json" \ -d "{\"name\":\"$KEY\",\"private\":false,\"auto_init\":false,\"default_branch\":\"main\"}" # c. push the extracted repo cd "$WORK/$KEY" git remote add origin "https://git.fromaitochitta.com/open/$KEY.git" git push origin --all git push origin --tags cd - # d. flip this entry in the catalog (cumulative — previously-flipped entries pass through unchanged) node MIG/60-rewrite-marketplace.mjs --only "$KEY" \ --in .claude-plugin/marketplace.json --out /tmp/mp.json cp /tmp/mp.json .claude-plugin/marketplace.json git add .claude-plugin/marketplace.json git commit -m "chore(marketplace): externalise $KEY" git push origin main # e. INSTALL-SMOKE (SC8) before moving to the next target — fresh Claude Code session: # /plugin marketplace update # /plugin install $KEY@ktg-plugin-marketplace ``` **Install-smoke order by surface** (largest command surface first, so the most likely to expose a resolution problem is caught early): `linkedin-studio` (29 commands) → `llm-security` → `config-audit` → `ms-ai-architect` → `voyage` → the rest. For each: confirm commands/skills/agents register and one representative command runs. > Do not start a target until the previous target's install-smoke has passed. A mixed-source catalog > (some `./plugins/x`, some external) is a fully valid live state (SC3/SC8) — that is exactly what makes > the one-at-a-time rollout safe. --- ## (4) Thin the catalog — LAST Only after **all 11 repos are pushed and every install-smoke has passed**: ```bash bash MIG/70-thin-catalog.sh --workspace /tmp/thin-catalog # review /tmp/thin-catalog: no plugins/ or shared/, CONVENTIONS.md present, README.md all-external ``` Apply the thin state to the catalog repo (remove `plugins/`, `shared/`, `scripts/sync-design-system.mjs`, add `CONVENTIONS.md`, swap in the rewritten `README.md` + thinned `CLAUDE.md`), commit, then: ```bash git push origin main git push origin pre-polyrepo-archive # publish the rollback anchor tag (D2) ``` The marketplace.json is now fully external; the catalog hosts only the manifest + landing/governance/ conventions docs. --- ## Ref updates (steady state) When a plugin cuts a new release, bump its `ref` in `.claude-plugin/marketplace.json` (re-run `60-rewrite-marketplace.mjs --only ` after updating the tag in `plugin-map.json`, or edit the `ref` by hand), push the catalog, and consumers pick it up with `/plugin marketplace update`. --- ## Rollback A flip is **reversible right up until the `./plugins/` source is removed** (i.e. until thinning). Order of preference: - **Single bad flip (pre-thinning):** `git revert ` and push — the entry returns to `./plugins/`, which is still present, so the marketplace works immediately. Then fix the external repo and re-flip. - **Never remove a `./plugins/` source before that plugin's external repo is pushed AND its install-smoke has passed** (R1 / SC8). Thinning (step 4) is the point of no easy return — do it only when every external repo is verified live. - **Whole-operation anchor:** the `pre-polyrepo-archive` tag is the pre-migration state of the monorepo. If the catalog needs to be reset wholesale, reset to that tag (local) before it was force-published. ## Failure stops - Pilot (0d) fails → STOP, revert, diagnose the Forgejo/HTTPS/ref chain. Touch nothing else. - Any per-repo push or install-smoke fails → STOP at that target, revert its flip, leave the rest live. - Never thin the catalog while any plugin is still `./plugins/`-only without a verified external repo.