ktg-plugin-marketplace/docs/marketplace-polyrepo-migration/migration/RUNBOOK.md
Kjell Tore Guttormsen 3403648c6c fix(migration): operator-window SC2 must be regression-relative, not strict
The operator window's step [a] called 40-validate-standalone.sh directly (strict
exit-code), so it STOPped on the first target carrying pre-existing in-repo test
red — voyage (2 doc-consistency drifts re phase_models/phase_signals, content moved
to docs/operations.md) and ai-psychosis (1). But the migration's ratified contract,
the one the Step-11 dry-run validated as PASS 11/11, is 'introduce no regression':
pre-existing in-repo red is the plugin's own concern, not a migration regression.
The window enforced a STRICTER gate than the contract the dry-run signed off.

Fix: new 41-validate-or-regression.sh — the single per-target gate the window calls
in [a]. It runs 40 strict, then on failure passes iff the standalone failing-test
NAME set is a SUBSET of the live in-repo set (the exact decision 99-dryrun.sh makes),
reusing sc2-regression.sh. A genuine extraction-introduced regression still STOPs the
window; a structure-validator fail and the config-audit gate stay strict.

Single-source the failing-name capture: extract capture_fails into capture-fails.sh
(mirrors the sc2-regression.sh extraction) so the live gate and the dry-run agree on
what 'failing' means; 99-dryrun.sh now delegates to it (behaviour identical).

Verified end-to-end on the real extracts: 40 strict fails voyage+ai-psychosis while
41 passes them 'N pre-existing, regression-relative'; clean targets (llm-security,
graceful-handoff) still pass via the strict path. New hermetic tests: capture-fails
3/3, 41 6/6 (strict-pass, regression-relative-pass, genuine-regression-fail,
structure-not-eligible, gate pass/fail). RUNBOOK per-repo step updated to 41.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 06:25:55 +02:00

9.5 KiB
Raw Blame History

Polyrepo migration — operator-window RUNBOOK

The single document the operator follows in the authorized window. Everything before this (Steps 19, 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:0023: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):

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):

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):

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:

/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 firstplayground-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-churnvoyagellm-securitylinkedin-studioms-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-churnconfig-auditokrai-psychosishuman-friendly-styleclaude-design.

Per-repo procedure (identical for every target):

KEY=<target>     # 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"    # <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-securityconfig-auditms-ai-architectvoyage → 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 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:

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 <key> 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/<key> source is removed (i.e. until thinning). Order of preference:

  • Single bad flip (pre-thinning): git revert <the externalise commit> and push — the entry returns to ./plugins/<key>, which is still present, so the marketplace works immediately. Then fix the external repo and re-flip.
  • Never remove a ./plugins/<key> 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/<key>-only without a verified external repo.