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>
9.5 KiB
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) andpython3 >= 3.6onPATH— the extraction (10-extract.sh) and every map-driven script depend on them.00-preflight.shand10-extract.shboth 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_TOKENis exported (macOS Keychain →~/.zshenv) withwrite:org+reposcope.- The
pre-polyrepo-archivetag 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):
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):
- 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). - High-churn —
voyage→llm-security→linkedin-studio→ms-ai-architect.ms-ai-architectcarries the 148 MB screenshot blob-bomb (F3); the extract strips >1 MB blobs whenblob_strip_safe(verified true), so its push is a normal size. Confirm the push completes.
- Low-churn —
config-audit→okr→ai-psychosis→human-friendly-style→claude-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-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 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-archivetag 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.