# Primitives catalog + replication checklist

Audience: AI coding agents first. Files in `../templates/` are VENDORED from the platform repo at merged main 2026-07-06 (PR#67→#81, all gate-proven), plus the three offload primitives added 2026-07-26 (`register-runner.sh`, `check-offload-workflow.mjs`, `workflows/pr-gate-dedicated.yml`), gate-proven on debian1/debian2. Templates here = canonical generic copies; any project's copy is an instance and may diverge freely. When you materially improve an instance, back-port the generic part here.

## File catalog (templates/)

| File | Role | Adaptation points |
|---|---|---|
| `cpu-limit.sh` | THE wrapper — systemd scope/slice, nice, ionice, SCHED_IDLE, taskset, GOMAXPROCS, VITEST_MAX_FORKS/THREADS, node heap, machine-wide flock, turbo cache-dir preference | cache dir name; slice name; `pnpm bin` → your PM; drop turbo-specific `--concurrency` injection if no turbo |
| `safe-merge.mjs` | fail-closed merge wrapper — refuses unless named gate check SUCCESS; authoritative on PR STATE not gh exit code; deletes leftover remote branch | workflow name "PR Gate" / job name "Full gate" |
| `ci-artifact-gate.test.mjs` | executable workflow contract — asserts gate topology + safe-merge behavior so drift fails the gate | rewrite asserts to your workflows; keep the PATTERN (topology as test) |
| `lefthook.yml` | pre-push ≤10s handoff only; NEVER typecheck/build/test/e2e | hook manager (lefthook/husky); controller handoff command |
| `workflows/pr-gate.yml` | pre-merge full gate on `pull_request`; env: `BUILD_CPU_RESERVE`, `TURBO_CACHE_DIR`, **`BUILD_SCHED_IDLE: "0"`** (MUST keep — CI under SCHED_IDLE starves) | runner labels; gate command; drop `TURBO_CACHE_DIR` if runner ≠ dev box |
| `workflows/main-gate.yml` | post-merge run — delivery trigger ONLY, no auto-revert | same |
| `workflows/delivery.yml` | publish+deploy gated on Main Gate success, pinned to `workflow_run.head_sha` | registry, deploy target, secrets |
| `vitest-config/` | shared factory — `realPg` preset: singleFork + hookTimeout 300s / testTimeout 180s (contention budget, factory-OWNED). Per-package overrides silently CAP factory via mergeConfig — DO NOT add them | preset names; drop realPg if no DB suites |
| `pg-template/` | `initdbCached()` — initdb template cache, ~3x faster DB boot. Key = version + binary size/mtime + user + args + LC_*/LANG/TZ; atomic-rename race; refuses non-empty dataDir; fail-open to real initdb. Tests prove each invariant RED | none (host-agnostic Node) |
| `hooks/deny-raw-pr-merge.sh` | PreToolUse hook blocking raw `gh pr merge` → delivery controller | project settings.json registration |
| `register-runner.sh` | registers ONE repo-scoped runner on a remote box, user-scope systemd, no sudo. Emits one JSON line; exit 0 ONLY when runner confirmed **online** (30×2s API poll) | `--repo --host --name --label`; nothing else |
| `check-offload-workflow.mjs` | executable contract for offload workflows — asserts fork-PR guard on every self-hosted `pull_request` job + no UNTRUSTED event field interpolated into `run:`. Dependency-free | vendor INTO the repo as `tools/check-offload-workflow.mjs`; pass workflow paths as argv |
| `workflows/pr-gate-dedicated.yml` | pre-merge gate for a runner on a DEDICATED CI box | runner label, node version, install + gate steps. NEVER add `TURBO_CACHE_DIR`/`BUILD_CPU_RESERVE` — no second consumer, no interactive work to reserve for |

## One-time per MACHINE (not per project)

```bash
# shared cache dir (dev user + runner user)
sudo mkdir -p /var/cache/<project>-turbo && sudo chown <dev>:<dev> /var/cache/<project>-turbo
sudo setfacl -R -m u:<runner-user>:rwX -m d:u:<runner-user>:rwX -m d:u:<dev>:rwX /var/cache/<project>-turbo
# aggregate CPU cap — ~/.config/systemd/user/build.slice
#   [Slice]
#   CPUQuota=800%
systemctl --user daemon-reload
```

Seed cache from an existing checkout: `cp -an <repo>/.turbo/cache/. /var/cache/<project>-turbo/` (NOT `cp -n` with glob — copies nothing silently).

## Offloading a project to a dedicated CI box (checklist, order matters)

Proven live 2026-07-26 across invariantum, overdeck, vibeflare, trance-shop-israel, ForumZone on debian1/debian2.

1. **Register the runner FIRST** — `templates/register-runner.sh --repo owner/name --host debian2 --name <project>-debian2 --label <project>-gate`. Wait for its `online` JSON. A label with no online runner does NOT error, it queues the job forever.
2. **One runner per REPO.** `alexcodeplace` is a GitHub **User** account, not an org (`/orgs/alexcodeplace` → 404), so org-level runners are impossible without transferring repos. Per-repo registration needs no user involvement: existing `gh` scopes mint `POST /repos/<repo>/actions/runners/registration-token`. A repo-scoped runner only ever receives its own repo's jobs, so a shared `<project>-gate` label pattern is safe.
3. **Copy `workflows/pr-gate-dedicated.yml`**; set label + node + gate steps.
4. **Vendor `check-offload-workflow.mjs` INTO the repo** as `tools/check-offload-workflow.mjs`. MUST be in-repo: the local handoff gate runs the test command through `local-gate`, which sandboxes the filesystem to the worktree — a checker outside the repo is invisible and the gate dies `MODULE_NOT_FOUND`.
5. **Place the contract step AFTER `setup-node`.** A dedicated CI box carries NO system node; "fail fastest" placement right after checkout exits 127.
6. **Set `dest: ${{ runner.temp }}/setup-pnpm`** on `pnpm/action-setup`. Default is a fixed `~/setup-pnpm`; two repo-scoped runners on one box race there and leave a truncated binary (exit 127) or EACCES.
7. **Install extra toolchains ONCE PER BOX, never per job** (bun, etc.) — per-job installs violate the prime directive. Add a `run: echo "$HOME/.bun/bin" >> "$GITHUB_PATH"` step before Install.
8. **Verify green on the box.** Warm dedicated-box gate ≈ 1m20s (invariantum, overdeck).

### Gotchas that cost a red run each

- `runsvc.sh` ships under `bin/` ONLY; `svc.sh install` (root) is what copies it to the runner root. A user-scope unit MUST copy it itself or the service dies `203/EXEC` on every restart. `register-runner.sh` already does this.
- Non-interactive ssh to debian1/2 has a **broken PATH** (pre-existing profile fault): bare `curl`/`tar`/`systemctl`/`node` exit 127. Remote steps MUST `export PATH` or use absolute paths. Plain `ssh host "cmd"` works; `bash --noprofile --norc -c "..."` nesting mangles dot-file reads.
- `loginctl enable-linger` MUST be set or the runner dies with the ssh session.
- `--with-deps` on `playwright install` needs sudo — omit it on a user-scope runner; these boxes already carry the system libraries.
- Offload EXPOSES pre-existing repo defects that never ran anywhere (gitignored test suites, stale migration baselines, `vitest run` exiting 1 with zero specs, absolute `file:` deps on a sibling checkout). Report them as findings; do NOT assume the offload caused them. Check whether the same step is already red on `main` first.

## New-project checklist (order matters)

1. Machine setup above (skip cache-sharing + SCHED_IDLE entirely if runner is a dedicated box).
2. Copy `cpu-limit.sh`; wire every heavy script through it. Test BOTH lock branches + `BUILD_SCHED_IDLE=0` + `BUILD_NO_CAP`.
3. Copy workflows; set runner labels + gate command; `BUILD_SCHED_IDLE: "0"` stays.
4. Copy `safe-merge.mjs` + deny hook + lefthook guard; rename check/job strings consistently in all three + workflow.
5. Write the workflow-contract test (adapt `ci-artifact-gate.test.mjs`); wire into the smoke.
6. DB suites: vitest factory (singleFork + factory timeouts), socket-only harness, then `pg-template` for initdb.
7. Prove: cold gate run, warm replay run (expect ~10x), post-merge main-gate replay. Record numbers.
