diff --git a/docs/marketplace-polyrepo-migration/migration/RUNBOOK.md b/docs/marketplace-polyrepo-migration/migration/RUNBOOK.md new file mode 100644 index 0000000..f65b5ba --- /dev/null +++ b/docs/marketplace-polyrepo-migration/migration/RUNBOOK.md @@ -0,0 +1,190 @@ +# 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 + +- 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) +if [ "$KEY" = "config-audit" ]; then + WORK=$WORK bash MIG/50-config-audit-sc2.sh # config-audit: SC2 PASS (...) +else + WORK=$WORK bash MIG/40-validate-standalone.sh "$KEY" # : PASS (...) +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.