ktg-plugin-marketplace/docs/marketplace-polyrepo-migration/migration/run-operator-window.sh
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

216 lines
11 KiB
Bash
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.

#!/usr/bin/env bash
# run-operator-window.sh — turnkey execution of the polyrepo operator window (RUNBOOK §0§4).
#
# RUN AS THE OPERATOR, via the chat `!` prefix: ! bash docs/marketplace-polyrepo-migration/migration/run-operator-window.sh
# NOT via Claude's Bash tool — creating public repos + bulk-pushing trees to new remotes is gated by the
# auto-mode safety classifier (by design). Running it yourself via `!` is the sanctioned path: it executes
# as you, so the classifier / permission layer / push-window hook are all out of the picture.
#
# Idempotent + stop-on-first-failure. Safe to re-run after a partial run (created repos → 409 ok, pushed
# refs → up-to-date, already-flipped catalog entries → skipped).
#
# DEFAULT (no args) — §0§3, REVERSIBLE:
# For all 11 targets in RUNBOOK order: (a) validate the extract → (b) create the Forgejo repo
# (auto_init:false, public) → (c) push the extract over SSH → (d) verify HTTPS+ref resolution
# (the install-smoke PROXY — a real `/plugin install` still needs a fresh Claude Code session) →
# (e) flip marketplace.json to the external nested source + push the catalog.
# graceful-handoff runs first as the PILOT gate. Standing up the 11 repos is what unblocks per-plugin
# parallel work — thinning is NOT required for that. Every flip is `git revert`-able while ./plugins/<k>
# still exists, so this whole phase is reversible.
#
# --thin — §4, build + verify the thin-catalog preview (no apply).
# CONFIRM_THIN=1 ... --thin — §4 APPLY: git rm plugins/ shared/ sync-script, swap in CONVENTIONS/CLAUDE/
# README, commit, push, push the pre-polyrepo-archive tag. IRREVERSIBLE point of no easy return —
# run only after the real `/plugin install` smoke-tests for all 11 have passed.
#
# Prereqs: $FORGEJO_TOKEN exported (Keychain → ~/.zshenv); on branch main; the 11 extracts buildable in
# $WORK (the validate step self-extracts if missing — that self-heal also rebuilds the mirror via preflight).
set -uo pipefail
HOST="git.fromaitochitta.com"
API="https://$HOST/api/v1"
MIG="$(cd "$(dirname "$0")" && pwd)"
ROOT="$(cd "$MIG/../../.." && pwd)"
MAP="$MIG/plugin-map.json"
LIVE="$ROOT/.claude-plugin/marketplace.json"
WORK="${WORK:-/tmp/polyrepo-migration}"
ARCHIVE_TAG="pre-polyrepo-archive"
PILOT="graceful-handoff"
REST="playground-design-system voyage llm-security linkedin-studio ms-ai-architect config-audit okr ai-psychosis human-friendly-style claude-design"
# The shared design-system is a standalone repo (stood up so consumers can vendor from an upstream), but it
# is NOT a marketplace plugin: it has no .claude-plugin/plugin.json and no marketplace.json entry. Steps (d)
# and (e) special-case it (assert its DS root marker; no catalog flip — mirrors 60-rewrite --all).
DS_KEY="playground-design-system"
die() { printf '\n✖ %s\n' "$*" >&2; exit 1; }
say() { printf '%s\n' "$*"; }
# ---- preconditions ----
[ -n "${FORGEJO_TOKEN:-}" ] || die "FORGEJO_TOKEN not set — export it from Keychain via ~/.zshenv first."
command -v python3 >/dev/null 2>&1 || die "python3 not found"
command -v node >/dev/null 2>&1 || die "node not found"
command -v curl >/dev/null 2>&1 || die "curl not found"
[ -f "$MAP" ] || die "plugin-map.json missing at $MAP"
[ -f "$LIVE" ] || die "live marketplace.json missing at $LIVE"
BR="$(git -C "$ROOT" rev-parse --abbrev-ref HEAD)"
[ "$BR" = "main" ] || die "catalog repo not on main (on '$BR')"
mapget() { python3 -c "import json;print(json.load(open('$MAP'))['targets']['$1'].get('$2',''))"; }
entry_present() {
python3 -c "import json
print('yes' if any(x['name']=='$1' for x in json.load(open('$LIVE'))['plugins']) else 'no')"
}
entry_is_external() {
python3 -c "import json
p=[x for x in json.load(open('$LIVE'))['plugins'] if x['name']=='$1']
print('yes' if (p and isinstance(p[0].get('source'),dict)) else 'no')"
}
count_local() {
python3 -c "import json
print(sum(1 for x in json.load(open('$LIVE'))['plugins'] if isinstance(x.get('source'),str) and x['source'].startswith('./plugins/')))"
}
rollout_one() {
key="$1"
tag="$(mapget "$key" tag)"
[ -n "$tag" ] || die "no tag for $key in plugin-map.json"
say ""
say "==== $key (tag $tag) ===="
# (a) validate the extract — self-extracts if $WORK/$key is absent.
# SC2 is REGRESSION-RELATIVE (the contract the Step-11 dry-run validated): a target passes iff the
# extraction introduces NO NEW failure. Pre-existing in-repo red (voyage's 2 doc-consistency drifts,
# ai-psychosis's 1) is the plugin's own concern — 41-validate-or-regression.sh enforces that exact
# contract (strict 40 first, then standalone-failing ⊆ in-repo-failing) so the window does NOT STOP on
# red the rehearsal blessed. config-audit keeps its dedicated deterministic gate.
say " [a] validate extract…"
if [ "$key" = "config-audit" ]; then
WORK="$WORK" bash "$MIG/50-config-audit-sc2.sh" >/dev/null 2>&1 || die "$key SC2 gate FAILED — STOP"
else
sc2out="$(WORK="$WORK" bash "$MIG/41-validate-or-regression.sh" "$key" 2>&1)" \
|| die "$key standalone validation FAILED (incl. regression-relative SC2) — STOP${sc2out:+ :: $sc2out}"
case "$sc2out" in *pre-existing*) say " ${sc2out#*: }";; esac
fi
# (b) create the Forgejo repo (201 created | 409 already exists)
say " [b] create repo open/${key}"
body="$(mktemp)"
code="$(curl -sS -o "$body" -w '%{http_code}' -X POST "$API/orgs/open/repos" \
-H "Authorization: token $FORGEJO_TOKEN" -H "Content-Type: application/json" \
-d "{\"name\":\"$key\",\"private\":false,\"auto_init\":false,\"default_branch\":\"main\"}")"
case "$code" in
201) say " created";;
409) say " already exists (ok, idempotent)";;
*) cat "$body" >&2; rm -f "$body"; die "$key repo-create unexpected HTTP $code — STOP";;
esac
rm -f "$body"
# (c) push the extract over SSH (proven auth; the marketplace source URL stays HTTPS)
say " [c] push extract over SSH…"
( cd "$WORK/$key" || exit 1
git remote remove origin >/dev/null 2>&1 || true
git remote add origin "ssh://git@$HOST/open/$key.git" || exit 1
git push origin --all || exit 1
git push origin --tags || exit 1
) || die "$key push FAILED — STOP"
# (d) install-smoke PROXY: HTTPS clone at the pinned tag (the resolution path users hit)
say " [d] verify HTTPS+ref resolution…"
sm="$(mktemp -d)"
git clone --quiet --branch "$tag" "https://$HOST/open/$key.git" "$sm/r" >/dev/null 2>&1 \
|| die "$key does NOT resolve over HTTPS at $tag — STOP, diagnose the Forgejo/HTTPS/ref chain"
if [ "$key" = "$DS_KEY" ]; then
# design-system: no plugin.json — assert its DS root marker instead (tokens.css, per plugin-map test_cmd)
[ -f "$sm/r/tokens.css" ] || die "$key clone missing tokens.css (DS root marker) — STOP"
else
[ -f "$sm/r/.claude-plugin/plugin.json" ] || die "$key clone missing .claude-plugin/plugin.json — STOP"
fi
say " resolves: https://$HOST/open/$key @ $tag"
# (e) flip the catalog entry to the external nested source (idempotent) + push.
# The design-system has no marketplace entry (consumers vendor it) — nothing to flip; this mirrors
# 60-rewrite --all, which only touches the 10 plugins actually present in marketplace.json.
if [ "$(entry_present "$key")" = "no" ]; then
say " [e] $key has no marketplace entry (shared design-system, vendored by consumers) — no catalog flip"
elif [ "$(entry_is_external "$key")" = "yes" ]; then
say " [e] catalog already external for $key (skip)"
else
say " [e] flip catalog → external + push…"
node "$MIG/60-rewrite-marketplace.mjs" --only "$key" --in "$LIVE" --out /tmp/mp-rollout.json >/dev/null \
|| die "$key marketplace rewrite FAILED — STOP"
cp /tmp/mp-rollout.json "$LIVE" || die "$key cp marketplace.json FAILED"
git -C "$ROOT" add .claude-plugin/marketplace.json || die "$key git add FAILED"
git -C "$ROOT" commit -q -m "chore(marketplace): externalise $key" || die "$key catalog commit FAILED"
git -C "$ROOT" push origin main || die "$key catalog push FAILED — STOP"
say " flipped + pushed"
fi
say "$key DONE"
}
# ======================== §4 thinning ========================
do_thin() {
say "OPERATOR WINDOW — §4 thin catalog"
locals="$(count_local)"
[ "$locals" = "0" ] || die "$locals catalog entries still local — finish §0§3 rollout before thinning"
ws="$(mktemp -d)"
bash "$MIG/70-thin-catalog.sh" --workspace "$ws" >/dev/null || die "thin-catalog preview FAILED"
[ ! -d "$ws/plugins" ] || die "thin preview still contains plugins/"
[ ! -d "$ws/shared" ] || die "thin preview still contains shared/"
[ -f "$ws/CONVENTIONS.md" ] || die "thin preview missing CONVENTIONS.md"
say " thin preview verified at $ws (no plugins/ shared/, CONVENTIONS.md present)"
if [ "${CONFIRM_THIN:-}" != "1" ]; then
say ""
say " PREVIEW ONLY (irreversible apply is gated). To APPLY:"
say " CONFIRM_THIN=1 bash $0 --thin"
exit 0
fi
say " APPLYING irreversible thin state to the live catalog…"
git -C "$ROOT" rm -r -q --ignore-unmatch plugins shared scripts/sync-design-system.mjs scripts/sync-design-system.test.mjs || true
cp "$ws/CONVENTIONS.md" "$ROOT/CONVENTIONS.md" || die "cp CONVENTIONS.md FAILED"
cp "$ws/CLAUDE.md" "$ROOT/CLAUDE.md" || die "cp CLAUDE.md FAILED"
cp "$ws/README.md" "$ROOT/README.md" || die "cp README.md FAILED"
git -C "$ROOT" add CONVENTIONS.md CLAUDE.md README.md || die "git add (thin docs) FAILED"
git -C "$ROOT" add -A plugins shared scripts 2>/dev/null || true
git -C "$ROOT" commit -q -m "chore(marketplace): thin catalog to manifest + docs (polyrepo migration complete)" \
|| die "thin commit FAILED"
git -C "$ROOT" push origin main || die "thin push FAILED"
git -C "$ROOT" push origin "$ARCHIVE_TAG" || die "archive-tag push FAILED"
say ""
say "✓ THINNING COMPLETE — catalog is manifest + docs only; archive tag $ARCHIVE_TAG pushed."
say " POLYREPO MIGRATION COMPLETE."
exit 0
}
# ======================== main ========================
if [ "${1:-}" = "--thin" ]; then
do_thin
fi
say "OPERATOR WINDOW — §0§3 rollout (create + push + flip; REVERSIBLE)"
say "Targets in order: $PILOT (pilot) → $REST"
say ""
say "### PILOT: $PILOT — must pass before the other 10 ###"
rollout_one "$PILOT"
say ""
say "### PILOT PASSED — rolling out the remaining 10 ###"
for k in $REST; do rollout_one "$k"; done
say ""
say "──────────────────────────────────────────────────────────────"
say "✓ ROLLOUT COMPLETE — 11 repos created + pushed; marketplace.json all-external (nested HTTPS+ref)."
say " Each plugin now lives at https://$HOST/open/<name> — clone & work on them in parallel."
say ""
say " NEXT (recommended before thinning) — real install-smoke per plugin in a FRESH Claude Code session:"
say " /plugin marketplace update"
say " /plugin install <name>@ktg-plugin-marketplace # confirm commands/skills/agents load"
say " Reversible until thinning: 'git revert' the externalise commit and ./plugins/<name> resolves again."
say ""
say " When every install-smoke passes, run the irreversible cleanup:"
say " CONFIRM_THIN=1 bash $0 --thin"