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

197 lines
9.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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):
```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=<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
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 <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.