# Rollout Phase A

> Phase A of a qvac-fabric rollout — set up overlay port and validate all 7 consumers against the fabric branch before publishing to the registry. Pass --on-top-of-pr to apply the overlay onto an open PR's head branch instead of a dedicated validation branch.

- Skill: `tetherto/rollout-phase-a` (Agent Skill)
- Install (CLI): `npx skillmds@latest add tetherto/rollout-phase-a`
- Raw SKILL.md: https://api.skillmd.com/api/skills/tetherto/rollout-phase-a/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: tetherto (https://skillmd.com/u/tetherto)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/tetherto/rollout-phase-a

---


# Rollout Phase A — Overlay Validation

**Prerequisites:** Fabric PR open in `tetherto/qvac-fabric-llm.cpp`. Tag does NOT exist yet.

**The 7 consumers:** `embed-llamacpp`, `fabric`, `llm-llamacpp`, `model-fit`, `ocr-ggml`,
`translation-nmtcpp`, `vla-ggml`.

`classification-ggml` is **not** a fabric consumer — it dropped the `qvac-fabric` vcpkg dependency
and now consumes the published npm package `@qvac/fabric`. It needs neither overlay validation nor
a `version>=` bump during a rollout. Do not re-add it.

Re-derive the roster if in doubt — it is one command, and it changes:
```bash
git -C <repo> grep -l "qvac-fabric" origin/main -- "packages/*/vcpkg.json"
```

## THE GOLDEN RULE
Never bump `default-registry.baseline` in `vcpkg-configuration.json`. Not now, not ever during a rollout.

Why nothing breaks without it: vcpkg resolves `version>=` against the registry's **published versions
(HEAD)**, not the baseline commit — the baseline is only a *floor* for ports with no explicit
constraint. Baselines are advanced only by separate, unrelated infra "Sync with fabric" maintenance
PRs; bumping the baseline in a rollout PR is the #1 review rejection.

## Two modes

| Invocation | Where the overlay lands |
|---|---|
| no `--on-top-of-pr` | **Default.** A dedicated validation branch, as below. |
| `--on-top-of-pr <pr-url>` | On top of that open PR's head branch, as one revertable commit. |

The default exists to validate fabric in isolation. `--on-top-of-pr` exists for the common case where
the fabric change was made *because of* an open feature PR: that PR already carries the consumer code
needing the new fabric, so validating on a separate branch leaves the PR's own CI building against
the published (old) fabric, and the two only meet after the whole rollout lands.

Every difference the mode makes is called out inline below under **`--on-top-of-pr`**; everything
else is shared. The overlay is scaffolding either way — `/rollout-phase-b` removes it, and in this
mode it does so by reverting the single commit Step 5 creates.

## Steps

### Step 0 — Preflight, then ask

**Do this before anything else.** Nothing below runs until the user approves.

| Case | Behaviour |
|---|---|
| `<fabric-version>` / `<fabric-branch-or-commit>` given on the invocation | Use them **verbatim**. |
| Not given | **Derive from context** (ladder below), recording the source of each value. |
| Neither | Name what is missing and stop. |

**What was supplied is what gets used.** Never substitute the registry's latest published version or
the newest fabric tag for a value the user gave you, and never "correct" a supplied value to one of
them. The version is passed **bare** — `10297.0.0`, not `v10297.0.0`; the `v` is added at tag time in
Phase B. A `v`-prefixed value is malformed: report the expected form and ask.

**Derivation ladder** — first hit wins, and remember which one it was:

1. **This session**, or the open fabric PR under validation — whose head ref is also
   `<fabric-branch-or-commit>`.
2. **The newest `temp-<N>` release branch with no corresponding `v<N>.*` tag.** Fabric releases are
   cut from branches named `temp-<upstream-llama.cpp-build>`, and the version is
   `<build>.<major>.<minor>` — `temp-10069` carries `v10069.0.0`, `temp-9341` carries `v9341.1.6`. An
   untagged `temp-<N>` is therefore the next rollout, and `<N>` gives the version's leading component.
   The trailing `.<major>.<minor>` is still a human decision — propose `<N>.0.0` and ask.

**Never derive the version from the registry's latest entry, the newest `v*` tag, or the consumers'
current `version>=` floors.** All three hold the *previous* version.

**Resolve against the remote** — read-only; the fabric repo is public, so no token:

| Fact | Command |
|---|---|
| ref → commit sha | `git ls-remote https://github.com/tetherto/qvac-fabric-llm.cpp <ref>` |
| `v<VERSION>` must NOT exist yet | `git ls-remote --tags https://github.com/tetherto/qvac-fabric-llm.cpp "v<VERSION>"` |
| commit subject + date | `gh api repos/tetherto/qvac-fabric-llm.cpp/commits/<sha> --jq '{sha:.sha,msg:.commit.message,date:.commit.committer.date}'` |

Do **not** compare the ref against `master` — it tracks upstream, not the release line, and reports
`diverged` for every legitimate rollout.

#### `--on-top-of-pr` — resolve and vet the target PR

Only in this mode, and before anything else in Step 0 is presented:

```bash
gh pr view <n> --repo tetherto/qvac \
  --json number,title,state,isDraft,headRefName,headRefOid,headRepositoryOwner,baseRefName,labels,files
```

**Hard stops.** Report the reason, not a bare failure — each of these means the run cannot do what it
claims to:

| Condition | Why it stops the run |
|---|---|
| the URL is not a `tetherto/qvac` PR | a **fabric-repo** PR URL is the likely slip — that is the change being validated, not the target |
| PR is not `OPEN` | nothing to validate |
| head repo ≠ `tetherto/qvac` (a fork) | the overlay commit cannot be pushed to a fork's head branch |
| `baseRefName` ∉ `main` / `release-*` / `feature-*` / `tmp-*` | every `on-pr-<consumer>.yml` carries that `branches:` filter, so **no** consumer workflow fires at all |
| PR already carries `overlay-ports` or `vcpkg-overlays/ports/qvac-fabric/` | already overlaid — re-applying doubles it |
| local worktree dirty, or local branch ≠ `headRefOid` | the commit would sweep in unrelated work |

**Then the stage-eligibility report — this is the check that makes the mode honest.** CI stages are
routed by event, and `pull_request_target` is *not* a trusted event:
`.github/actions/ci-router/action.yml` enables every stage for `workflow_dispatch` / `workflow_call` /
`push` / `schedule`, but for a PR event it enables only `run_verified_checks` plus whatever the
granular labels select — and a **draft PR routes nothing at all**.

That matters because `run_verified_checks` gates only `sanity-checks` and `cpp-lint`. The `prebuild`
job — the one that actually compiles the addon against fabric — is gated on `run_prebuilds`.

Compute the report from the PR's own labels and draft state; never assume it:

```
  target PR       #3352  "QVAC-21981 feat[api]: ABot-World …"
                  head QVAC-21981/abot-world @ a2d3265d  ->  base main
                  labels: (none)   draft: no

  sanity-checks   eligible
  cpp-lint        eligible
  prebuild        NOT eligible   <- needs `prebuilds`
  cpp tests       NOT eligible   <- needs `run-cpp-addon-tests`
  desktop tests   NOT eligible   <- needs `run-desktop-addon-tests`
  mobile tests    NOT eligible   <- needs `run-mobile-addon-tests`

  ⚠ Without at least `prebuilds`, the overlay is never compiled — the PR
    goes green having built nothing against the new fabric.
```

A draft PR is its own `⚠`: it routes nothing, so the overlay sits inert until the PR is marked ready.
This is the same failure the misplaced-overlay warning in Step 3 describes — a green result that
validated the old version — reached by a different route, so treat it with the same seriousness.

**Present, then ask.** Label every value `(as given)` or `(derived: <source>)` — that label is what
lets the user catch a bad derivation, so never omit it. Render an anomaly (unknown ref, `v<VERSION>`
already present) as a `⚠` row naming its consequence:

```
Phase A — confirm before proceeding:

  fabric version   10297.0.0    (derived: temp-10297 is untagged)
  validating ref   temp-10297   (derived: newest untagged release branch)
  resolves to      6a32c29a7    "<subject>"   <date>
  tag v10297.0.0   does not exist yet ✓  (Phase B creates it)
  target          validation branch  (default mode)
```

In `--on-top-of-pr` mode the last row names the PR instead, and the stage-eligibility report above is
printed immediately below the block:

```
  target          PR #3352 on tetherto/qvac   (as given)
                  head QVAC-21981/abot-world @ a2d3265d  ->  base main
```

Then `AskUserQuestion` with exactly:

**Proceed** · **Correct a value** (say which) · **Abort**

The run does not start until Proceed is chosen — silence is not consent, and approval is required
even when every value resolves cleanly. When a `⚠` is present, Proceed's description must name what
is being accepted — an unlabelled PR means accepting that nothing will be compiled. If nothing yields
a version, say which value is missing and stop; never guess one.

**`--on-top-of-pr`:** the working branch for Steps 1–4 is the PR's head branch — `gh pr checkout <n>
--repo tetherto/qvac`. **Do not rebase it.** Rewriting a contributor's history is not this skill's
business, and the overlay works from wherever the PR's head happens to sit. Everything in Steps 1–3
is otherwise identical, all 7 consumers included.

### Step 1: Compute SHA512 of the fabric tarball
The fabric repo is public — no token needed. Use the `/archive/` URL (what vcpkg fetches). Do NOT use
`gh api …/tarball/` — its differently-named top-level directory yields a different hash.
```bash
curl -fsSL https://github.com/tetherto/qvac-fabric-llm.cpp/archive/<fabric-branch-or-commit>.tar.gz -o /tmp/fabric.tgz
vcpkg hash /tmp/fabric.tgz
```
Or let vcpkg print the expected hash on first failed fetch (intentionally wrong hash triggers it).

### Step 2: Add shared overlay port
Copy ALL files from the registry port (`tetherto/qvac-registry-vcpkg/ports/qvac-fabric/`) into `vcpkg-overlays/ports/qvac-fabric/` (repo root — `vcpkg-overlays/` already exists, holding `triplets/` and `toolchains/`). Then update `portfile.cmake` to point at the fabric branch/commit (NOT a tag — it doesn't exist yet):
```cmake
vcpkg_from_github(
  OUT_SOURCE_PATH SOURCE_PATH
  REPO tetherto/qvac-fabric-llm.cpp
  REF <fabric-branch-or-commit>
  SHA512 <sha512>
)
```
Keep the call to those four arguments. The registry port carries **no `HEAD_REF`**, and fabric's
default branch is `master` — so a copied-in `HEAD_REF main` is both an addition to the port you were
told to copy verbatim and a wrong value.

The registry port's `REF` is parameterized as `REF v${VERSION}`; the overlay replaces it with the
literal branch/commit precisely because no tag exists yet. Phase B restores the parameterized form
by leaving the registry portfile's `REF` untouched.

Update `vcpkg-overlays/ports/qvac-fabric/vcpkg.json` with `"version": "<VERSION>"`. (Overlays bypass
version resolution entirely, so this version only needs to satisfy the consumers' existing
`version>=` pin — the new `<VERSION>` is fine.)
Copy any other files from the registry port too (e.g. `android-vulkan-version.cmake` if present).

### Step 3: Add overlay-ports to all 7 consumers
In each `packages/<consumer>/vcpkg-configuration.json`, add:
```json
"overlay-ports": ["../../vcpkg-overlays/ports"]
```
The path is relative to the `vcpkg-configuration.json` that declares it — hence two levels up out
of `packages/<consumer>/` and back down into `vcpkg-overlays/ports`. Same string for all 7.

If this path is wrong, **nothing errors**: vcpkg finds no overlay, silently resolves `qvac-fabric`
from the registry, and you validate the OLD version while believing you tested the new one. Confirm
the overlay is live by checking the install log for the overlay's `<VERSION>`.

Do NOT change `default-registry.baseline`.

**Do NOT bump consumer `vcpkg.json` `"version>="` in Phase A** — that happens in Phase B. The overlay bypasses version resolution entirely; only the overlay port's own `vcpkg.json` version needs to match.

### Step 4: Confirm changes with user before committing

`git diff --stat` must show **exactly 9 files** — the 7 `packages/<consumer>/vcpkg-configuration.json`
plus `vcpkg-overlays/ports/qvac-fabric/{portfile.cmake,vcpkg.json}`. Anything else means something
got swept in; in `--on-top-of-pr` mode that something is the PR author's own work.

### Step 5: Commit and push to upstream branch (NOT fork)
```bash
git push origin <branch>
```
`origin` = `tetherto/qvac` (cloned directly, not a fork). Push straight to origin.

#### `--on-top-of-pr` — one commit, and only the overlay in it

Phase B removes the overlay by **reverting this commit**, so its contents decide whether that revert
is safe. Two halves, both required:

- **The commit holds those 9 files and nothing else.** Consumer C++ fixes from Step 7 are real work
  that stays on the PR after the rollout — they go in *separate* commits. Fold a fix into the overlay
  commit and Phase B's revert silently removes the fix along with the overlay.
- **The message has to be findable and self-explanatory**, because the next person to read it is
  whoever is triaging the PR:

  ```
  <TICKET> chore[notask]: overlay-validate the 7 fabric consumers against <fabric-ref>

  Rollout Phase A for the <feature> fabric change. Adds the shared qvac-fabric overlay
  port and points all 7 consumers at it, so they build against the fabric branch head
  before the tag exists and before anything is published to the registry.

  Consumer version>= pins are untouched — the overlay bypasses version resolution, so
  only the overlay port's own version matters here; bumping the pins is Phase B.
  default-registry.baseline is untouched in all 7.

  TEMPORARY. This commit is reverted by /rollout-phase-b --on-top-of-pr before the
  fabric dependency bump. The PR itself is meant to merge; this commit is not.

  Rollout-Overlay: qvac-fabric <VERSION> ref=<fabric-branch-or-commit> sha=<fabric-sha>
  ```

  The trailing `Rollout-Overlay:` line is functional metadata — it is what Phase B greps for. Keep it
  last and keep the prefix exact.

**Do not copy `DO NOT MERGE` from a validation-branch commit.** On a dedicated branch that marked the
whole branch as throwaway. Here the PR *is* meant to merge; it is the overlay **commit** that must go
first, and the wording above says so precisely.

Push to the PR's head branch, then **print the resulting commit SHA** — it is Phase B's input.

### Step 6: Trigger validation CI per consumer (`workflow_dispatch`)
```bash
for w in embed-llamacpp fabric llm-llamacpp model-fit ocr-ggml translation-nmtcpp vla; do
  gh workflow run on-pr-$w.yml --repo tetherto/qvac --ref <validation-branch>
done
```
- The workflow file for the `vla-ggml` consumer is `on-pr-vla.yml` (NOT `on-pr-vla-ggml.yml`).
  `on-pr-fabric.yml` and `on-pr-model-fit.yml` follow the standard naming — `vla` is the only
  exception.
- **Triage `fabric` first.** It is the shared runtime addon the other consumers link against, so a
  break there usually explains failures in the rest; fixing it first avoids chasing the same root
  cause across six other runs.
- Do NOT add stage labels to a validation PR — with `prebuilds` or any `run-*` label on it, every
  push re-runs ALL consumers (`pull_request_target` fires on each synchronize). `workflow_dispatch`
  is a trusted event in `ci-router` (no label needed, every stage enabled) and lets you re-run only
  the consumer(s) you actually changed. `verified` is **not** one of the labels to avoid adding or
  to add — `ci-router` reads only `prebuilds`, `run-cpp-addon-tests`, `run-desktop-addon-tests`
  and `run-mobile-addon-tests`; `verified` selects nothing.
- **A draft validation PR routes nothing at all.** `ci-router` sets `IS_AUTHORIZED=false` when
  `draft` is true, so *zero* stages run however many labels are on it — labels on a draft buy no
  coverage. Mark it ready, or drive it by dispatch.
- First pass: all seven. Later iterations: re-dispatch just the one(s) you touched.
- **A re-pin counts as touching all seven.** Moving the overlay's `REF`/`SHA512` changes the fabric
  every consumer builds against, so re-dispatching only `fabric` after one leaves the other six
  showing a **stale green from the previous fabric ref**, with nothing to say so. Nothing re-runs on
  this branch unless you dispatch it — there is no PR here, so no path filter is doing it for you.

**Expected failures / flakes while monitoring:**
- `merge-guard` / `validate-pr` always fail on a no-PR dispatch branch — expected, ignore. (Not so in
  `--on-top-of-pr` mode: a real PR has a real title, so these pass and a failure is genuine.)
- Known-flaky macOS and mobile device-farm jobs are typically non-blocking and orthogonal to a fabric
  change — confirm they're pre-existing / baseline-reproducible, retry the failed job (not the whole
  run), and don't chase them as rollout blockers.

#### `--on-top-of-pr` — do NOT dispatch; the push already triggered CI

The Step 5 push auto-triggers all 7 consumer workflows: the overlay commit edits every
`packages/<consumer>/vcpkg-configuration.json`, which each `on-pr-<consumer>.yml` path-matches via
`packages/<consumer>/**`. Monitor those runs. Dispatching as well would duplicate every run on the
same ref.

**Ask before adding labels.** Step 0's eligibility report says which stages the PR is actually
eligible for, and without `prebuilds` none of them compile anything. Adding labels changes the CI
cost of someone else's PR, so present the report, ask, and add only what was approved. `prebuilds`
alone gets the addon built against fabric; `run-cpp-addon-tests` / `run-desktop-addon-tests` /
`run-mobile-addon-tests` add the test tiers (each of the latter two implies `prebuilds`).

**Re-pinning the overlay re-triggers all 7 — `paths:` matches the whole PR, not the push.** When you
iterate by moving the overlay's `REF`/`SHA512`, a commit touching only
`vcpkg-overlays/ports/qvac-fabric/portfile.cmake` still fires every consumer workflow. On a
`pull_request_target` event GitHub evaluates `paths:` against the PR's **cumulative** changed-file set
(`base...head`), not the delta of the push that triggered it — and Step 5's overlay commit already put
all 7 `packages/<consumer>/vcpkg-configuration.json` into that set, so each `on-pr-<consumer>.yml`
keeps matching on `packages/<consumer>/**` for the rest of the PR's life. Do **not** touch the 7
configs to force a rebuild, and do not dispatch the six: both add noise to someone else's PR for an
effect you already have.

The same rule is why adding a label re-runs all 7 — a `labeled` event carries no file delta at all,
yet the cumulative path match still holds. Verified on qvac#3725: commit `40a157675` changed two lines
of the portfile and fired Fabric, Embed, LLM, Model Fit, NMTCPP, OCR-GGML and VLA.

This is the one place where the two modes genuinely diverge, so do not carry the default flow's
instinct across: on a dispatch-driven validation branch there is no PR and no cumulative diff, so
nothing re-runs unless you dispatch it — see the re-pin warning in Step 6 above.

### Step 7: Fix consumer breaks
Apply C++ fixes (API drift, new failure modes) using the Edit tool — specific lines only. Never `git checkout <branch> -- file.cpp` (takes the whole file, causes regressions).

Watch for:
- **API drift** — a fabric bump pulls a newer upstream llama.cpp/ggml whose API may have moved
  (renamed/relocated struct fields, changed signatures). Fix in the consumer addon.
- **Runtime behaviour shifts** (perf, threading, init paths) — if an integration test now hits a
  timeout, calibrate the timeout with a comment explaining why; NEVER skip/disable the test. Guard
  new failure modes (e.g. an init call returning null) with a catchable error, not a segfault.

**`--on-top-of-pr`:** these fixes are real work that stays on the PR — commit them **separately** from
the Step 5 overlay commit, never as an amend to it. That commit has to stay a pure overlay so Phase B
can revert it without taking a fix with it.

### Step 8: Update the fabric PR description with the validation runs
Once all consumers are green, add the validation CI runs to the fabric PR description — one bullet
per consumer, all 7 listed, noting the head commit they validated:
```
- <consumer name>: <CI run link>
```
In `--on-top-of-pr` mode these are the target PR's own runs; name the PR and the head commit they
validated, so a later reader can tell which fabric ref the green belongs to.

### Step 9: Post Slack message when all consumers green
```
Hi Team, Please review the PR for <feature name>
Fabric PR: https://github.com/tetherto/qvac-fabric-llm.cpp/pull/<N>
Validation branch: <validation-branch> (consumer CI runs listed in the fabric PR description)
```
If a qvac validation PR was opened, add its link (`Add on PR: https://github.com/tetherto/qvac/pull/<N>`).

In `--on-top-of-pr` mode, replace the validation-branch line with the target PR and say the overlay is
temporary, so nobody merges it by mistake:
```
Validated on: https://github.com/tetherto/qvac/pull/<N> (carries a TEMPORARY qvac-fabric
overlay commit — /rollout-phase-b reverts it before the dependency bump)
```

If you have no Slack access, print the message (with the links) to the console so the user can post it.
Wait for lead to merge fabric PR before proceeding to `/rollout-phase-b`.

