---
name: bootstrap
description: Provision a repo into the org with board, registry, rules, and plugin setup.
---

**Host-native invocation:** Claude `/mmi:bootstrap` · Codex `$mmi:bootstrap` · Cursor `/bootstrap` · jervcode `/bootstrap` · Hermes skill_view. A backticked `/name` in this doc names the matching workflow (this skill or a sibling), not a literal command.

# /bootstrap — provision a repo into the org

The one-time onboarding that turns a repo into a first-class org citizen. **Master-admin only, run from
`MMI-Hub` (the hub).** Every step operates org-level resources (Project, Ruleset, org secrets, member
access) **through the GitHub App's installation token** — not a human credential — so the gate is who may
invoke the skill (the master holds the App key), not the caller's GitHub role.

Bootstrap operates on the target repo entirely through the App — **no per-repo checkout**. The repo must
already be named on the taxonomy `<CATEGORY>-<PascalName>`.

## Seed sources + create-vs-upgrade

The org-standard scaffolding is a **machine-readable manifest** — `skills/bootstrap/seeds/manifest.json`
(loaded by the CLI; `mmi-cli devops bootstrap apply <repo> [--execute]` consumes it). Every seed carries an **ownership**:

- **`org`** — org-delivered, **overwritten on upgrade** (the org owns it): the issue templates, the gate
  workflow, and the org-managed `.gitignore` block. `source: self` = copied verbatim from MMI-Hub's own
  current file. Personal **agent guides** (`AGENTS.md`/`CLAUDE.md`/`.claude/settings.json`) are **never**
  seeded — they are developer-owned and gitignored, carried per machine by each developer's own plugins
  (Jervaise's ride Jerv PowerTools), not delivered, overwritten, or fanned out by MMI; the org push ruleset
  `mmi-no-agent-files-org` even blocks committing them. Board moves are central (the Hub webhook), so **no
  per-repo board workflow is stamped**.
- **`repo`** — created **once on a fresh bootstrap**, **never clobbered on upgrade** (the repo owns its
  content): `README.md`, `architecture.md`.
  These render `seeds/*.template.*` with `{{PLACEHOLDERS}}`.

So **create** stamps every seed; **upgrade** refreshes the `org` seeds and adds any *missing* `repo` seeds
without overwriting existing repo-owned files (D35: legacy docs are archived + written fresh, never carried
over verbatim). The manifest's `labels` list is the canonical label set — three: `bug`, `feature`, `task`.
Priority is a **board field**, never a label (#1454 retired the four `priority:*` labels; `bootstrap verify`
checks they are gone). The per-step instructions below
are the manual path; `bootstrap apply <repo> [--execute]` automates them from this manifest.

**`bootstrap apply` is a single-repo tool** — fresh bootstrap/onboarding, or refreshing one repo's own
drifted seed. It is **never** the fleet-wide fan-out for an `org`-owned seed edit: that write path is
propagation from a Hub merge (#4233), and the seeded `agent-pr.yml` itself now fails the merge gate on
any repo that receives a hand-edit to an org-owned target path outside a propagation/bootstrap-delivery
branch (#4241). Running `apply --execute` against every registry repo by hand to fan out a Hub edit is
exactly the copy-per-repo pattern this rule exists to end — see Hub#4234.

**Rollback is per-repo, never a second fleet overwrite (#4240).** When a propagated `org`-owned seed
breaks a repo, the recovery is `mmi-cli devops bootstrap rollback <repo> --target <t> [--execute]` — it resolves
the ONE merge commit that repo's `bootstrap propagate` (#4238) run actually landed (live from the repo's
`seed-propagate-<slug>` merged-PR history, or replayed from a persisted propagate `--json` report via
`--record`) and opens an ordinary revert PR of it, through that repo's own gate, on a `seed-rollback-<slug>`
branch. It refuses — never guesses a commit — when no clean, single-file propagation record resolves for
that repo+target. **Explicitly forbidden as a rollback path: a fleet-wide `bootstrap apply --execute` of
"the old bytes"** — that is the second blind fleet overwrite #4233 rules out, and it races the propagation
lane. **MMI-Hub stays the source of truth during a rollback**: a per-repo revert is the emergency stop, not
the fix — closure is the Hub reverting (or fixing forward) the bad seed commit on `development`, after
which reverted repos match the Hub again and the drift alarm closes itself. A per-repo revert with no
Hub-side follow-up re-alarms as drift within a week, deliberately, so an emergency divergence can never
silently become permanent.

**Release gap — a new topology value must reach TWO surfaces, and a local dev build only fixes one (#2928).**
A new `--project-type`, `--deploy-model`, or `--release-track` lands on `development` but is not live until a
release train ships it — in **both** of these places:

1. **The published/installed `mmi-cli`**, which rejects the unknown enum with no hint that it merely lags
   `development`. A local dev build routes around this:
   `node <MMI-Hub checkout>/cli/dist/index.cjs bootstrap apply ...` from a `development` checkout (rebuild
   `dist` first: `npm --prefix cli ci && npm --prefix cli run build`).
2. **The deployed registry Lambda**, which carries its *own* copies of the enums
   (`infra/src/registry-route.ts` validates `projectType` **and** `deployModel`; `releaseTrack` is CLI-side
   only) and **deploys from `main`**. A local dev build does nothing for this one.

So the local dev build is **not** a workaround for a just-merged enum value — it gets you further and then
fails at the step that matters. The CLI cheerfully accepts the new value, sends it, and the API rejects it:

```
ddb register <slug> (failed: HTTP 400 — projectType must be one of:
  web-app, hub-service, content, desktop-game, non-deployable, cli-tool, worker)
```

That failure lands **after** seeding, labels, and the ruleset, so the repo is left half-provisioned with an
almost-empty registry META row. **Bootstrapping against a topology value merged since the last train requires
a `/release` to `main` first.** The local dev build alone suffices only for values already live in the API.

## Step 0 — capability shape (confirm the four axes before any mutation)

Bootstrap is **destructive to undo**: a wrong shape means converting in place later — deleting protected
branches, temporarily disabling the org-wide `mmi-train-floor` ruleset (which affects every repo for that
window), and tearing down train artifacts (#1450). So **before Step 0a touches anything**,
surface and **confirm all four topology axes with the master** — present the recommended default for each,
never apply it silently. Use the structured-question UI; one question per axis (or one grouped confirm).

- **`class`** — `deployable` (ships an app/service; full or direct train) · `content` (docs/content;
  trunk, `main`-only). *Controls the branch model.* Recommend from what the repo **is**, not a fixed default.
- **`project-type`** — e.g. `web-app`, `cli-tool`, `worker`, `desktop-app`, `desktop-game`, `mobile-app`, `content`. *Controls which Hub
  services attach.*
- **`deploy-model`** — e.g. `tenant-container`, `none`, `content`. *Controls the deploy path.*
- **`release-track`** — `full` (development·rc·main) · `direct` (development·main, skips rc) · `trunk`
  (`main` only). *The branch set follows the track, not just the class (#1097).*


For a **deployable** repo, also capture the **gate runtime** (`node` | `python`), the **check command**, and
the **working directory** — these feed the product gate (`--var GATE_RUNTIME=`, `--var GATE_CMD=`,
`--var GATE_WORKDIR=`, and `--var GATE_CACHE_DEP_PATH=` for a non-root Node lockfile; see Step 5 / the
"Product gate + required checks" note below). Default to `node` at the repo root; recommend `python` when the
repo's app is Python.

A **content/trunk** choice provisions: `main`-only (no `rc`), **no** `.github/workflows/gate.yml` and **no**
product required-check ruleset, and a `projects.json` entry with `branch: main` (which marks it a content-class
repo). A clean content repo then verifies green — `bootstrap verify --class content` no longer reports
the deployable gate checks as FAIL (#1450).

Record the confirmed axes; Step 0a runs `verify --class <confirmed>`, Step 0b creates the repo on the
track's default branch, and the apply flags (`--project-type` / `--deploy-model` / `--release-track`) carry
the confirmed values — no silent defaulting.

## Step 0a — no-mutation verifier

Before mutating anything, run the verifier so the current gaps are concrete:
```bash
mmi-cli devops bootstrap verify "$OWNER/$REPO" --class deployable --json
# or for content repos:
mmi-cli devops bootstrap verify "$OWNER/$REPO" --class content --json
```

Run it again after Step 7. A repo is not ready for real developers until every check is green, or the report
names an explicitly manual-only item that the master has accepted for that repo. For a deployable repo the
report ends with the train doctor verdict per lane (`release`; plus `rcand` on a full track) — the same
`mmi-cli devops train doctor` door `/release` and `/rcand` open (#6070); a `ready: false` there is an
unfinished repo, **with one expected exception**: a full track's two lanes start from different branches
(`release` from `rc`, `rcand` from `development`) and one checkout stands on one branch, so the lane you
are not standing on reports the never-healed `branch-mismatch` blocker (#6432). Verify is read-only and
does not check the other branch out: a lane whose ONLY finding is that mismatch is printed `NOT EVALUATED
from this branch` and does not fail the verify (#6677). That is the finished state, not a gap — run
`mmi-cli devops train doctor --lane <lane> --json` from that lane's own branch for its verdict. Any other
blocker in that lane, or an unread origin, still makes the report red.

**Two verify FAILs are un-run apply steps or API lag, not code bugs — diagnose them as such, but never as
acceptable end states (#2928):**
- `branch protection exists` / `push allowlist configured` — **`bootstrap apply --execute` applies the
  train-branch push allowlist (Step 2b)**, so these FAIL until the live apply has run. They are security
  controls and must go green (or be an explicitly master-accepted manual item per the rule above) before
  the bootstrap is complete. A red branch-protection or push-allowlist check is an **unfinished repo**,
  never a known-benign FAIL to sign off around.
- `README has Agent context section — README.md not readable via API` on a README that demonstrably has the
  section — GitHub's contents API lags for a minute or so right after a merge. **Re-run before believing a
  content-read failure**; it clears on its own.

## Step 0b — create the repo (when it does not exist yet)

If `$OWNER/$REPO` is not on GitHub yet, create it and seed an initial commit **before** Step 1 — a brand-new
repo has no commits, so there is no branch to push and no default branch to set. **Create the first commit
through the contents API, on every track** (#3655) — an API write is not a push, so neither the #1660
protected-branch guard nor `mmi-train-floor` objects, and it is the same server-side mechanism Step 1 uses
for `rc`/`main` (#3433). `$FIRST` is the track's first branch: `development` for **full**/**direct**,
`main` for **trunk**:

```bash
FIRST=development     # full / direct
FIRST=main            # trunk (content)

# One-time namespace + first-ref creation uses the authenticated master-admin GitHub session. The current
# mmi-cli has no App-backed command for a repository that does not exist; this is the named exception.
gh repo create "$OWNER/$REPO" --private --disable-wiki
gh api -X PUT "repos/$OWNER/$REPO/contents/.gitkeep" \
  -f message="chore: initial commit" -f content="Cg==" -f branch="$FIRST"
```

`--disable-wiki` seeds `has_wiki=false` at creation: wikis are retired org-wide (compute-at-read, #4206),
GitHub defaults new repos to `has_wiki=true`, and `bootstrap apply --execute` / `bootstrap reconcile --apply`
enforce the same rule as drift checks afterwards.

On an empty repo that PUT creates `$FIRST` **and leaves it the default branch**, so no `gh repo edit
--default-branch` is needed here. Skip this step when the repo already exists with the track's default branch.

The two `gh` writes above are authorized by the authenticated master-admin login and are the one-time
bootstrap exception while no App-backed `mmi-cli` command owns a repository that does not yet exist. They
are not a general write lane: after the namespace and first ref exist, use the App-backed
`mmi-cli devops bootstrap apply --execute` path for managed seeds, labels, rulesets, and registry state.

For a **content** repo the API form is not merely tidier, it is the only thing that works: `main` is that
track's first and only branch, so the initial commit IS a push to `main` and the #1660 guard fences it — and
the deny's own advice ("land through a CI-gated PR to development") is meaningless on a repo with no commits,
no `development`, and no CI (#3541).

The old recipe here was a local `git init` + empty commit + `git push` for deployable repos, opening with
`gh repo create --private --confirm`. **That flag no longer exists** — `gh` removed it, so the first command
of the first step errored out. The local-git ritual it opened is also unnecessary: the API form above does
the same job for `development` in one call.

## Step 1 — branches

Determine the repo's **release track** first — the branch set follows the track, not just the class (#1097),
so a direct-track repo never gets a stray `rc` the `mmi-train-floor` ruleset can't clean up:

- **full** (deployable default) — three permanent branches, default `development`: `development`, `rc`, `main`.
- **direct** (deployable, skips rc — e.g. cli-tool/worker/Hub) — two permanent branches, default
  `development`: `development`, `main`.
- **trunk** (content) — one permanent branch, default `main`: `main` only.

Ensure exactly the track's permanent branches exist and set the default branch — create only what the track
uses; never create an `rc` for a direct-track repo.

**`bootstrap apply --execute` creates `rc`/`main` server-side from the track's first branch (#6070).** The
local **#1660 protected-branch push guard** fences every push to `main`/`master`/`rc`, including the
legitimate *creation* of those refs on a brand-new repo (#3433); a ref created through the API is not a
force-push, so both the guard and the `mmi-train-floor` ruleset allow it. The apply's train-readiness plan
prints one `lane-branch` row per track branch — `CREATE` (missing), `SKIP` (already exists), or `GAP`
(`bootstrap-gap`: the first branch from Step 0b is missing, so there is no source ref) — and the live apply
creates every `CREATE` row; a `422 Reference already exists` reply is the idempotent no-op, not a failure.
`development` (or `main`, for a content repo) already exists from Step 0b. Only the default branch stays a
`gh` write:
```bash
gh repo edit "$OWNER/$REPO" --default-branch development   # full / direct
gh repo edit "$OWNER/$REPO" --default-branch main          # trunk (content)
```
Verify with `gh api "repos/$OWNER/$REPO/branches" --jq '.[].name'`, or read the `lane-branch` rows of the
next dry-run `bootstrap apply`.

## Step 2 — authority (org Ruleset)

Confirm the org-level rulesets already target this repo (they apply org-wide): **`mmi-branch-protection`**
PR-gates `development` and blocks force-push/deletion there (bypass = org admins + the GitHub App 3026732),
and **`mmi-train-floor`** blocks force-push/deletion on `rc`/`main` (no PR rule — the train pushes merges and
tags directly; no bypass). (Definitions mirror live in `.github/rulesets/mmi-branch-protection.json` and
`.github/rulesets/mmi-train-floor.json`.) MMI-Hub additionally has its own repository ruleset,
`.github/rulesets/mmi-hub-required-checks.json`, for the Hub-only `cli`, `infra`, and `docs` jobs; do not
apply those contexts org-wide unless every target repo exposes them.

## Step 2b — lock the train branches (who can push)

The ruleset says *a PR is required*; this says *who may merge it*. `bootstrap apply --execute` applies the
classic branch protection on `development`/`rc`/`main` for deployable repos, or just `main` for content
repos, with **"Restrict who can push"** = the org owners + the App (#6070): the owners are resolved live
from the GitHub org admins (`orgs/$OWNER/members?role=admin`), never a hard-coded user list — an empty
resolution refuses to write an empty allowlist — and the App is `mmi-github-app`. The repo's full-write
people get added at Step 4b. Everyone else is `write`-locked on protected branches: they push feature
branches and open PRs but cannot merge there.

The plan prints one `push-allowlist` row per track branch — `UPDATE` (preserve every existing
review/status/admin/force-push bit and replace only the push restriction), `SKIP` (exact owners + App
already present), or `GAP` (`bootstrap-gap`: the protection read failed; zero writes are performed while any
control-plane read is unverified). Only the master (sole repo `admin`) can change this afterward. Full
grant/lock mechanics + inspect commands: `docs/Guides/repo-access.md`.

## Step 3 — attach to the repo's Project (one board per repo; confirm)

**One board per repo, named after it** — that is the convention the org actually runs, and what the rest of
the system assumes: the registry META carries a single `projectId` per `PROJECT#<slug>`, and the Hub webhook
adds each new issue to that one board. There are no division boards; a `<CATEGORY>`-prefixed default would
resolve to nothing (#3542). Still **confirm with the master** — repos and projects are genuinely **not 1:1**,
and joining an existing board remains a legitimate answer for a repo that belongs to an existing effort.
```bash
gh project list --owner "$PROJECT_OWNER" --format json   # existing boards to choose from
```
- **Attach** to the chosen project:
  ```bash
  gh project link "$PROJECT_NUMBER" --owner "$PROJECT_OWNER" --repo "$OWNER/$REPO"
  ```
- **Create** the chosen board if it doesn't exist yet — clone the **named template board**, never an
  arbitrary existing one, so the 4-lane `Status` field (`Todo · In Progress · In Review · Done`), the
  built-in workflows, **and** the org's view/card-field shape (#4093) all carry over instead of whatever
  the previous copy-of-a-copy happened to drift to:
  ```bash
  gh project copy 4 --source-owner "$PROJECT_OWNER" \
    --target-owner "$PROJECT_OWNER" --title "<Project>"
  ```
  **Project 4 (MMI-Hub's own board) is the template** (#4093) — it already carries the org-standard view
  triple (`List`/`Board`/`Roadmap`), Board grouping (columns=Status, swimlanes=Repository), and the
  canonical Board card-field set (`Title, Assignees, Status, Labels, Linked pull requests, Parent issue,
  Sub-issues progress, Priority`). `bootstrap verify` asserts all three against every board (next note); a
  freshly copied project starts green on them instead of inheriting a stale source's drift. Copying from
  anything else re-introduces the "5 card-field variants across 16 boards" problem #4093 was filed to end.
  Then **interview the master** for the seed short description + README (what it tracks, member repos, links
  to the repos' `README.md`/`architecture.md`); set them via the `updateProjectV2` mutation
  (`shortDescription`, `readme`). If cloning, verify the built-in workflows survived (next note).
- **Built-in workflows:** `mmi-cli devops bootstrap verify` checks these Project workflows are enabled:
  `Auto-add sub-issues to project`, `Auto-archive items`, `Item added to project`, and `Item closed`. The
  Todo/In Progress/In Review moves are **central** (the Hub webhook); the built-in `Item closed` sets `Done`
  on merge. GitHub's public GraphQL schema exposes delete/read surfaces for Project workflows but no
  create/update/enable mutation; if any required workflow is missing or disabled, repair it in the project's
  **Workflows** settings before calling the repo ready for developers.
- **Board shape (#4093):** `mmi-cli devops bootstrap verify` also checks the view triple (`List`/`Board`/`Roadmap`
  by name **and** layout), the Board view's grouping, and its card fields against the standard the template
  board (project 4) carries. Grouping (columns=`Status`, swimlanes=`Repository`) has **no** GraphQL create/
  update mutation (`ProjectV2ViewConfigurationInput` carries only `visibleFieldIds`) — a drifted swimlane or
  column can only be fixed in the UI: Board view → ⚙ (top-right) → Group by / Swimlanes → Save view. The
  view triple and the card fields (`Title, Assignees, Status, Labels, Linked pull requests, Parent issue,
  Sub-issues progress, Priority`) ARE API-writable (`createProjectV2View`/`updateProjectV2View`), so a
  drifted one can be fixed live instead of only reported.

Record the chosen `projectNumber`/`projectId` plus Status/Priority field ids in the Hub registry META
(`PROJECT#<slug>`) by re-running `mmi-cli devops bootstrap apply --execute` **after** `gh project link`. Apply reads
the repo's linked board and derives all of them itself (#3543) — pass `--var PROJECT_ID=<node id>` only to
override, which is also what you need when the repo is linked to more than one board and apply therefore
refuses to guess. **Verify the row afterwards** (`mmi-cli oracle org project get <owner/repo>`): a registered repo
whose META carries no `projectId`/`statusFieldId` looks finished everywhere else while the Hub webhook has
nothing to move issues with, and `registry project board META exists` is the single check that says so. Going forward the thin Lambda adds each new issue to that project on `issues.opened` and sets
`Status: Todo`.

**Register the project in the Hub registry.** The same registry META row carries `{name, slug, projectId,
repos[]}`; do not append to a committed `projects.json`. Repo wikis and doc freshness are owned by
the repo itself. If attaching this repo to an existing project, merge this repo into that project's `repos[]`
instead of creating a new project.

## Step 4 — vault tiers + deploy substrate

Provision the repo's vault namespace and deploy substrate from the Hub, not from repo-local Actions
secrets. Runtime config names live in the two-tier vault (`/mmi-future/<slug>/dev|rc|main/*`) and are
managed through `/secrets`; never use `gh secret set` for product runtime config.

The default `tenant-container` substrate is a **Hetzner box** (`hetzner-ssh`): the box writes the release
`.env` from the registry + vault and runs the container via docker-compose, deployed over the Hub's bounded
SSH lane. Do **not** create an AWS OIDC deploy role or a repo deploy Action for it. **Ask the master for the
box assignment** — the `sshHost` (and the loopback port) per stage — then write the `DEPLOY#<stage>` rows with
`mmi-cli oracle org project set-deploy <owner/repo> --stage <dev|rc|main> --ssh-host <host> [--port <p>]` (defaults:
`substrate: hetzner-ssh`, deploy path `/opt/mmi/<slug>/<stage>`, service = slug, ssh-user `root`). Without those
rows the tenant cannot deploy (`tenant-deploy.yml` errors on missing `DEPLOY#` coords), so do not skip this.
`bootstrap apply` prints one `deploy-row` row per track stage for container models (#6070): `SKIP` when the
registry row exists, `GAP` (`bootstrap-gap`) with the exact `set-deploy` command otherwise — a box
assignment cannot be invented, so this row is the one train-readiness fact the apply never writes itself.
Keep every runtime config value in the vault; never paste secret values into logs.

Only an AWS `tenant-container` (the exception) provisions the reusable tenant stack: release bucket,
per-stage OIDC deploy role, and constrained service-control role/document. Either way the deploy path
trusts the Hub's central `tenant-deploy.yml` / `tenant-control.yml` environments, not a product repo deploy
workflow.

## Step 4b — developer access

**Repository Environments (#4458, plan-gating #5032)** — the track's deployment environments are
control-plane state this skill's own `bootstrap verify` asserts, so live apply provisions them itself:
`mmi-cli devops bootstrap apply --execute` creates `dev`/`rc`/`main` (per the release track: direct has no
`rc`, trunk has `main` only) with a **deployment-branch policy = protected branches only** — the one
protection the check exists to carry. On an org plan whose billing entitlement rejects protection rules
(an explicit `wait_timer` or required `reviewers` comes back 422 naming the plan), apply provisions the
settable policy and prints `plan-gated protections` for those rules; `bootstrap verify` then reports the
environment rows as green with an `advisory:` detail naming what the plan cannot set — never a FAIL. The
policy is set either way, so a repo on the lower plan is still protected and ready for developers.

Grant each developer their **org membership + repo access** through the App (`gh api` with the App token):
`write` for a developer; for a **full-write** member ("project-admin") also add them to the train-branch
push allowlist from Step 2b (so they can merge protected development PRs). GitHub's
collaborator list + the per-branch allowlist are the record — no separate roster. Exact commands:
`docs/Guides/repo-access.md`.

## Step 4c — CI trigger, labels, templates

- **Enable workflow triggers** — a freshly-provisioned repo can have GitHub Actions auto-trigger stuck off
  (push/PR never run workflows though dispatch does). Toggle it off→on:
  ```bash
  gh api -X PUT repos/$OWNER/$REPO/actions/permissions -F enabled=false
  gh api -X PUT repos/$OWNER/$REPO/actions/permissions -F enabled=true -f allowed_actions=selected
  ```
- **Self-hosted CI is the org default** — the `mmi-automation` runner group is org-wide (all repos), so a
  new repo needs no manual group join. Any CI workflow it adds uses
  `runs-on: [self-hosted, linux, x64, mmi-live]` (never `ubuntu-*`/`windows-*`/`macos-*`, which bill
  GitHub-hosted minutes); jobs needing system packages run in a `container:`. The runner has **six
  concurrent job lanes** — independent checks belong in separate parallel jobs (each with its own
  `runs-on`), never chained into one serial job or throttled with unneeded `concurrency` groups. See
  `docs/Guides/gh-runner-runbook.md`.
- **Product gate + required checks (#1333, stack-aware #1550)** — deployable repos bootstrap-seed
  `.github/workflows/gate.yml` (single `gate` job — the simple default for one check command; a repo
  that grows several independent suites splits them into parallel jobs across the runner's twelve lanes,
  as MMI-Hub's `cli`/`infra`/`docs` gate does) and a ruleset reference at
  `.github/rulesets/mmi-product-required-checks.json`. The gate is stack-aware: capture the repo's
  **runtime** (`node` | `python`), its **check command**, and its **working directory** at interview time
  and pass them as `--var GATE_RUNTIME=node|python`, `--var GATE_CMD=...`, `--var GATE_WORKDIR=...` (and
  `--var GATE_CACHE_DEP_PATH=<path/to/package-lock.json>` when the app's Node lockfile is not at the repo
  root). Defaults: `node` runtime, `npm run check` / `npm ci` at the repo root; a Python repo defaults to
  `pytest` / `pip install -e ".[dev]"` with `--var GATE_PY_VERSION=` (3.11 default). The runtime selects
  which setup step (`setup-node` vs `setup-python`) the rendered `if:` fires. The check command runs
  under the org **wall-clock budget** (#3178): the render pins `run-with-budget` to the CLI's blessed SHA
  with `--var GATE_MAX_SECONDS=` (300s onboarding default — tighten once the gate is measured, via
  `--var` or `org project set <repo> --var gate={"maxSeconds":N}`); `ci audit` and the release train
  both enforce the step, so do not remove it. Every trigger runs the full `GATE_CMD`. `bootstrap apply --execute` installs that JSON as the
  repository ruleset itself (#6070) and reports one `product-ruleset` row: `PARK` (the gate file is not on
  the base branch yet — installed disabled), `WAIT` (the gate exists but has not emitted a proven-green
  `gate` context — kept disabled; re-run apply after the gate passes), `ACTIVATE` (gate green — enforcement
  active), or `SKIP` (already exact). The activation boundary is fail-closed on purpose: a required context
  that cannot pass yet would lock the seed PR behind itself. `mmi-cli devops ci reconcile --apply --repo
  $OWNER/$REPO` remains the supported fallback; confirm `product required-check ruleset enforcement active`
  with `bootstrap verify` before reporting bootstrap complete. MMI-Hub keeps its own three-job gate
  (`cli`/`infra`/`docs`) — never apply the product ruleset there.
- **The committed ruleset owns its up-to-date policy.** New repositories default to
  `strict_required_status_checks_policy: false`. Bootstrap and CI reconciliation preserve that policy
  when updating contexts, and converge the live ruleset to the committed setting. Repositories may
  explicitly require an up-to-date head. `pr merge` / `pr land` still update a BEHIND head from the base
  with a merge commit and re-wait once — see `docs/Guides/train-troubleshooting.md#head-behind-base`.
- **A brand-new repo cannot pass the gate you just installed — its first commit must carry a real project
  (#2928).** The seeded `gate.yml` runs `GATE_INSTALL_CMD` + `GATE_CMD` (`npm ci` + `npm run check` by
  default) **unconditionally**. An empty repo has no `package.json`, so the gate **fails on the seed PR
  itself** — and since the seed PR is what *installs* the gate, this is the normal first-bootstrap path, not
  an edge case. The repo cannot go green and `bootstrap verify` cannot pass until the first commit contains a
  real project satisfying `GATE_CMD`: a `package.json` with a `check` script (or the Python equivalent), plus
  its lockfile. Landing that minimal project is **part of finishing the bootstrap**, not follow-up work —
  otherwise the gate is unpassable by construction.
  **The `check` must be able to fail.** It has to actually compile or lint the code being committed — a
  script that exits 0 without looking at anything does **not** satisfy `GATE_CMD` and must never be committed
  to force a green gate. The goal is a gate that catches a broken change, not a green tick wired to nothing.
  A typecheck plus a lint is enough to start. **Do not seed a test suite to satisfy `GATE_CMD`** — tests are
  opt-in org-wide (#3562); a new repo seeds tests only for paths its own `test-policy.json` marks mandatory,
  and an empty `mandatory` array is the normal answer.
- **Standard labels** — type labels only (the issue templates reference them). **Priority is a Project
  field, not a label** (#416): never seed `priority:*` labels; `--priority` writes the board field.
  ```bash
  gh label create bug     --color d73a4a --description "Something is broken or behaving wrong" -R $OWNER/$REPO
  gh label create feature --color a2eeef --description "New capability or enhancement"        -R $OWNER/$REPO
  gh label create task    --color 0052cc --description "Task, chore, or improvement"           -R $OWNER/$REPO
  ```
- **Board view (card fields, #4093)** — the Project's Board view must show the org-standard card set:
  `Title, Assignees, Status, Labels, Linked pull requests, Parent issue, Sub-issues progress, Priority`.
  Cloning from the template board (project 4, Step 3) already produces this; `bootstrap verify` asserts it
  (`Board view card fields match the org standard`) on every subsequent run, so drift shows up there rather
  than only at bootstrap time. Fix via Board view → **Fields**, or `updateProjectV2View(configuration:
  {visibleFieldIds:[...]})`. Strip any legacy `priority:*` / taxonomy labels with `mmi-cli oracle board doctor --fix`.
- **Org App credentials** — nothing to register per repo (#494). Board moves are central (the Hub webhook
  moves Todo/In Progress/In Review for every repo) and the org App token is minted inside the Hub's own
  central workflows, so `MMI_APP_ID` / `MMI_APP_PRIVATE_KEY` live only on the Hub — a product repo seeds no
  App var/secret.
- **Issue templates** — seed `.github/ISSUE_TEMPLATE/` (Bug · Feature · Task + `config.yml`).
- **Merge settings (org canon)** — every repo: auto-merge on, squash on, automatic merged-branch deletion
  off. A merged remote branch survives until delayed cleanup proves no open PR still uses it as a base
  (#5789); deleting it at merge can irreversibly close stacked PRs:
  `gh api -X PATCH repos/$OWNER/$REPO -f allow_auto_merge=true -f allow_squash_merge=true -F delete_branch_on_merge=false`
- **Cursor environment** — `.cursor/environment.json` is retired org-wide (#2501): it caused Cursor to
  auto-spawn cloud agents nobody asked for. Bootstrap no longer seeds it and no
  repo should carry a tracked copy.

## Step 5 — install the plugin + seed docs

- Bootstrap does **not** seed `.claude/settings.json` or any agent guide (hub-v3 WS4). The developer installs
  the org plugins per machine: `mmi@mutmutco` (the plugin carries the org tooling + skills) and
  `superpowers@claude-plugins-official` (Anthropic's skills framework — TDD, debugging, subagent dev).
  Personal agent guides (`AGENTS.md`/`CLAUDE.md`) are developer-owned and gitignored — MMI never delivers,
  overwrites, or deletes them; Jervaise's guide rides Jerv PowerTools.
- Seed `README.md` + `architecture.md` from the templates, then **fill them before finishing** — `bootstrap
  verify` now fails on any leftover `(placeholder)` or `{{TOKEN}}` (#1520). Fill the code-local fields from
  what this bootstrap already knows: **Stack / Run locally / Verify** from the Step-4c gate + install commands
  and the repo's actual code; **Gotchas** and the architecture **Overview / Build & deploy** from the confirmed
  Step-0 axes (class, project-type, deploy-model). Do **not** write the release track, board number, or deploy
  coords as a value — the templates point at `mmi-cli oracle org project get` (registry SSOT, never copied, so it cannot drift).
  Do not write `AGENTS.md` / `CLAUDE.md` — these are developer-owned, gitignored agent guides, never a bootstrapped repo file (the `mmi-no-agent-files-org` ruleset blocks committing them).
- **`docs/index.md` is an optional generated routing index (#3545).** Apply may create `docs/index.md` once for link routing; it never rewrites it. Once the repo has docs of its own,
  `mmi-cli oracle docs index --write` owns the routing artifact and `--check` gates drift — it is not product
  current-state SSOT. Decision records under `docs/decisions/` remain append-only *why*.
- `.claude/settings.local.json` is local-only and gitignored; bootstrap seeds no committed `.claude/settings.json`.
- **No agent guide is committed — none, anywhere (#2921).** `mmi-no-agent-files-org` is active with no bypass
  and restricts `AGENTS.md`, `CLAUDE.md`, `GEMINI.md`, `.claude/**`, `.codex/**`, `.agents/**` **and
  `.cursor/rules/**`**. Bootstrap used to seed `.cursor/rules/<repo-slug>.mdc`, which the wall refuses with a
  409 — it wrote a file its own ruleset bans, then verified the file was there. That seed, its template, and
  the verify check are gone. Repo-specific agent guidance belongs in **`README.md` § Agent context** (stack,
  verify commands, gotchas) with the deep build truth in `architecture.md`; agents read those. Personal agent
  guides stay developer-owned and gitignored, carried per machine by each developer's own plugins.

## Step 6 — register Hub META

Do not seed a product repo control-plane config file. `mmi-cli` carries the Hub API endpoint and resolves board,
deploy, secret, and project state from the Hub registry at runtime.

Choose the v2 shape explicitly. Use `--project-type web-app --deploy-model tenant-container` for ordinary
web tenants, `--project-type desktop-game --deploy-model none --clear-web-profile` for a desktop game,
`--project-type desktop-app --deploy-model none --clear-web-profile` for a packaged desktop application
(swap in `--deploy-model registry-publish` when it also publishes a package alongside its installer),
`--project-type mobile-app --deploy-model none --clear-web-profile` for a phone app distributed through the
app stores, and
`--class content --project-type content --deploy-model content --clear-web-profile` for a content/KB repo.
Run the apply path with the board variables discovered above, or register the same values with
`mmi-cli oracle org project set` from the Hub or from the target project checkout:

```bash
mmi-cli devops bootstrap apply "$OWNER/$REPO" --class deployable \
  --project-type web-app --deploy-model tenant-container --execute \
  --var PROJECT_OWNER="$PROJECT_OWNER" \
  --var PROJECT_NUMBER="$PROJECT_NUMBER" \
  --var PROJECT_ID="$PROJECT_ID" \
  --var STATUS_FIELD_ID="$STATUS_FIELD_ID" \
  --var STATUS_TODO="$STATUS_TODO" \
  --var STATUS_IN_PROGRESS="$STATUS_IN_PROGRESS" \
  --var STATUS_IN_REVIEW="$STATUS_IN_REVIEW" \
  --var STATUS_DONE="$STATUS_DONE"
```

Tenant-container repos must carry a `docker-compose.yml` and Dockerfile that build from the shipped source
archive; the train does not ship a prebuilt `dist/`. **Bootstrap seeds both files** for
`deployModel: tenant-container` from `skills/bootstrap/seeds/` (rendered from
the bootstrap seed Dockerfile and compose templates). The box
writes the release `.env` from the registry + vault at deploy time; the compose file carries `env_file: .env`
and the app reads plain env vars — it must **not** self-load SSM and must **not** ship a committed `.env`.

For a `web-app` that declares `oauth` META, print the canonical OAuth surface and provision the client once:

```bash
mmi-cli vault org oauth plan --repo "$OWNER/$REPO"        # the exact JS origins + redirect URIs + canonical SSM keys
```

Register those JS origins + `/api/auth/callback` redirect URIs on the Console client (master, per
`docs/Guides/oauth-provision.md`), then store the creds in the canonical keys in one step:

```bash
mmi-cli vault org oauth set-creds --repo "$OWNER/$REPO" < client.json   # the Console "Download JSON" file
```

The keys are the one stageless pair `GOOGLE_CLIENT_ID` + `GOOGLE_CLIENT_SECRET` at the slug root — every
stage reads it (#2244/#2528). Never a staged `{dev,rc,main}/GOOGLE_*`, `GOOGLE_OAUTH_CLIENT_*`, or `prod/`
variant; the runtime reads only the canonical names and declare-first rejects the rest.

Local `/stage` is optional product-owned configuration. A repo that wants `/stage` may carry a local
`stage` block, but that file must not contain board, deploy, or secret registry facts.

**Stage port block (required for central-container):** run `mmi-cli stage port-range <Repo>` to assign
(idempotently) the repo's local port block from the central registry. `tenant-container` and
`solo-container` **must** have Hub registry `portRange` — `mmi-cli devops bootstrap verify` fails without
it (#5539), and `mmi-cli stage` cannot derive a local preview until the block exists. `bootstrap apply
--execute` assigns the block automatically after DDB register for those models; for older META holes,
run `stage port-range` explicitly. Use `$STAGE_PORT` in `stage.up` / `healthUrl`; `/stage` then picks a
free port inside the block so a dev can run several projects/versions locally without collisions.

## Step 7 — seed the org-managed .gitignore block

The org-managed `.gitignore` block is delivered by the `managed-block` bootstrap seed (`skills/bootstrap/seeds/manifest.json`),
which merges the canonical block into the repo's `.gitignore` in place, preserving the repo's own ignore lines.
The independent CLI/user-settings resume path may run doctor thereafter. The block carries **only** org-universal ignores —
never agent guides or a spine. The fanout pipeline that used to push this block via App-token PRs is retired
(Hub#3010), and the whole-spine fanout was retired earlier (hub-v3 WS4.2 #2219).

## Step 8 — report

Repo, default branch, ruleset applied, train branches locked (push allowlist), repository Environments
provisioned per track (plan-gated protections named in apply output), project attached/created
(+ info seeded, Status lanes and Labels field verified), secrets set (names only), developer access, plugin
installed, docs seeded, registry META written, issue templates committed, org App credentials registered,
org-managed `.gitignore` block seeded, the final `mmi-cli devops bootstrap verify "$OWNER/$REPO" --class ... --json` result,
and — for a deployable repo — the train door itself, run from the repo's own checkout on `development`:

```bash
mmi-cli devops train doctor --lane release --repo "$OWNER/$REPO" --json
```

`ready: true` is the bootstrap's train-readiness proof; every finding carries `code`, `severity`, `source`,
`remedy` and a `docs/Guides/train-troubleshooting.md#<code>` anchor. A full-track repo runs
`--lane rcand` as well. A `bootstrap-gap` finding names the apply row still missing — a blocker for a
train branch absent on origin, a warning for a stage with no required status check (#6136): read the
warnings too, `ready: true` alone does not prove the product ruleset is active.

## Retro — one check before you finish
Before your final report, answer one question honestly: did **this skill's own instructions** misfire
this run — ambiguous wording, a misleading message, or an environment failure it should have warned
about? (Process only — never the user's code or task; e.g. an ambiguous seed, registry, or OIDC step, or
a guard that fired on a healthy repo.) If yes, file **one** lesson and move on; a clean run is silent
(hard cap: one per run). It lands on the owning board (deduped) and is fixed only via a reviewed PR — never
edit the skill live; the retro is advisory, so if the call fails, note it and continue. File through MMI:
`mmi-cli learning skill-lesson --skill bootstrap --title "<what misfired>" --body "<what; evidence; proposed amendment>"`
