ktg-plugin-marketplace/docs/marketplace-polyrepo-migration/migration/run-operator-window.sh
Kjell Tore Guttormsen f35e4ec46d fix(migration): multiplex operator-window SSH pushes through one master connection
Forgejo rate-limits rapid port-22 handshakes. The bare per-target
`git push --all` + `git push --tags` opened ~22 SSH connections in rapid
succession; the server refused around the 6th (voyage's tag push) with
"port 22: Connection refused" (TCP-level, not auth), deterministically, on
every run. Export GIT_SSH_COMMAND with ControlMaster/ControlPath/ControlPersist
so all git-over-SSH reuses ONE persistent master connection (one TCP handshake
for the whole rollout).

Verified: 8 rapid multiplexed connections all succeed where the 6th bare
connection is refused; the full 11-target rollout then completed clean.

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

225 lines
12 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')"
# ---- SSH connection multiplexing (REQUIRED — Forgejo rate-limits rapid port-22 handshakes) ----
# The bare per-target `git push --all` + `git push --tags` opens 2 SSH connections per target (22 total)
# in rapid succession; Forgejo refuses around the 6th handshake ("ssh: connect to host ... port 22:
# Connection refused" — TCP-level, not auth), which killed voyage's tag push on every run. Route ALL
# git-over-SSH through ONE persistent master connection so the whole rollout costs a single TCP handshake.
# Verified: 8 rapid multiplexed connections all succeed where the 6th bare connection is refused.
# ControlPersist keeps the master alive across the inter-target validate/clone gaps.
export GIT_SSH_COMMAND="ssh -o ControlMaster=auto -o ControlPath=/tmp/polyrepo-ssh-%C -o ControlPersist=600"
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"