ktg-plugin-marketplace/docs/marketplace-polyrepo-migration/plan.md
Kjell Tore Guttormsen a44e37b71f docs(migration): polyrepo migration plan (11 steps, adversarially reviewed + revised)
Plan for splitting the monorepo into per-plugin Forgejo repos + a DS repo
+ a thin catalog. Built from the ratified brief via /trekplan: 6-agent
exploration verified the brief and surfaced 6 under-scoped findings
(F1-F6: plugin renames, ssh->https, 148MB blob bloat, config-audit
snapshot path-coupling, tag namespace, README link rot). plan-critic
REVISE on first draft (3 blockers + 8 majors) all addressed after
re-verifying contested facts; scope-guardian ALIGNED. Local Claude half
(Steps 1-11) is fully reversible NULL-push; operator window is a separate
runbook. Held unpushed per D8.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 11:49:22 +02:00

985 lines
69 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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.

# Marketplace Polyrepo Migration — Implementation Plan
> **Plan quality: B+** (85/100) — APPROVE_WITH_NOTES (revised after adversarial review — Phase 9)
>
> Generated by trekplan v5.1.1 on 2026-06-17 — `plan_version: 1.7`
> Brief: `docs/marketplace-polyrepo-migration/brief.md` (RATIFIED, D1D8). Profile: premium (Opus).
>
> **⚠ Brief corrections requiring operator ratification** (the plan overrides two literal values
> in the RATIFIED brief — surface these for explicit annotation, do not absorb silently):
> 1. **`ssh://` → `https://`** (F2). Brief SC1 (§4) and the §6 entry shape literally specify
> `ssh://git@…`; D4 ratified only *pin-to-tag*, silent on scheme. Claude Code rejects SSH
> marketplace sources (issue #9740) → the plan uses `https://git.fromaitochitta.com/open/<repo>.git`
> everywhere. Correct engineering call, but it edits a ratified literal.
> 2. **config-audit SC2 carve-out** (F4). Two back-compat test files
> (`json-backcompat.test.mjs`, `raw-backcompat.test.mjs`) assert byte-stability of a capture that
> embeds the original machine's absolute path + a sibling marketplace + deleted plugins; the plan
> **excludes them by name** from config-audit's SC2 gate (Step 7) — touches test fixtures, edges
> the §5 non-goal. Operator must accept the carve-out.
## Context
The `ktg-plugin-marketplace` monorepo (10 plugins under `plugins/`, a shared design system under
`shared/`, a catalog at the root) is split into **one Forgejo repo per plugin + a versioned
design-system repo + a thin catalog repo at the same URL**. The plugins are already independent
units (independent versions, self-contained `plugin.json`/`README`/`CLAUDE.md`, **no live
cross-plugin runtime imports** — confirmed hard by exploration), so the split aligns structure with
reality and removes concurrent-session git contention structurally rather than mitigating it. The
marketplace must stay installable and live at **every** step (mixed in-repo + external sources).
This plan covers the **Claude-run, local, reversible half**: it builds the migration tooling, runs a
full **local dry-run extraction with standalone validation**, and stages (but never runs) the
outward steps — all with **NULL push** (honoring D8). The **operator-run irreversible window**
(Forgejo repo creation, first push, live `marketplace.json` cutover) is captured as a runbook
deliverable (Step 10) + the Execution Strategy, not as machine-verified steps, because those are not
local file edits.
Every decision below traces to the brief: the goal (§3), success criteria SC1SC8 (§4), the scope
fence (§5), decisions D1D8 (§7), risks R1R8 (§8), the five §9 open questions, and the execution
model (§10). Exploration surfaced **six findings the brief under-scoped** (F1F6 below); the plan
absorbs all six.
### Exploration deltas the brief did not have (load-bearing)
- **F1 — Plugin renames split history.** `voyage``ultraplan-local` (136 commits), `llm-security`
`llm-security-copilot` (3), `linkedin-studio``linkedin-thought-leadership` (50). A single-path
filter (`--subdirectory-filter plugins/voyage`) **silently drops** the pre-rename history. Renamed
plugins MUST be extracted with multiple `--path` arguments (both names) → SC6.
- **F2 — HTTPS, not `ssh://`.** The brief's §6 `ssh://git@…` `url` source is **unverified/likely
rejected** (Claude Code issue #9740 shows `marketplace add` rejecting SSH). HTTPS
(`https://git.fromaitochitta.com/open/<repo>.git`) is the only form verified-accepted on both the
`url` source schema and `marketplace add`. The discriminator key is **`source`** (brief correct),
not `type`. Forgejo has **no** background auto-update token → **public** repos sidestep the gap →
answers §9 visibility.
- **F3 — `ms-ai-architect` is a 148 MB screenshot blob-bomb** (86% of `.git`; 26 blobs already
deleted from HEAD but alive in history). filter-repo resurrects them → blob-strip decision.
- **F4 — `config-audit` snapshot tests embed the absolute path** (verified directly): the
`default-output/` snapshots assert byte-equal output whose deep per-file paths (e.g.
`…/tests/fixtures/marketplace-medium/CLAUDE.md`) are **not** normalized
(`normalizeScanOrchestrator` scrubs only `meta.target`/`timestamp`/`duration_ms`), so they break at
a new clone path; the `v5.0.0/` back-compat snapshots (loaded by `json-backcompat.test.mjs` +
`raw-backcompat.test.mjs`) additionally embed a sibling marketplace (`ktg-privat`) + deleted
plugins from their original capture. SC2 blocker → real fix uses the existing `UPDATE_SNAPSHOT=1`
seam for the rebasable suite + by-name exclusion of the 2 frozen back-compat tests (Step 7).
- **F5 — Tag namespace is inconsistent** (3 schemes; bare `v5.0.0`/`v7.7.2` carry no plugin
identity; `v3.4.1`→deleted `ultraplan-local`). filter-repo copies ALL tags into EACH extract →
strip-all-then-re-tag.
- **F6 — README/metadata reference rot.** Every plugin `README.md:7` links `../../README.md`
(AI-disclosure) → 404 standalone; root-only `.mailmap`/`.gitleaks.*` don't travel with
`--path plugins/x`; `ai-psychosis/plugin.json` already points at a standalone URL while others
point at the monorepo (reconcile).
### The five §9 open questions — RESOLVED with evidence
1. **Exact filter-repo invocation + `shared/` history into DS.** For non-renamed plugins:
`git filter-repo --path plugins/<name>/ --path-rename plugins/<name>/:` (≡
`--subdirectory-filter plugins/<name>`). For **renamed** plugins: explicit multi-`--path` with a
`--path-rename` per old+new name (F1). DS: `git filter-repo --subdirectory-filter shared`
**yes**, preserve `shared/` history into the DS repo (clean: 10/12 `shared/`-touching commits are
pure-shared, 2 are single-plugin-entangled but conflict-free). All extractions run on a
`git clone --no-local` fresh clone (avoids filter-repo's spurious freshness abort on local
clones; do **not** paper over with `--force`).
2. **True DS consumers.** Exactly **2**`llm-security`, `ms-ai-architect` (only two with a
`playground/vendor/` dir). `claude-design` + `human-friendly-style` have **no** `playground/`
don't need DS. The "6" in `shared/PLAYGROUND-MAINTENANCE.md:5` is stale (one entry,
`ultraplan-local`, is the ghost pre-rename name for `voyage`). DS-repo standup scope = 2.
3. **Repo visibility.** **Public** (recommended) — sidesteps the Forgejo no-auto-update-token gap
(F2), matches the existing `open/` model, lets HTTPS clone work without credential setup.
Operator confirms at the window.
4. **`playground-examples/` home.** **DS repo.** It is DS-coupled (every HTML references
`playground-design-system`; `PLAYGROUND-MAINTENANCE.md` anchors `ros-lier-kommune.html` to
ms-ai-architect renderers). Travels with the DS repo, not the thin catalog.
5. **Per-repo CHANGELOG.** **Carry existing** — filter-repo preserves each plugin's `CHANGELOG.md`
and its commit history (D2); tags are re-created cleanly per repo (F5).
## Architecture Diagram
```mermaid
graph TD
subgraph BEFORE["Monorepo (today) — open/ktg-plugin-marketplace"]
MJ0[".claude-plugin/marketplace.json<br/>10x ./plugins/x"]
PLUGS["plugins/ x10"]
SH["shared/<br/>design-system + examples"]
SYNC["scripts/sync-design-system.mjs"]
SYNC -->|vendors| PLUGS
SH -->|source| SYNC
end
subgraph AFTER["Polyrepo (target) — same URL"]
CAT["open/ktg-plugin-marketplace<br/>THIN CATALOG<br/>marketplace.json (external+ref) · README · GOVERNANCE · CONVENTIONS · .mailmap · .gitleaks"]
DS["open/playground-design-system<br/>design-system + examples + sync-design-system.mjs(--source/--check)"]
R1["open/voyage (v5.1.1)"]
R2["open/llm-security (v7.7.2)"]
R3["open/linkedin-studio (v0.4.0)"]
Rn["open/<plugin> ... x10 total"]
CAT -.->|url + ref pin HTTPS| R1
CAT -.->|url + ref pin HTTPS| R2
CAT -.->|url + ref pin HTTPS| R3
CAT -.->|url + ref pin HTTPS| Rn
DS -->|vendor via re-homed sync| R2
DS -->|vendor via re-homed sync| Rn
end
BEFORE ==>|filter-repo extraction<br/>NULL push until window| AFTER
```
## Codebase Analysis
- **Tech stack:** Node.js `.mjs` scripts (zero npm deps house style, `node:test`), bash 3.2 hooks/
validators, one TypeScript analytics subtree (`linkedin-studio/scripts/analytics`, needs `npm
install` + `tsx`). Git on Forgejo (`git.fromaitochitta.com`, org `open/`). 1890 source files,
741 commits, `.git` 144 MB.
- **Key patterns:** Each plugin self-contained (`.claude-plugin/plugin.json` + `README` + `CLAUDE.md`,
optional `hooks/ tests/ scripts/ playground/`). Hooks resolve via `${CLAUDE_PLUGIN_ROOT}`
(relocation-invariant — confirmed). DS is **vendored** (`playground/vendor/`), never live-linked.
- **Relevant files (verified):**
- `.claude-plugin/marketplace.json` — 10 entries, all `source: "./plugins/<name>"`, entry shape
`{name, source, description}` (no `version`/`ref` today).
- `scripts/sync-design-system.mjs` — SOURCE at `:26-27` (`MARKETPLACE_ROOT/shared/…`), target at
`:146` (`plugins/<name>/playground/vendor/…`), SHA-256 `MANIFEST.json` + drift-guard at
`:118-127,151-156`. **No `--source` flag today** (`parseArgs` `:30-46`).
- `.gitignore``plugins/*/` patterns at `:9` (`plugins/*/.claude/`) and `:12`
(`plugins/*/reports/*-beskrivelse.*`); global patterns (`STATE.md`, `*.local.md`, …) stay.
- `.gitleaksignore` — 2 plugin-prefixed fingerprints (`:2` llm-security, `:5` linkedin-studio);
`.gitleaks.toml` allowlist `:13` already plugin-relative.
- `plugins/linkedin-studio/scripts/analytics/src/utils/storage.ts:18-33,48` — marker resolver
(standalone-safe primary) + `../../../../` fallback (latent depth assumption, dead branch).
- `plugins/voyage/tests/helpers/hook-helper.mjs:4` — verbatim copy (C7), stays a copy.
- **Reusable code:** the existing SHA-256 `MANIFEST.json` + `detectDrift` in
`sync-design-system.mjs` already implements SC5's hash mechanism — extend, don't rebuild. The
`validate-plugin.sh` structure-validator in `claude-design/tests/` + `ms-ai-architect/tests/` is
the pattern to port to the two test-less plugins (okr, human-friendly-style).
- **Recent git activity:** HEAD `f2d41c8` (ratified brief, **3 commits ahead of origin, unpushed**).
Only `main`, no feature branches. Cross-plugin bleed commits confirmed (`f460814` touches 10
plugins; `69610d4` 2) — ~1.8% of commits span 3+ plugins, mostly marketplace-wide doc sweeps.
## Research Sources
| Topic | Source | Key findings | Confidence |
|---|---|---|---|
| `git filter-repo` subdir extract | [git-filter-repo man page](https://manpages.debian.org/testing/git-filter-repo/git-filter-repo.1.en.html) | `--subdirectory-filter X``--path X/ --path-rename X/:`; removes `origin`; rewrites tags; aborts on non-fresh clone | High |
| Local-clone freshness | [filter-repo #38](https://github.com/newren/git-filter-repo/issues/38) | `git clone --no-local` from a local source (not `--force`) | High |
| marketplace.json external/mixed/ref | [plugin-marketplaces](https://code.claude.com/docs/en/plugin-marketplaces), [plugins-reference](https://code.claude.com/docs/en/plugins-reference) | discriminator is `source` not `type`; `{source:"url",url,ref}`; mixed in-repo+external in one file **supported**; `/plugin marketplace update` refreshes a bumped ref | High |
| SSH rejection | [claude-code #9740](https://github.com/anthropics/claude-code/issues/9740) (open) | `marketplace add` rejects SSH URLs → **use HTTPS** | High (use HTTPS) |
| Forgejo auto-update token | plugin-marketplaces token table | no `FORGEJO_TOKEN` for bg auto-update of private repos → prefer **public** | Medium |
| Forgejo repo create | [Forgejo api-usage](https://forgejo.org/docs/latest/user/api-usage/) | `POST /api/v1/orgs/{org}/repos`, header `Authorization: token …`, body `{name,private,auto_init:false,default_branch}` | High |
## Implementation Plan
> **Execution invariants (apply to every step):**
> - **D8 — NULL PUSH.** Every `git commit` here is **local only**; no `git push` until the authorized
> operator window. Checkpoints are local commits held on `main` ahead of origin.
> - **All extraction runs from a `git clone --no-local` fresh clone** in a throwaway workspace
> (`$WORK`, default `/tmp/polyrepo-migration`), never the working checkout (R4). The working repo
> is read-only to every extraction.
> - Migration tooling lives under `docs/marketplace-polyrepo-migration/migration/` so it sits beside
> the brief/plan and is removed/archived at thinning, never polluting the live catalog.
> - The single source of truth for per-plugin paths/versions/flags is `migration/plugin-map.json`
> (Step 1); every later script reads it.
### Step 1: Preflight, archive tag, and the plugin map
- **Files:** `docs/marketplace-polyrepo-migration/migration/00-preflight.sh`,
`docs/marketplace-polyrepo-migration/migration/plugin-map.json`,
`docs/marketplace-polyrepo-migration/migration/00-preflight.test.mjs`
- **Changes:** (new files) `00-preflight.sh` asserts tooling and repo hygiene: `git filter-repo
--version` returns a real version, `python3 --version` ≥ 3.6, HEAD is `f2d41c8` **or a descendant**
on `main`, `origin/main` is behind (unpushed state expected). **Hygiene check is relaxed and
idempotent (M12 fix):** it aborts only if `git status --porcelain plugins shared
.claude-plugin/marketplace.json scripts` is non-empty (uncommitted change to a migration-sensitive
surface) — committed `migration/` files and unrelated untracked docs are tolerated, so re-running
preflight mid-migration does not abort. It creates the **local** annotated tag
`pre-polyrepo-archive` at HEAD (D2; pushed later in the window) idempotently. **It establishes the
dedicated extraction source (R4 / M6 fix):** `git clone --no-local "$REPO_ROOT"
"$WORK/_mirror"` **once**, at the pinned HEAD — `$MIRROR` is the single source every per-target
extraction clones from, so concurrent commits to the operator's working checkout can never affect
any extraction (the working checkout is never read by filter-repo). It also **enumerates
blob-strip candidates (M4 fix):** lists every blob >1 MB in ms-ai-architect history with its path,
and emits `blob_strip_safe: true` into `plugin-map.json` iff **all** >1 MB blobs are under
`playground/screenshots/` (else `false` → the surgical `--path-glob` strategy is selected instead
of size-strip, decided deterministically, no runtime prompt). `plugin-map.json` encodes, per
extraction target: the rename-aware `--path` list, the `--path-rename` map, the release tag (from
`plugin.json` `version`), the new **HTTPS** repo URL, `has_vendor_ds` (true only for llm-security +
ms-ai-architect), `blob_strip` + `blob_strip_safe` (ms-ai-architect), `test_cmd`, and
`standalone_caveat`. Includes the DS target (`shared/` → `open/playground-design-system`) and an
explicit `drop: ["plugins/ultra-cc-architect/"]` note (removed plugin, already its own repo — its
history is dropped, not extracted).
- **Reuses:** version strings from each `plugins/<name>/.claude-plugin/plugin.json` (task-finder
table); churn order from git-historian (voyage→llm-security→linkedin-studio→ms-ai-architect).
- **Test first:**
- File: `docs/marketplace-polyrepo-migration/migration/00-preflight.test.mjs` (new)
- Verifies: `plugin-map.json` parses; has exactly 11 targets (10 plugins + DS); the 3 renamed
plugins carry ≥2 `--path` entries each; every plugin's `repo_url` is `https://` (not `ssh://`);
ms-ai-architect has `blob_strip: true`; only llm-security + ms-ai-architect have
`has_vendor_ds: true`.
- Pattern: `plugins/voyage/tests/validators/*.test.mjs` (node:test style)
- **Verify:** `bash docs/marketplace-polyrepo-migration/migration/00-preflight.sh --dry-run` →
expected: prints `PREFLIGHT OK` and the 11-target table, creates no tag in `--dry-run`; then
`node --test docs/marketplace-polyrepo-migration/migration/00-preflight.test.mjs` → expected:
`pass`.
- **On failure:** escalate — if tooling is missing (`git filter-repo` absent), stop and instruct
`brew install git-filter-repo`; do not proceed.
- **Checkpoint:** `git commit -m "chore(migration): preflight script + rename-aware plugin map"`
- **Manifest:**
```yaml
manifest:
expected_paths:
- docs/marketplace-polyrepo-migration/migration/00-preflight.sh
- docs/marketplace-polyrepo-migration/migration/plugin-map.json
- docs/marketplace-polyrepo-migration/migration/00-preflight.test.mjs
min_file_count: 3
commit_message_pattern: "^chore\\(migration\\): preflight script \\+ rename-aware plugin map$"
bash_syntax_check:
- docs/marketplace-polyrepo-migration/migration/00-preflight.sh
forbidden_paths:
- .claude-plugin/marketplace.json
- scripts/sync-design-system.mjs
must_contain:
- path: docs/marketplace-polyrepo-migration/migration/plugin-map.json
pattern: "ultraplan-local"
- path: docs/marketplace-polyrepo-migration/migration/plugin-map.json
pattern: "https://git.fromaitochitta.com/open/"
```
### Step 2: Re-home and extend sync-design-system.mjs (--source + --check)
- **Files:** `scripts/sync-design-system.mjs`, `scripts/sync-design-system.test.mjs`
- **Changes:** Add a `--source <dir>` flag so the script vendors from an arbitrary DS checkout
(the re-homed DS repo) instead of the hardcoded `MARKETPLACE_ROOT/shared/playground-design-system`
(`:26-27`); keep the in-repo path as the default fallback so the script still works pre-migration.
Add a `--check` mode that re-hashes a target plugin's vendored tree against its committed
`MANIFEST.json` and exits non-zero on drift, with **no** source required — making SC5 a single
runnable command in a clean clone (D3). Resolve the plugin/target dir from a `--target <dir>`
arg when `MARKETPLACE_ROOT/plugins/<name>` no longer exists. This is migration **rewiring**, not a
plugin-functionality change (non-goal §5 untouched). The modified script + test travel into the DS
repo at standup (Step on the DS target) and are removed from the catalog at thinning (Step 9).
- **Reuses:** existing `detectDrift` + SHA-256 `MANIFEST.json` writer (`:118-127`) — `--check`
reuses the same hashing.
- **Test first:**
- File: `scripts/sync-design-system.test.mjs` (new)
- Verifies: `--source <tmpDS>` vendors byte-identically to the default in-repo source (SC5 parity);
`--check` passes on an unmodified vendored tree and fails (exit 2) after a tampered byte;
omitting `--source` still resolves the in-repo default.
- Pattern: `plugins/voyage/tests/scripts/*.test.mjs`
- **Verify:** `node --test scripts/sync-design-system.test.mjs` → expected: `pass` (all assertions);
then `node scripts/sync-design-system.mjs --check ms-ai-architect` → expected: `MANIFEST OK`
against the current vendored tree.
- **On failure:** revert — `git checkout -- scripts/sync-design-system.mjs
scripts/sync-design-system.test.mjs`; the legacy in-repo sync still works, so this is non-blocking
for the rest until retried.
- **Checkpoint:** `git commit -m "feat(migration): sync-design-system --source + --check for DS re-home"`
- **Manifest:**
```yaml
manifest:
expected_paths:
- scripts/sync-design-system.mjs
- scripts/sync-design-system.test.mjs
min_file_count: 2
commit_message_pattern: "^feat\\(migration\\): sync-design-system --source \\+ --check for DS re-home$"
bash_syntax_check: []
forbidden_paths:
- .claude-plugin/marketplace.json
must_contain:
- path: scripts/sync-design-system.mjs
pattern: "--source"
- path: scripts/sync-design-system.mjs
pattern: "--check"
```
### Step 3: Rename-aware extraction driver
- **Files:** `docs/marketplace-polyrepo-migration/migration/10-extract.sh`,
`docs/marketplace-polyrepo-migration/migration/10-extract.test.mjs`
- **Changes:** (new files) Given a target key from `plugin-map.json`, it: `git clone --no-local
"$WORK/_mirror" "$WORK/<key>"` (clones from the **dedicated mirror** built in Step 1, never the
working checkout — R4/M6); runs **a single `git filter-repo` invocation** that composes all history
rewrites in one pass (M5 fix — avoids the second-pass `--force` abort): the rename-aware
`--path`/`--path-rename` set from the map (single path for non-renamed; **both** names for
voyage/llm-security/linkedin-studio — F1) **plus**, for ms-ai-architect, the blob strip selected
deterministically from `blob_strip_safe` (Step 1): `--strip-blobs-bigger-than 1M` when all >1 MB
blobs are screenshots, else `--path-glob '!plugins/ms-ai-architect/playground/screenshots/*'` —
no runtime prompt (M4/F3). filter-repo composes `--path`, `--path-rename`, and
`--strip-blobs-bigger-than` in one invocation (verified against the man page). **After** the filter
completes (so the tag points at the final rewritten HEAD — M5 ordering fix), it strips every tag
filter-repo carried over and re-creates **exactly one** annotated tag `v<version>` at HEAD (F5).
Idempotent: re-running wipes `$WORK/<key>` first. **No push.**
- **Reuses:** `plugin-map.json` (Step 1); `git clone --no-local` per research-scout.
- **Test first:**
- File: `docs/marketplace-polyrepo-migration/migration/10-extract.test.mjs` (new)
- Verifies (runs the driver against the live monorepo into a temp `$WORK`): the `voyage` extract's
`git log --all` contains commits whose original paths were under **both** `plugins/voyage/` and
`plugins/ultraplan-local/` (≥150 commits, proving F1 handled); the extract `git tag` lists
exactly `v5.1.1` and nothing else; the extract has **no** `plugins/` prefix at root (contents at
repo root); `origin` remote is absent post-filter.
- Pattern: integration test style in `plugins/voyage/tests/integration/*.test.mjs`
- **Verify (self-contained — M6):** `bash docs/marketplace-polyrepo-migration/migration/00-preflight.sh
&& bash docs/marketplace-polyrepo-migration/migration/10-extract.sh voyage` (preflight builds
`$WORK/_mirror` if absent; extract is self-healing) → expected:
`EXTRACT OK voyage → $WORK/voyage (257 commits, tag v5.1.1)`; then
`git -C "$WORK/voyage" log --oneline | wc -l` → expected: `≥ 250`; `git -C "$WORK/voyage" tag` →
expected: exactly `v5.1.1`.
- **On failure:** retry — if filter-repo aborts on freshness, confirm the source is `$WORK/_mirror`
(a fresh `--no-local` clone); if a rename path is missing, the test catches the dropped-history
count. Revert leaves no working-tree change (extract is in `$WORK`).
- **Checkpoint:** `git commit -m "chore(migration): rename-aware filter-repo extraction driver"`
- **Manifest:**
```yaml
manifest:
expected_paths:
- docs/marketplace-polyrepo-migration/migration/10-extract.sh
- docs/marketplace-polyrepo-migration/migration/10-extract.test.mjs
min_file_count: 2
commit_message_pattern: "^chore\\(migration\\): rename-aware filter-repo extraction driver$"
bash_syntax_check:
- docs/marketplace-polyrepo-migration/migration/10-extract.sh
forbidden_paths:
- .claude-plugin/marketplace.json
- plugins
must_contain:
- path: docs/marketplace-polyrepo-migration/migration/10-extract.sh
pattern: "no-local"
- path: docs/marketplace-polyrepo-migration/migration/10-extract.sh
pattern: "ultraplan-local"
```
### Step 4: Per-repo config re-rooting (.gitignore, .gitleaks, .mailmap)
- **Files:** `docs/marketplace-polyrepo-migration/migration/20-rehome-config.sh`,
`docs/marketplace-polyrepo-migration/migration/templates/gitignore.plugin.tmpl`,
`docs/marketplace-polyrepo-migration/migration/20-rehome-config.test.mjs`
- **Changes:** (new files) Operates on an extracted repo in `$WORK/<key>`: writes a re-rooted
`.gitignore` from the template (the `plugins/*/` prefixes stripped → `.claude/`,
`reports/*-beskrivelse.*`; the global `STATE.md`/`*.local.md`/OS patterns kept — F6/C3); generates
a per-plugin `.gitleaks.toml` from the root baseline and a `.gitleaksignore` with the matching
fingerprint **re-rooted by dropping ONLY the `plugins/<name>/` path prefix and keeping the full
`:rule-id:line` suffix** (M7 fix — the gitleaks fingerprint format is `path:rule-id:line`, not
`path:line`; dropping the rule-id yields a fingerprint that no longer suppresses) — only for
llm-security (`examples/malicious-skill-demo/evil-project-health/lib/telemetry.mjs:generic-api-key:18`)
and linkedin-studio (`hooks/prompts/content-quality-gate.md:linkedin-client-id:14`); copies the
root `.mailmap` into every extract (F6 — it does not travel with `--path`). For the DS target it
also copies the modified `sync-design-system.mjs` + test (Step 2) and
`shared/PLAYGROUND-MAINTENANCE.md` + `playground-examples/` into the DS repo root (§9 Q4).
- **Reuses:** root `.gitignore` (`:9,:12`), `.gitleaks.toml` (`:13`), `.gitleaksignore` (`:2,:5`),
`.mailmap`.
- **Test first:**
- File: `docs/marketplace-polyrepo-migration/migration/20-rehome-config.test.mjs` (new)
- Verifies (against a freshly extracted llm-security in `$WORK`): the new `.gitignore` contains
`.claude/` but **no** `plugins/*/` prefix; the re-rooted `.gitleaksignore` fingerprint is exactly
`examples/malicious-skill-demo/evil-project-health/lib/telemetry.mjs:generic-api-key:18` (no
`plugins/llm-security/` prefix, **rule-id `generic-api-key` retained**); `.mailmap` exists; a
plugin with no fingerprint (graceful-handoff) gets a `.gitleaks.toml` but an empty/absent
`.gitleaksignore`.
- Pattern: `plugins/config-audit/tests/lib/*.test.mjs`
- **Verify:** `bash …/20-rehome-config.sh llm-security` then `git -C /tmp/polyrepo-migration/llm-security
check-ignore .claude/x` → expected: `.claude/x` (ignored); `grep -c 'plugins/' /tmp/polyrepo-migration/llm-security/.gitleaksignore`
→ expected: `0`.
- **On failure:** retry — config generation is idempotent; revert leaves working tree clean.
- **Checkpoint:** `git commit -m "chore(migration): per-repo gitignore/gitleaks/mailmap re-rooting"`
- **Manifest:**
```yaml
manifest:
expected_paths:
- docs/marketplace-polyrepo-migration/migration/20-rehome-config.sh
- docs/marketplace-polyrepo-migration/migration/templates/gitignore.plugin.tmpl
- docs/marketplace-polyrepo-migration/migration/20-rehome-config.test.mjs
min_file_count: 3
commit_message_pattern: "^chore\\(migration\\): per-repo gitignore/gitleaks/mailmap re-rooting$"
bash_syntax_check:
- docs/marketplace-polyrepo-migration/migration/20-rehome-config.sh
forbidden_paths:
- .gitignore
- .gitleaks.toml
- .gitleaksignore
must_contain:
- path: docs/marketplace-polyrepo-migration/migration/20-rehome-config.sh
pattern: "mailmap"
```
### Step 5: Reference-rot rewriter (README links + plugin.json/package.json URLs)
- **Files:** `docs/marketplace-polyrepo-migration/migration/30-fix-references.mjs`,
`docs/marketplace-polyrepo-migration/migration/30-fix-references.test.mjs`
- **Changes:** (new files) Operates on an extracted repo in `$WORK/<key>`: replaces the
`[Full disclosure →](../../README.md#ai-generated-code-disclosure)` footnote at every plugin
`README.md:7` with the **inlined** disclosure text (M13 — inline chosen over a cross-repo link:
deterministic, self-contained, no dangling anchor); fixes `graceful-handoff/README.md:355` and any
other `../../README.md` / `../../.claude-plugin/marketplace.json` reference (e.g. linkedin-studio
remediation docs); sets `plugin.json` `repository` and `package.json` `homepage`/`repository` to
the new standalone `open/<plugin>` HTTPS URL. **Scope note (M14):** `ai-psychosis/plugin.json`
**already** points at `open/ai-psychosis` (verified) — for it this is a verify-only no-op; the real
reconcile work is the **9 other plugins** still pointing at the monorepo URL/subpath (e.g.
linkedin-studio, llm-security `package.json` homepage). Reads URLs from `plugin-map.json`. Reports
every file it rewrote.
- **Reuses:** `plugin-map.json` repo URLs.
- **Test first:**
- File: `docs/marketplace-polyrepo-migration/migration/30-fix-references.test.mjs` (new)
- Verifies (against an extracted graceful-handoff): no `../../README.md` substring remains in any
`*.md`; `plugin.json.repository` ends with `/open/graceful-handoff`; an idempotent second run
makes zero changes.
- Pattern: `plugins/linkedin-studio/render/__tests__/*.test.mjs`
- **Verify:** `node …/30-fix-references.mjs graceful-handoff` then `grep -rn "\.\./\.\./README.md"
/tmp/polyrepo-migration/graceful-handoff` → expected: no matches (exit 1 from grep).
- **On failure:** retry — rewriter is idempotent and reports diffs; revert leaves working tree clean.
- **Checkpoint:** `git commit -m "chore(migration): standalone reference-rot rewriter"`
- **Manifest:**
```yaml
manifest:
expected_paths:
- docs/marketplace-polyrepo-migration/migration/30-fix-references.mjs
- docs/marketplace-polyrepo-migration/migration/30-fix-references.test.mjs
min_file_count: 2
commit_message_pattern: "^chore\\(migration\\): standalone reference-rot rewriter$"
bash_syntax_check: []
forbidden_paths:
- plugins
must_contain:
- path: docs/marketplace-polyrepo-migration/migration/30-fix-references.mjs
pattern: "ai-generated-code-disclosure"
```
### Step 6: Standalone validation harness (SC2 + SC7)
- **Files:** `docs/marketplace-polyrepo-migration/migration/40-validate-standalone.sh`,
`docs/marketplace-polyrepo-migration/migration/40-validate-standalone.test.mjs`
- **Changes:** (new files) Per `plugin-map.json` `test_cmd`: clones the extracted repo to a clean
`/tmp/claude-*` dir (no marketplace parent) and runs its self-validate command in **the runner the
plugin itself declares** — `node --test 'tests/**/*.test.mjs'` (quoted globstar, Node-≥21 native;
config-audit/llm-security/voyage) or `node --test <dir>/*.test.mjs` glob for plugins with a flat
layout — **never `node --test <dir>`** (Node 25 gotcha). Per-plugin caveats baked in:
`ai-psychosis` runs from repo root (relative-fixture cwd); **`linkedin-studio` SC2 is explicitly
two-tier (M11):** the `.mjs` core (`node --test hooks/scripts/__tests__/*.test.mjs
render/__tests__/*.test.mjs agents/__tests__/*.test.mjs`) is the **hard gate** (must pass with zero
deps); the TS analytics suite (`cd scripts/analytics && npm install && npm test`) is reported as a
**separate ADVISORY line — network-required, NOT a clean-room hard gate** (not silently waived,
explicitly labelled non-blocking for SC2). `claude-design` + `ms-ai-architect` run `bash
tests/validate-plugin.sh`; `okr` + `human-friendly-style` run the ported
`validate-plugin.generic.sh` structure check (Step 7 provides it) since they have no unit suite
(H2). **`voyage/.forgejo/` survival (M17):** for voyage, assert the extract still contains
`.forgejo/ISSUE_TEMPLATE/` (it travels under `--path plugins/voyage/`) and that no file in it
references a monorepo-relative path (it holds only issue templates, no workflows — confirmed).
SC7 assertions for every repo: `git ls-files | grep -E 'STATE\.md|\.local\.md$'` is empty
**except** the intentional tracked `okr/templates/okr.local.md.template`; no `../../README.md`
remains; `.gitignore` ignores `.claude/`. Emits a per-repo PASS/FAIL table.
- **Reuses:** the per-plugin command table (test-strategist); `validate-plugin.sh` pattern.
- **Test first:**
- File: (the harness is itself the test surface) — assert via a smoke run that the harness exits
non-zero when fed a deliberately broken extract (a planted tracked `STATE.md`) and zero on a
clean one. Encoded as `docs/marketplace-polyrepo-migration/migration/40-validate-standalone.test.mjs`.
- Verifies: harness FAILs a repo with a tracked `*.local.md`; PASSes a clean repo; uses glob test
form (asserts the command string contains `*.test.mjs`, not a bare dir).
- Pattern: `plugins/voyage/tests/synthetic/*.test.mjs`
- **Verify:** `bash …/40-validate-standalone.sh graceful-handoff` → expected: `graceful-handoff:
PASS (6 tests, standalone-safe)`; `node --test …/40-validate-standalone.test.mjs` → expected:
`pass`.
- **On failure:** escalate per repo — a FAIL is a genuine SC2/SC7 blocker for that plugin (e.g.
config-audit until Step 7); record it, continue other repos, do not mask.
- **Checkpoint:** `git commit -m "chore(migration): standalone SC2/SC7 validation harness"`
- **Manifest:**
```yaml
manifest:
expected_paths:
- docs/marketplace-polyrepo-migration/migration/40-validate-standalone.sh
- docs/marketplace-polyrepo-migration/migration/40-validate-standalone.test.mjs
min_file_count: 2
commit_message_pattern: "^chore\\(migration\\): standalone SC2/SC7 validation harness$"
bash_syntax_check:
- docs/marketplace-polyrepo-migration/migration/40-validate-standalone.sh
forbidden_paths:
- plugins
must_contain:
- path: docs/marketplace-polyrepo-migration/migration/40-validate-standalone.sh
pattern: "\\*.test.mjs"
```
### Step 7: Resolve the config-audit SC2 blocker + port a structure-validator to okr/human-friendly-style
- **Files:** `docs/marketplace-polyrepo-migration/migration/50-config-audit-sc2.sh`,
`docs/marketplace-polyrepo-migration/migration/templates/validate-plugin.generic.sh`,
`docs/marketplace-polyrepo-migration/migration/50-config-audit-sc2.test.mjs`
- **Changes:** (new files) **config-audit (F4 — corrected against verified test code):** config-audit
declares its runner as `node --test 'tests/**/*.test.mjs'` (52 files, CLAUDE.md:109). Two distinct
portability defects exist (both verified directly):
(a) The **rebasable** suite `snapshot-default-output.test.mjs` asserts byte-equal CLI output whose
deep per-file paths are **not** normalized (`normalizeScanOrchestrator` scrubs only
`meta.target`/`timestamp`/`duration_ms`, `normalizeTokenHotspots` only `duration_ms`), so it breaks
at a new clone path. **Fix:** the script runs `UPDATE_SNAPSHOT=1 node --test
tests/snapshot-default-output.test.mjs` **inside the clean clone** to re-seed those snapshots at the
clone's own path — it then passes (the seam exists, line 33; this is the intended re-approval path).
(b) The **frozen back-compat** tests `json-backcompat.test.mjs` + `raw-backcompat.test.mjs` load the
`tests/snapshots/v5.0.0/` fixtures, which embed the **original** capture machine's absolute path + a
sibling marketplace (`ktg-privat`) + deleted plugins — regenerating them would defeat their
byte-stability purpose. **Fix:** these two files are **excluded by name** from config-audit's SC2
gate, which runs over the file list `tests/**/*.test.mjs` **minus** those two (deterministic file
enumeration, no invented env var). The exclusion + rationale is recorded in the config-audit repo's
test README. **This edges the §5 non-goal "NOT changing plugin code"** (it re-seeds fixtures + names
an exclusion; no functionality changes) — surfaced as a **brief-correction requiring operator
ratification** (header) + Assumption 4. **okr + human-friendly-style (H2):**
`validate-plugin.generic.sh` is a ported, parameterized copy of the existing
`claude-design/tests/validate-plugin.sh` (valid `plugin.json`, expected command/skill/agent files
present, frontmatter parses) so SC2 has a runnable gate for the two test-less plugins.
- **Reuses:** `plugins/claude-design/tests/validate-plugin.sh` (the pattern); config-audit's own
`UPDATE_SNAPSHOT=1` seam (line 33) + declared runner (CLAUDE.md:109).
- **Test first:**
- File: `docs/marketplace-polyrepo-migration/migration/50-config-audit-sc2.test.mjs` (new)
- Verifies: after the script runs against an extracted config-audit, the SC2 gate (full
`tests/**/*.test.mjs` **minus** json-backcompat + raw-backcompat) passes; the gate file list does
NOT include the two excluded files; the generic validator PASSes okr and FAILs a plugin with a
deleted `plugin.json`.
- Pattern: `plugins/config-audit/tests/snapshot-default-output.test.mjs` (the rebasable one)
- **Verify:** `bash …/50-config-audit-sc2.sh` then in the extracted clone the emitted SC2 gate
command (e.g. ``node --test $(ls tests/**/*.test.mjs | grep -vE 'json-backcompat|raw-backcompat')``)
→ expected: `pass`; `bash validate-plugin.generic.sh okr` → expected: `okr: STRUCTURE OK`.
- **On failure:** escalate — surface to the operator the choice: accept config-audit SC2 as
"full suite minus 2 frozen back-compat tests" (recommended), or invest in normalizing the v5.0.0
snapshots' embedded paths (config-audit's own backlog, out of migration scope).
- **Checkpoint:** `git commit -m "fix(migration): config-audit SC2 gate (re-seed + back-compat exclusion) + generic validator"`
- **Manifest:**
```yaml
manifest:
expected_paths:
- docs/marketplace-polyrepo-migration/migration/50-config-audit-sc2.sh
- docs/marketplace-polyrepo-migration/migration/templates/validate-plugin.generic.sh
- docs/marketplace-polyrepo-migration/migration/50-config-audit-sc2.test.mjs
min_file_count: 3
commit_message_pattern: "^fix\\(migration\\): config-audit SC2 gate \\(re-seed \\+ back-compat exclusion\\) \\+ generic validator$"
bash_syntax_check:
- docs/marketplace-polyrepo-migration/migration/50-config-audit-sc2.sh
- docs/marketplace-polyrepo-migration/migration/templates/validate-plugin.generic.sh
forbidden_paths:
- plugins/config-audit
must_contain:
- path: docs/marketplace-polyrepo-migration/migration/50-config-audit-sc2.sh
pattern: "UPDATE_SNAPSHOT"
- path: docs/marketplace-polyrepo-migration/migration/50-config-audit-sc2.sh
pattern: "json-backcompat"
```
### Step 8: marketplace.json rewriter (mixed-source aware, HTTPS + ref pin)
- **Files:** `docs/marketplace-polyrepo-migration/migration/60-rewrite-marketplace.mjs`,
`docs/marketplace-polyrepo-migration/migration/60-rewrite-marketplace.test.mjs`
- **Changes:** (new files — **authored, not run against the live file until the window**) Rewrites a
`marketplace.json` so a named subset of entries become external
`{"source":"url","url":"https://git.fromaitochitta.com/open/<name>.git","ref":"v<version>"}`
(F2 — HTTPS + discriminator `source`, ref = the plugin's release tag, D4) while the rest stay
`./plugins/<name>` — enabling the mixed-source intermediate states that keep the marketplace live
(SC3/SC8). `--only <names>` flips a subset; `--all` flips everything (final state). Reads versions
from `plugin-map.json`; validates output against the marketplace schema (each entry has
`name`+`source`+`description`; external entries have a valid `url`+`ref`). Writes to a `--out` path
by default (never the live file) so Step 11's dry-run exercises it without violating D8.
- **Reuses:** `plugin-map.json`; the current `.claude-plugin/marketplace.json` as the input shape.
- **Test first:**
- File: `docs/marketplace-polyrepo-migration/migration/60-rewrite-marketplace.test.mjs` (new)
- Verifies: `--only voyage` produces a file where `voyage` is an external `url`+`ref:v5.1.1`
entry and the other 9 stay `./plugins/…` (mixed-source proven); every external `url` starts
`https://` (no `ssh://`); `--all` leaves zero `./plugins/` sources; schema-validates.
- Pattern: `plugins/voyage/tests/parsers/*.test.mjs`
- **Verify:** `node …/60-rewrite-marketplace.mjs --only voyage --out /tmp/mp-voyage.json` then
`grep -c '"./plugins/' /tmp/mp-voyage.json` → expected: `9`; `grep -c 'ssh://' /tmp/mp-voyage.json`
→ expected: `0`.
- **On failure:** retry — pure transform, idempotent; revert leaves working tree clean.
- **Checkpoint:** `git commit -m "chore(migration): mixed-source marketplace.json rewriter (HTTPS+ref)"`
- **Manifest:**
```yaml
manifest:
expected_paths:
- docs/marketplace-polyrepo-migration/migration/60-rewrite-marketplace.mjs
- docs/marketplace-polyrepo-migration/migration/60-rewrite-marketplace.test.mjs
min_file_count: 2
commit_message_pattern: "^chore\\(migration\\): mixed-source marketplace.json rewriter \\(HTTPS\\+ref\\)$"
bash_syntax_check: []
forbidden_paths:
- .claude-plugin/marketplace.json
must_contain:
- path: docs/marketplace-polyrepo-migration/migration/60-rewrite-marketplace.mjs
pattern: "https://git.fromaitochitta.com/open/"
```
### Step 9: Catalog-thinning script (staged for the window)
- **Files:** `docs/marketplace-polyrepo-migration/migration/70-thin-catalog.sh`,
`docs/marketplace-polyrepo-migration/migration/70-thin-catalog.test.mjs`
- **Changes:** (new files — **staged, not run until the window**) Produces the thin-catalog state in a
workspace: removes `plugins/` + `shared/` + `scripts/sync-design-system.mjs` (+ its test); extracts
the marketplace-wide conventions from the root `CLAUDE.md` into a new `CONVENTIONS.md` (D6); thins
the catalog `CLAUDE.md` to catalog-maintenance only; rewrites the landing `README.md` to reference
the external plugin repos. **The landing-README rewrite is the place the stale version/count drift
is corrected (M15 — coupled explicitly, not a side ride-along):** D6 mandates the README rewrite,
and a rewrite that re-states the plugin roster MUST state the verified counts/versions (10 plugins,
not ~8; linkedin-studio 0.4.0 not "4.1.0"; config-audit 5.1.0; voyage 5.1.1; …) or it ships a
known-false document. The correction is therefore part of the D6 README deliverable, coupled to it,
not a separable doc-fix. Verifies the result:
`ls` shows **no** `plugins/`, **no** `shared/` (SC4); the catalog retains `marketplace.json`,
`README.md`, `GOVERNANCE.md`, `CONVENTIONS.md`, `.mailmap`, a `.gitleaks` baseline.
- **Reuses:** root `CLAUDE.md` conventions block; `GOVERNANCE.md` as the conventions sibling.
- **Test first:**
- File: `docs/marketplace-polyrepo-migration/migration/70-thin-catalog.test.mjs` (new)
- Verifies (against a workspace copy): post-thin tree has no `plugins/`, no `shared/`, no
`scripts/sync-design-system.mjs`; `CONVENTIONS.md` exists and contains the Forgejo/conventional-
commits/three-doc rules; `marketplace.json` + `README.md` + `GOVERNANCE.md` remain.
- Pattern: `plugins/config-audit/tests/*.test.mjs`
- **Verify:** `bash …/70-thin-catalog.sh --workspace /tmp/thin-catalog` then `ls /tmp/thin-catalog`
→ expected: no `plugins`/`shared` entries; `test -f /tmp/thin-catalog/CONVENTIONS.md` → expected:
exit 0.
- **On failure:** revert — operates only in `--workspace`; the live tree is untouched (forbidden
path enforced).
- **Checkpoint:** `git commit -m "chore(migration): catalog-thinning script (staged for window)"`
- **Manifest:**
```yaml
manifest:
expected_paths:
- docs/marketplace-polyrepo-migration/migration/70-thin-catalog.sh
- docs/marketplace-polyrepo-migration/migration/70-thin-catalog.test.mjs
min_file_count: 2
commit_message_pattern: "^chore\\(migration\\): catalog-thinning script \\(staged for window\\)$"
bash_syntax_check:
- docs/marketplace-polyrepo-migration/migration/70-thin-catalog.sh
forbidden_paths:
- plugins
- shared
- CLAUDE.md
- README.md
must_contain:
- path: docs/marketplace-polyrepo-migration/migration/70-thin-catalog.sh
pattern: "CONVENTIONS.md"
```
### Step 10: Operator-window runbook (pilot gate + batch order)
- **Files:** `docs/marketplace-polyrepo-migration/migration/RUNBOOK.md`
- **Changes:** (new file) The single document the operator follows in the authorized window. It
encodes: **(0) Pilot gate (B3 ordering fix — the flip MUST precede the install-smoke, else the
test is vacuous).** Recommend `graceful-handoff` (low-churn, has tests, no DS, no blob bloat).
Steps, in order: **(0a)** create it on Forgejo (`POST /api/v1/orgs/open/repos` with
`{"name":"graceful-handoff","private":false,"auto_init":false,"default_branch":"main"}`, header
`Authorization: token $FORGEJO_TOKEN`); **(0b)** `git remote add origin https://… && git push
origin --all && git push origin --tags`; **(0c) flip graceful-handoff's entry in the catalog
`marketplace.json` to the external `url`+`ref` form** (`60-rewrite-marketplace.mjs --only
graceful-handoff`) and push the catalog in its still-mixed state (9× `./plugins/x` + 1× external;
`plugins/` still present, thinning comes last); **(0d)** in a fresh Claude Code session `/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` — this is the actual test of the HTTPS + external-source
+ ref-pin + Forgejo chain** (F2 / Assumption 1, the highest-stakes unverified link). If 0d fails,
STOP — revert the flip (graceful-handoff back to `./plugins/graceful-handoff`, marketplace stays
live) and diagnose before touching the other 10. **(1) DS repo first**, **(2) high-churn** (voyage →
llm-security → linkedin-studio → ms-ai-architect — corrected order), **(3) low-churn** (config-audit,
okr, ai-psychosis, human-friendly-style, claude-design), each: create repo → push → flip its
`marketplace.json` entry to external (`--only`) → push catalog → **install-smoke** (SC8) before the
next. **(4) Thin the catalog last** (Step 9), push catalog, push the `pre-polyrepo-archive` tag.
Includes the visibility decision (public — §9), the `auto_init:false` requirement, the ref-update
flow (`/plugin marketplace update`), the per-plugin install-smoke checklist ordered by surface
(linkedin-studio 29 cmds first), and the rollback note (a flip is reversible until the `./plugins/x`
source is removed; never remove it before the external repo is pushed+verified — R1/SC8).
- **Reuses:** all prior scripts (the runbook invokes them); research-scout's verified Forgejo/git
commands.
- **Test first:** *(documentation deliverable — no unit test; verified by review + the dry-run in
Step 11 exercising every script the runbook references)*
- **Verify:** `test -f docs/marketplace-polyrepo-migration/migration/RUNBOOK.md && grep -q
"auto_init" docs/marketplace-polyrepo-migration/migration/RUNBOOK.md && grep -q "Pilot"
docs/marketplace-polyrepo-migration/migration/RUNBOOK.md` → expected: exit 0.
- **On failure:** revert — `git checkout -- docs/marketplace-polyrepo-migration/migration/RUNBOOK.md`.
- **Checkpoint:** `git commit -m "docs(migration): operator-window runbook (pilot gate + batch order)"`
- **Manifest:**
```yaml
manifest:
expected_paths:
- docs/marketplace-polyrepo-migration/migration/RUNBOOK.md
min_file_count: 1
commit_message_pattern: "^docs\\(migration\\): operator-window runbook \\(pilot gate \\+ batch order\\)$"
bash_syntax_check: []
forbidden_paths:
- .claude-plugin/marketplace.json
must_contain:
- path: docs/marketplace-polyrepo-migration/migration/RUNBOOK.md
pattern: "auto_init"
- path: docs/marketplace-polyrepo-migration/migration/RUNBOOK.md
pattern: "Pilot"
```
### Step 11: Full local dry-run (all 11 targets, NULL push)
- **Files:** `docs/marketplace-polyrepo-migration/migration/99-dryrun.sh`,
`docs/marketplace-polyrepo-migration/migration/99-dryrun.test.mjs`
- **Changes:** (new files) `99-dryrun.sh` runs Steps 3→4→5→6 (+ Step 7 for config-audit/okr/
human-friendly-style) across **all 11 targets** (10 plugins + DS) into `$WORK`, then exercises
Step 8's rewriter (`--all` → `--out`) and Step 9's thinning (`--workspace`), and writes a generated
`dryrun-report.md` (an output artifact, not committed): per-target extract commit count (F1 history
retention), tag list (F5 clean re-tag), standalone PASS/FAIL (SC2), SC7 state-file check, and the
marketplace.json/catalog-thin previews — **with zero pushes and zero live-file mutation**. **It also
runs the cross-repo DS-vendor validation (M9 — the gap the pilot can't cover):** with both the DS
extract and the ms-ai-architect extract present in `$WORK`, it runs the re-homed sync
`node scripts/sync-design-system.mjs ms-ai-architect --source "$WORK/playground-design-system" --target "$WORK/ms-ai-architect"`
then `--check` against the committed `MANIFEST.json` and asserts byte-parity — exercising the actual
two-extracted-repo `--source`/`--target` invocation (SC5) locally, before the window.
**Honest scope of what the dry-run proves (M8):** it proves the **local mechanics** — extraction,
rename-aware history retention, re-rooting, reference rewrites, standalone validation, marketplace/
catalog transforms, and the DS-vendor chain. It **cannot** prove the **window-only** steps:
(i) Forgejo accepting the push of post-strip history, (ii) `auto_init:false` repo creation via the
API, (iii) Claude Code resolving an HTTPS `url`+`ref` source from self-hosted Forgejo
(Assumption 1 — covered instead by the Step 10 **pilot**, not the dry-run). The dry-run + the pilot
together gate the batch; neither alone is sufficient.
- **Reuses:** every prior script.
- **Test first:**
- File: `docs/marketplace-polyrepo-migration/migration/99-dryrun.test.mjs` (new)
- Verifies: `dryrun-report.md` lists all 11 targets; voyage shows ≥250 commits; config-audit shows
PASS under its SC2 gate (full suite minus `json-backcompat`/`raw-backcompat`, after re-seed); no
target shows a tracked state-file leak; the previewed all-external marketplace.json has zero
`./plugins/` and zero `ssh://`.
- Pattern: `plugins/voyage/tests/integration/*.test.mjs`
- **Verify:** `bash docs/marketplace-polyrepo-migration/migration/99-dryrun.sh` → expected:
`DRY-RUN OK — 11/11 targets extracted + validated, 0 pushes`; `git -C $REPO_ROOT status` →
expected: working tree shows only the committed `migration/` additions, **no** change to
`plugins/`, `shared/`, or `.claude-plugin/marketplace.json` (D8 held).
- **On failure:** escalate — any target FAIL is a real blocker to surface before the window; the
dry-run is reversible (everything in `$WORK`/`--out`/`--workspace`).
- **Checkpoint:** `git commit -m "chore(migration): full local dry-run harness + report"`
- **Manifest:**
```yaml
manifest:
expected_paths:
- docs/marketplace-polyrepo-migration/migration/99-dryrun.sh
- docs/marketplace-polyrepo-migration/migration/99-dryrun.test.mjs
min_file_count: 2
commit_message_pattern: "^chore\\(migration\\): full local dry-run harness \\+ report$"
bash_syntax_check:
- docs/marketplace-polyrepo-migration/migration/99-dryrun.sh
forbidden_paths:
- .claude-plugin/marketplace.json
- plugins
- shared
must_contain:
- path: docs/marketplace-polyrepo-migration/migration/99-dryrun.sh
pattern: "no-local"
```
### Manifest — objective completion predicate
Every step above carries a Manifest. The defining invariant across all of them:
**`forbidden_paths` always includes the live `.claude-plugin/marketplace.json`** (and, for
extraction/validation/thinning steps, `plugins/` + `shared/`) — this is the machine-checkable
enforcement of **D8 (NULL push / no live-marketplace mutation)** during the entire Claude-run local
phase. The live catalog and the plugins source are mutated **only** in the operator window
(Step 10's runbook), which is outside trekexecute's verified surface by design.
### Failure recovery rules
- **revert** — script-authoring steps: `git checkout -- <files>`; extraction/validation operate in
`$WORK` so reverting is `rm -rf $WORK/<key>`.
- **retry** — idempotent transforms (Steps 39, 11) re-run cleanly.
- **escalate** — a standalone-validation FAIL (Step 6/7/11) or a missing tool (Step 1) stops and
asks the operator; never mask an SC2/SC6 failure.
- **Checkpoint** — each step commits **locally** (D8: no push), so a later failure cannot corrupt
completed tooling.
## Alternatives Considered
| Approach | Pros | Cons | Why rejected |
|---|---|---|---|
| `--subdirectory-filter plugins/<name>` for every plugin (brief's literal §9 form) | Simplest one-flag invocation | **Silently drops** 136+50+3 commits of pre-rename history for voyage/llm-security/linkedin-studio (F1) | Violates SC6 for 3 high-churn plugins; multi-`--path` is mandatory for renamed plugins |
| `ssh://git@…` `url` sources (brief §6) | Matches the operator's SSH push workflow | `marketplace add` rejects SSH (#9740); `url` schema names only `https://`/`git@` | F2 — HTTPS is the only verified-accepted form; SSH risks a dead marketplace |
| Hard-cut history (`git init` from current tree) for all repos | Fast; no filter-repo | Loses granular blame/history | D2 ratified filter-repo preservation; only the archived monorepo keeps full history under hard-cut |
| Big-bang all-at-once cutover | One window, less bookkeeping | No reversible checkpoints; marketplace dark during cutover | D5 ratified incremental mixed-source; R1 |
| Run extraction in the working checkout | No extra clone | Races concurrent sessions on `.git/index` (R4); destructive to live tree | §10 mandates a dedicated `--no-local` fresh clone |
| Skip the pilot, push all 11 then test | Fewer window steps | If Forgejo HTTPS/external-source resolution fails, all 11 are already pushed | F2 — the Forgejo external-source chain is the highest-stakes unverified link; pilot de-risks it on 1 repo |
## Test Strategy
- **Framework:** Node `node:test` (glob form `node --test path/*.test.mjs` — Node 25 gotcha), zero
npm deps (house style); bash `validate-plugin.sh` for structure checks.
- **Existing patterns:** per-plugin `tests/**/*.test.mjs`; `claude-design`/`ms-ai-architect`
`validate-plugin.sh`; config-audit `UPDATE_SNAPSHOT` rebasing seam; the SHA-256 `MANIFEST.json` +
`detectDrift` in `sync-design-system.mjs`.
- **New tests in this plan:** one test per script step (Steps 19, 11), each asserting the script's
contract against the **live monorepo** or a freshly-extracted repo in `$WORK` — so the tooling is
proven before the dry-run, and the dry-run (Step 11) is the integration test over all 11 targets.
### Tests to write
| Type | File | Verifies | Model test |
|---|---|---|---|
| Unit | `migration/00-preflight.test.mjs` | plugin-map shape, rename-awareness, HTTPS URLs | `voyage/tests/validators/*.test.mjs` |
| Unit | `scripts/sync-design-system.test.mjs` | `--source` parity, `--check` drift | `voyage/tests/scripts/*.test.mjs` |
| Integration | `migration/10-extract.test.mjs` | renamed-plugin history retained, clean re-tag | `voyage/tests/integration/*.test.mjs` |
| Unit | `migration/20-rehome-config.test.mjs` | re-rooted gitignore/gitleaks, mailmap copied | `config-audit/tests/lib/*.test.mjs` |
| Unit | `migration/30-fix-references.test.mjs` | no `../../README.md`, URLs reconciled | `linkedin-studio/render/__tests__/*.test.mjs` |
| Integration | `migration/40-validate-standalone.test.mjs` | SC2/SC7 gate fails on planted leak | `voyage/tests/synthetic/*.test.mjs` |
| Unit | `migration/50-config-audit-snapshots.test.mjs` | portable snapshots pass; generic validator | `config-audit/tests/snapshot-default-output.test.mjs` |
| Unit | `migration/60-rewrite-marketplace.test.mjs` | mixed-source, HTTPS, ref pin | `voyage/tests/parsers/*.test.mjs` |
| Unit | `migration/70-thin-catalog.test.mjs` | no plugins/shared, CONVENTIONS.md present | `config-audit/tests/*.test.mjs` |
| Integration | `migration/99-dryrun.test.mjs` | 11/11 targets, SC2/SC6/SC7, NULL push | `voyage/tests/integration/*.test.mjs` |
## Risks and Mitigations
| Priority | Risk | Location | Impact | Mitigation |
|---|---|---|---|---|
| Critical | Renamed-plugin history silently dropped (F1) | `10-extract.sh` | SC6 fails; voyage/linkedin-studio lose 3060% history | Multi-`--path` per `plugin-map.json`; Step 3 test asserts commit count from both names |
| Critical | Forgejo HTTPS external-source resolution unverified (F2) | window / `RUNBOOK.md` | Marketplace could go dark if `url` source can't resolve self-hosted Forgejo | **Pilot gate** (Step 10) proves the chain on 1 repo before the other 10; HTTPS not SSH; public repos |
| High | ms-ai-architect 148 MB blob-bomb (F3) | `10-extract.sh` `blob_strip` | Extracted repo bloated; slow push | `--strip-blobs-bigger-than 1M` (default, operator-confirmed); decision surfaced in Step 3 |
| High | config-audit snapshots embed absolute path (F4) | Step 7 | SC2 blocker | `UPDATE_SNAPSHOT=1` re-seed of the rebasable `snapshot-default-output` in-clone; **by-name exclude** the 2 frozen back-compat tests (`json-backcompat`/`raw-backcompat`) from the SC2 gate; flagged for operator ratification (edges §5) |
| High | Tag collision/leak across extracts (F5) | `10-extract.sh` | Wrong tags in each repo; ambiguous `v5.0.0` | Strip all tags, re-create exactly `v<version>`; Step 3 test asserts single tag |
| High | Marketplace goes dark mid-migration (R1/SC8) | window | Broken installs | Mixed sources (Step 8 `--only`); never remove `./plugins/x` before external repo verified; install-smoke each step |
| Medium | README/metadata reference rot (F6) | `30-fix-references.mjs` | 404 disclosure links, wrong repo URLs standalone | Rewriter inlines disclosure + reconciles `repository`/`homepage`; Step 5 test asserts none remain |
| Medium | Root-only `.mailmap`/`.gitleaks.*` don't travel (F6/M5) | `20-rehome-config.sh` | Lost author dedup; re-flagged false positives | Copy `.mailmap`; generate per-plugin `.gitleaks` with re-rooted fingerprints |
| Medium | Concurrent-session corruption during surgery (R4) | all extraction | Index race in working tree | `git clone --no-local` fresh clone; working checkout read-only; quiesce other writers in the window |
| Medium | Untracked non-gitignored files leak on blanket add (M2) | extraction | `progress.json` + 2 briefs enter an extract | filter-repo is history-based (untracked files excluded); Step 6 SC7 check catches leaks |
| Low | `okr.local.md.template` is intentionally tracked | Step 6 | False SC7 failure | Explicit allow-exception in the SC7 check |
| Low | Forgejo no auto-update token for private repos (F2) | window | Background update of a private marketplace unsupported | Recommend **public** repos (§9); manual `/plugin marketplace update` otherwise |
## Assumptions
| # | Assumption | Why unverifiable | Impact if wrong |
|---|---|---|---|
| 1 | Claude Code resolves a self-hosted Forgejo `url` source over HTTPS with a pinned `ref` | Not testable until a repo exists on Forgejo (push is operator-gated, D8) | If false, the whole external-source model fails — **the Step 10 pilot is the explicit test that gates the rest** |
| 2 | `git filter-repo` carries each plugin's `CHANGELOG.md` + history intact under multi-`--path` | filter-repo tag/history edge-cases ("tags not on path") lack a crisp documented rule | Verified empirically by Step 3/11 tests (commit counts, tag list) before the window |
| 3 | Stripping blobs >1 MB from ms-ai-architect loses only screenshots, not source | Depends on whether any non-screenshot blob exceeds 1 MB | Step 3 surfaces the strip list as a decision; operator confirms before the window |
| 4 | Excluding config-audit's 2 frozen back-compat tests (`json-backcompat`/`raw-backcompat`) from SC2, plus re-seeding `snapshot-default-output` in-clone, is acceptable (edges §5 non-goal) | A judgment on "what SC2 means" for fixtures that embed the original capture machine's path + a sibling marketplace + deleted plugins | Flagged for operator ratification (header + Step 7); fallback = the config-audit-internal work of normalizing the v5.0.0 snapshot paths (out of migration scope) |
| 5 | Public visibility is acceptable for all `open/` plugin repos | Visibility is a policy choice, not technical | Operator confirms at the window; private repos lose background auto-update (F2) |
## Verification
*Per-step manifests run during execution (Steps 111). These are the cross-step, end-to-end checks
mapping to the brief's SC1SC8.*
- [ ] **SC6 (F1):** `git -C /tmp/polyrepo-migration/voyage log --oneline | wc -l` → expected: `≥ 250`
(both `voyage` + `ultraplan-local` history present).
- [ ] **SC6 (F5):** `git -C /tmp/polyrepo-migration/voyage tag` → expected: exactly `v5.1.1`.
- [ ] **SC2/SC7:** `bash docs/marketplace-polyrepo-migration/migration/40-validate-standalone.sh
--all` → expected: every target `PASS` (config-audit gate = full `tests/**/*.test.mjs` **minus**
`json-backcompat` + `raw-backcompat`, after the Step 7 re-seed; linkedin-studio `.mjs` core is the
hard gate, TS analytics ADVISORY); no tracked `*.local.md`/`STATE.md` except
`okr/templates/okr.local.md.template`.
- [ ] **SC5 (two extracted repos):** with the DS + ms-ai-architect extracts in `$WORK`,
`node scripts/sync-design-system.mjs ms-ai-architect --source "$WORK/playground-design-system"
--target "$WORK/ms-ai-architect"` then `node scripts/sync-design-system.mjs --check ms-ai-architect`
→ expected: `MANIFEST OK`; `git -C "$WORK/ms-ai-architect" diff --exit-code playground/vendor/` →
expected: empty (byte-identical re-vendor).
- [ ] **SC3 (F2):** `node …/60-rewrite-marketplace.mjs --all --out /tmp/mp-all.json && grep -c
'"./plugins/' /tmp/mp-all.json` → expected: `0`; `grep -c 'ssh://' /tmp/mp-all.json` → `0`.
- [ ] **SC4:** `bash …/70-thin-catalog.sh --workspace /tmp/thin && ls /tmp/thin` → expected: no
`plugins/`, no `shared/`; `CONVENTIONS.md` present.
- [ ] **D8 held:** after the full dry-run, `git -C $REPO_ROOT status --porcelain
.claude-plugin/marketplace.json plugins shared` → expected: **no output** (live surfaces
unchanged); `git log origin/main..main --oneline` shows only local `migration/` commits, **none
pushed**.
- [ ] **SC1/SC8 (operator window, post-pilot):** per `RUNBOOK.md`, `/plugin marketplace add
https://…/open/ktg-plugin-marketplace.git` + `/plugin install <plugin>@ktg-plugin-marketplace`
succeeds for the pilot, then after each graduation.
## Estimated Scope
- **Files to modify:** 1 (`scripts/sync-design-system.mjs`).
- **Files to create:** ~24 (11 migration scripts/docs + 11 tests + 2 templates), all under
`docs/marketplace-polyrepo-migration/migration/` + 1 root test.
- **Repos produced (local, unpushed):** 11 (10 plugins + DS) in `$WORK` during the dry-run.
- **Complexity:** high (rename-aware history surgery, mixed-source live-marketplace choreography,
operator-gated irreversibility) — but the **Claude-run local half is fully reversible** (NULL push,
throwaway clones); only Step 10's window is irreversible and operator-run.
## Execution Strategy
Steps exceed 5 → grouped into sessions/waves. The local half (Sessions 15) is trekexecutable now;
the window (Session 6) is operator-run from `RUNBOOK.md`, **not** via trekexecute.
### Session 1: Foundation
- **Steps:** 1, 2
- **Wave:** 1
- **Depends on:** none
- **Scope fence:**
- Touch: `docs/marketplace-polyrepo-migration/migration/`, `scripts/sync-design-system.mjs(.test)`
- Never touch: `.claude-plugin/marketplace.json`, `plugins/`, `shared/`
### Session 2: Extraction + per-repo prep scripts
- **Steps:** 3, 4, 5
- **Wave:** 2
- **Depends on:** Session 1 (reads `plugin-map.json`)
- **Scope fence:**
- Touch: `docs/marketplace-polyrepo-migration/migration/`
- Never touch: `plugins/`, `shared/`, `.claude-plugin/marketplace.json` (extraction operates in `$WORK`)
### Session 3: Validation + blockers
- **Steps:** 6, 7
- **Wave:** 3
- **Depends on:** Session 2 (validates extracted repos)
- **Scope fence:**
- Touch: `docs/marketplace-polyrepo-migration/migration/`
- Never touch: `plugins/config-audit/` live (Step 7 operates on the extract in `$WORK`)
### Session 4: Catalog + window staging
- **Steps:** 8, 9, 10
- **Wave:** 3 (parallel with Session 3 — independent scripts)
- **Depends on:** Session 1 (versions/URLs)
- **Scope fence:**
- Touch: `docs/marketplace-polyrepo-migration/migration/`
- Never touch: live `.claude-plugin/marketplace.json`, `CLAUDE.md`, `README.md` (rewriters write to `--out`/`--workspace`)
### Session 5: Local dry-run (de-risk proof)
- **Steps:** 11
- **Wave:** 4
- **Depends on:** Sessions 2, 3, 4 (exercises every script)
- **Scope fence:**
- Touch: `docs/marketplace-polyrepo-migration/migration/`, `$WORK`
- Never touch: any live source/catalog surface (D8 asserted by the step's own verify)
### Session 6: Operator window (NOT trekexecute — operator-run from RUNBOOK.md)
- **Steps:** runbook only (pilot → DS → high-churn → low-churn → thin catalog)
- **Wave:** 5 (authorized weekend window, D8)
- **Depends on:** Session 5 green dry-run + operator authorization
- **Scope fence:**
- Touch: Forgejo (repo creation + push), live `.claude-plugin/marketplace.json` (mixed→external), catalog thinning
- Never touch: anything before the pilot passes
### Execution Order
- **Wave 1:** Session 1
- **Wave 2:** Session 2 (after Session 1)
- **Wave 3:** Sessions 3 + 4 (parallel, after Session 2 / Session 1 respectively)
- **Wave 4:** Session 5 (after Sessions 24)
- **Wave 5:** Session 6 — operator window, gated on the dry-run + explicit authorization
### Grouping rules applied
- Steps sharing the `plugin-map.json` contract → Session 1 first.
- Extraction/prep scripts (independent files) → Session 2; validation depends on them → Session 3.
- Catalog/window staging is file-independent → Session 4 parallel with Session 3.
- The dry-run gates everything before the irreversible window.
## Plan Quality Score
| Dimension | Weight | Score | Notes |
|---|---|---|---|
| Structural integrity | 0.15 | 88 | `$WORK` lifecycle now defined via a dedicated mirror clone; blob/re-tag ordering fixed (single-pass filter-repo) |
| Step quality | 0.20 | 84 | Deterministic blob decision (no runtime prompt); config-audit step corrected to the verified `UPDATE_SNAPSHOT=1` seam; gitleaks fingerprint format fixed |
| Coverage completeness | 0.20 | 86 | SC1SC8 + D1D8 + R1R8 + §9 traced; F1F6 absorbed; DS cross-repo chain + `.forgejo` survival now covered |
| Specification quality | 0.15 | 85 | filter-repo/Forgejo/marketplace commands verified; config-audit framing corrected to the real test behaviour |
| Risk & pre-mortem | 0.15 | 86 | Pilot now flips-then-installs (actually tests the ref-pin chain); dry-run honestly scoped to local mechanics |
| Headless readiness | 0.10 | 80 | Local steps deterministic & headless-ready; window operator-run by design; TS analytics explicitly advisory |
| Manifest quality | 0.05 | 85 | Every step has a checkable manifest; `expected_paths ⊆ Files:`; D8 enforced via `forbidden_paths` |
| **Weighted total** | **1.00** | **85** | **Grade: B+** |
**Adversarial review (Phase 9):**
- **Plan critic:** REVISE on the first draft (63/C) — 3 blockers + 8 major + 6 minor. **All 3 blockers
+ all 8 majors + the actionable minors addressed** (see Revisions). Contested config-audit facts
re-verified directly against the test code before revising.
- **Scope guardian:** **ALIGNED** — 0 scope gaps; all SC1SC8 / D1D8 / §9 / R1R8 covered; 4 creep
items all ACCEPTABLE (2 forced by relocation, 2 operator-ratified); **1 DEVIATION** (ssh→https)
surfaced for operator ratification in the header.
## Revisions
| # | Finding (reviewer) | Severity | Resolution |
|---|---|---|---|
| 1 | Step 7 misdescribed config-audit tests; invented `SKIP_ENV_SNAPSHOTS`; wrong-glob under-ran the suite (critic) | blocker | Re-verified the real tests. Rewrote Step 7 to use the actual `UPDATE_SNAPSHOT=1` seam (re-seed `snapshot-default-output`) + **by-name exclusion** of the 2 frozen back-compat tests + the declared runner `node --test 'tests/**/*.test.mjs'` |
| 2 | Pilot couldn't test the ref-pin chain — install resolved the in-repo source (critic) | blocker | Step 10 pilot reordered: create→push→**flip the entry to external `url`+`ref` & push catalog**→then install-smoke, so the HTTPS+ref chain is actually exercised |
| 3 | `blob_strip` left as a runtime prompt (non-headless) (critic) | major | Step 1 preflight enumerates >1 MB blobs and emits `blob_strip_safe`; Step 3 picks size-strip vs surgical path-glob deterministically — no prompt |
| 4 | filter-repo second-pass + re-tag ordering hazard (critic) | major | Single filter-repo invocation composing `--path`/`--path-rename`/`--strip-blobs-bigger-than`; re-tag as the final op |
| 5 | `$WORK`/`$REPO_ROOT` undefined; concurrency race reintroduced (critic) | major | Step 1 builds a dedicated `$WORK/_mirror` once; all extractions clone from it; per-step verifies self-contained |
| 6 | `.gitleaksignore` re-rooting used wrong format `path:line` (critic) | major | Step 4 fixed to `path:rule-id:line` (keep `generic-api-key`/`linkedin-client-id` + line); test assertion corrected |
| 7 | DS cross-repo vendor chain never validated before window (critic) | major | Step 11 runs the re-homed sync `--source`/`--target` across the two extracted repos + `--check` parity (SC5) |
| 8 | Dry-run oversold as proving the migration (critic) | major | Step 11 reworded to local-mechanics only; window-only modes (push/create/resolve) explicitly listed as pilot-covered |
| 9 | `expected_paths` not ⊆ `Files:` in every step (critic) | major | Added each `.test.mjs` to its step's `Files:` line |
| 10 | linkedin-studio TS suite silently waived for SC2 (critic) | major | Step 6 states two-tier SC2: `.mjs` core = hard gate; TS analytics = explicit ADVISORY (network-gated) |
| 11 | Preflight HEAD/status assertions go stale (critic) | minor | Relaxed to porcelain on `plugins/ shared/ marketplace.json scripts` only; idempotent |
| 12 | Step 5 inline-vs-link either/or; ai-psychosis mis-scoped (critic) | minor | Inline chosen; ai-psychosis noted verify-only, real work = 9 monorepo-pointing plugins |
| 13 | Step 9 doc-fix coupling; `.forgejo` CI uncovered; pre-claimed grade (critic) | minor | Version correction coupled to the D6 README rewrite; Step 6 asserts `.forgejo/` survives; grade set post-review |
| 14 | ssh→https overrides a ratified brief literal without annotation (guardian) | deviation | Surfaced as a **brief correction requiring operator ratification** in the header; kept HTTPS (correct per #9740) |