Skip to content
Back to skills

Pulp Vellum Change Routing

ASecurity

Route repository-qualified changes across Pulp and Vellum using Pulp's exact ownership projection. Use when a change touches design import, Chromium authoring, DesignIR, visual harness, screenshot, Skia/Dawn, runtime assets, or another path represented by `.github/vellum-ownership.json`; when deciding whether work originates in Pulp or Vellum; or when validating the cross-repository routing contract.

  • 22 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added September 3, 2026
toolspythonrustgobashgitapifrontenddocumentation

Works with

  • terminal
  • cli
  • api
  • mcp

Security analysis

A100/100

Pro scans all 5 files and shows the line behind each finding

Scanned September 24, 2026

npx -y skills add danielraffel/pulp --skill pulp-vellum-change-routing --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Pulp Vellum Change Routing?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Pulp Vellum Change Routing
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/danielraffel-pulp-vellum-change-routing/badge)](https://www.skillsdirectory.com/skills/danielraffel-pulp-vellum-change-routing)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: pulp-vellum-change-routing
description: Route repository-qualified changes across Pulp and Vellum using Pulp's exact ownership projection. Use when a change touches design import, Chromium authoring, DesignIR, visual harness, screenshot, Skia/Dawn, runtime assets, or another path represented by `.github/vellum-ownership.json`; when deciding whether work originates in Pulp or Vellum; or when validating the cross-repository routing contract.
---

# Route Pulp and Vellum changes

Run from the Pulp repository root. Treat the projection as authority; do not
infer ownership from similar directory names or broad path prefixes.

## Route changed paths

Pass the repository where each path currently lives:

```bash
python3 .agents/skills/pulp-vellum-change-routing/scripts/route_change.py \
  --repository Generous-Corp/pulp \
  --json \
  tools/import-design/browser_capture/capture.mjs
```

For a coordinated change spanning repositories, combine the default repository
with repeated `--change REPOSITORY:PATH` arguments:

```bash
python3 .agents/skills/pulp-vellum-change-routing/scripts/route_change.py \
  --repository Generous-Corp/pulp \
  --change Generous-Corp/pulp:tools/scripts/package_cli.py \
  --change Generous-Corp/vellum:cli/vellum \
  --json
```

The temporary private delivery repository `danielraffel/vellum` resolves to the
permanent `Generous-Corp/vellum` owner. An exact route wins. Without an exact
route, an initial-cut path already transferred to Vellum remains Vellum-owned;
other unlisted Pulp paths remain Pulp-owned, and unlisted Vellum paths remain
Vellum-owned. A result with both owners is `coordinated`, not permission to copy
either repository's product-specific code into the other.

The exact routes are independently frozen in
`references/approved-exact-routes.v1.json`, bound to the reviewed matrix,
amendment, and route-set digests. A projection cannot substitute a newly
self-hashed route set.

## Fail closed

Stop when the command reports a malformed projection, unsafe path, duplicate
route, repository-incompatible role, or claimed-owner conflict. Do not replace
an absent exact expansion with a prefix or glob guess. The accepted projection
authorizes maintenance routing only; it does not authorize Pulp consumption,
downstream cutover, or implementation before the corresponding Vellum
acknowledgement.

## A test-manifest line can cross a capability family

`vellum_expansion_watch_check.py` matches changed paths against per-family
selectors, and `visual-proof-harness` owns `test/cmake/view_widget_bridge_tests.cmake`
outright. Registering **any** new test target in that manifest crosses the family
— including a control-plane or audio test with no screenshot, golden, capture or
render surface anywhere in it — so the change needs its own append-only event
under `.github/vellum-expansion-watch-events/`.

The failure reads as a false positive and is not one:

```
watch event family coverage differs; affected=['visual-proof-harness'] covered=[]
```

Two things make it hard to place. The checker reports the family, never the path
that selected it, so grep the selector lists in `vellum_expansion_watch_check.py`
for each changed path rather than guessing from the family's name. And an event
file is only counted once it is **committed** — the checker reads the diff, not
the working tree, so writing the JSON and re-running reports the identical
failure and reads as a rejected event.

The event's `capability_families` must equal the affected set exactly, and its
`rationale` should name the single line that crossed the selector, so a reader
can tell a manifest registration apart from a real harness change.

## GPU doctor boundary

Pulp owns the `pulp doctor gpu` CLI/MCP adapters, typed GPU-health evidence,
fixtures, tests, documentation, and skills. Those adapters may consume the
existing `Renderer3D`, `GpuCompute`, and `HeadlessSurface` interfaces.
`HeadlessSurface` itself is in the Vellum-authoritative transferred rendering
slice: do not change its header, implementation, or generic contract as part of
a Pulp GPU-doctor change. If the diagnostic cannot be implemented through the
existing interface, stop and route the required framework change to
`Generous-Corp/vellum`; do not add a Pulp-side duplicate or compatibility API.

The same ownership split applies to first-visible-frame GPU health. Pulp owns
the product measurement budget, closed result schema, capability-control
operation and executor adapter, CLI/MCP projection, Forge/DAW acceptance, and
Perfetto evidence interpretation. Generic frame-lifecycle spans, render-stage
identity, shader/pipeline cache instrumentation, or prewarm implementation are
Vellum work when their projected slice is framework-authoritative-transferred.
Keep nullable Vellum/source/shader and GPU/trace correlation seams in Pulp until
those producers exist; do not manufacture identities, promote incomplete event
captures, or advertise a live host capability before the exact product adapter
can return a validated snapshot.

For A3 receipts, distinguish capture integrity from instrumentation coverage.
Dropped or truncated available events invalidate every terminal disposition.
Named missing compile/upload/hidden/present/source/shader events may remain only
for a passing `no-change` or an over-budget `queue-B4-investigation`; the latter
must bind each missing event and argument to an exact path in the active
`framework-authoritative-transferred` `render-skia-dawn` slice. That route is a
request for post-adoption instrumentation and rerun, not authority to implement
the event or prewarm policy in Pulp.

A4 DPR orchestration, retained cell artifacts, deterministic analysis, and
protected-main publication receipts are Pulp-owned. `init-v2` must derive its
manifest from the fixed terminal A2T/A3 receipts; `finalize-v2` may consume only
nonce-bound runner snapshots, never a caller manifest/result/disposition. This
does not move generic DPR policy into Pulp: a freshly live-verified 84-plus-84
candidate only queues B5, which remains the separately reviewed Vellum adoption
boundary. The current zero-cell inconclusive result authorizes no route change.

The Pulp-owned live seam is `ControlGpuHealthProvider` plus
`ControlGpuHealthViewAdapter`: product hosts supply existing back-buffer and
`GpuSurface` callbacks, while exact generic present/source/shader/cache events
remain Vellum work. A capture-confirmed upper bound may populate a validated
snapshot but must keep the unratified startup verdict unverified. Route any
request for a true present hook or generic pipeline instrumentation to Vellum
instead of modifying generic window or render-lifecycle paths in Pulp.

The Pulp-owned A3 campaign runner may snapshot product-specific adapters,
validate 10+10 lifecycle/cache provenance, and name transferred instrumentation
gaps. That runner is not authority to add a missing generic producer in Pulp.
If the real adapter cannot source present, pipeline, upload, shader, or source
identity through an existing seam, leave the fields null, record the exact
`render-skia-dawn` gap, and route the post-adoption producer to Vellum. Never
make a warm label or a zero duration stand in for the missing boundary.

The checkout-owned `gpu_first_visible_a3_external_adapter.py` is within the
same Pulp evidence boundary: it may pin a role-specific executable, validate
its closed producer receipt, and run the independent blank/audio controls. It
does not move ownership of the producer's generic present, shader, pipeline,
upload, or cache events. When those facts are unavailable through existing
product seams, the producer must report the exact transferred gap rather than
adding a Pulp-side render hook.
The checked-in standalone, constrained-headless, REAPER, and Forge producers
are orchestration adapters inside that boundary. They may pin exact source,
product, host, and lifecycle-driver identities and reject missing endpoint
truth; their existence does not authorize a Pulp implementation of a missing
generic native-present or cache event.

Pulp also owns the A4 DPR runner, product-scenario adapters, evidence ingestion,
and the B5 dependency receipt. The runner may classify a measured candidate,
but B5 remains `waiting-trigger` until the adopted Vellum API refresh. Any
generic DPR policy, render-lifecycle instrumentation, or framework adapter
needed to act on that result originates in Vellum; do not implement it in this
Pulp evidence lane.

## Editing a pinned file stales its handoff row — and that is now fine

`docs/status/gpu-vellum-handoff.yaml` pins a `revision` and an `object_id` per
path. `revision` is the most recent commit touching that path; `object_id` is
`git rev-parse <revision>:<path>`. Change a pinned file and its row still names
the previous blob.

**Do not re-pin it in your PR.** `version_at_land._refresh_derived` regenerates
the ledger and its receipt inside the `chore: bump versions` commit the release
bot writes to `main`, which is the one place a re-pin costs nobody a rebase.
Doing it in a PR instead re-creates the cost the move removed: ~148 non-merge
commits in 25 days changed **only** those two generated files, and they collide
on github.com, where the local `pulp-gpu-ledger` merge driver cannot run.
`gpu_handoff_pin_freshness.py` — in `gates.sh` and the pre-push hook — fails an
identity-only re-pin for that reason.

Staleness is survivable while you wait for the bot: provenance is ancestral, so
the always-on tier accepts a stale-but-real pin, and currency at HEAD is opt-in
behind `PULP_GPU_HANDOFF_REQUIRE_CURRENT`, which nothing in `.github` sets.

Survivable means no gate goes red, not that the pins are right.
`gpu_handoff_provenance.py check` is the strong currency claim, and on `main` it
is **expected to be red between a merge that touched a pinned path and the next
`chore: bump versions` commit** — that window is the designed steady state now
that the bot owns the refresh. Nothing runs `check` for you (the
`gpu-handoff-provenance-selftest` ctest holds the currency assertion but skips
it without `PULP_GPU_HANDOFF_REQUIRE_CURRENT=1`), so a red `check` is a reading
of where `main` sits in that cycle, not a task. Do not hand-regenerate to clear
it; that is the re-pin the guard rejects.

**Two shapes still belong to the PR**, because no regenerator can decide them:

- **An editorial ledger change** — adding, removing or re-stating a pinned
  path, and equally an edit to `authorities`, `upstream`, `cutover_trigger`,
  `self_binding`, `stop_rules`, or an entry's `vellum_paths`,
  `terminal_evidence`, `input_receipts` or `accepted_dispositions`. The guard
  keys on the whole document minus the three derived identity fields, so any
  of these passes; only a move confined to those three fields is a re-pin.
- **A pinned path you deleted or renamed.** Unlike staleness this is *not*
  survivable: the ledger names a path that is not there and the required gate
  goes red. Drop or move the row in the same PR.

Either one owes the regeneration, in the same commit as the inventory edit:

```bash
python3 tools/scripts/gpu_handoff_provenance.py check                       # every stale row, and the repair command
python3 tools/scripts/gpu_handoff_provenance.py write --receipt             # regenerate, and refresh the receipt
```

Pass `--receipt`. The published receipt is asserted against the ledger's bytes,
so regenerating without it leaves a second gate red for the next reader.

When a stale identity *does* surface, it surfaces through
`gpu-recipe-catalog-selftest`, far from the file you edited:

```
gpu-recipe-catalog: INVALID: handoff entries[N].pulp_paths[M] has stale revision/blob/tree identity
```

It is easy to misread as someone else's flake: the test is named for the GPU
recipe catalog and the change that broke it may have nothing to do with GPU. Do
not expect it on one specific lane — it is an ordinary ctest, so it fails on
whichever full-suite lane finishes first, and it has been observed reddening
both the required `macos` gate and `Linux (x64) [github-hosted]`. A green macOS
gate is therefore not evidence about your pins. Run
`python3 tools/scripts/gpu_recipe_catalog.py` locally; it takes a second and
answers the question outright. It has no `--write` mode and is deliberately
validate-only — the generator above owns those identities.

`gpu_recipe_catalog.py` will not catch that omission. It validates the ledger's
own identities and reports `OK` while the receipt still names the previous
ledger bytes. `gpu_handoff_provenance.py check` **does** catch it now: it binds
the published receipt to the ledger's bytes (sha256 + canonical paths, no git,
no currency claim), prints a `RECEIPT …` line and exits 1 when they disagree,
and says `receipt: none at …; binding not checked` out loud when there is no
receipt to bind. It used to exit 0 on exactly the state this paragraph warns
about, while the selftest that would have caught it
(`test_published_receipt_binds_the_checked_in_ledger`) is opt-in behind
`PULP_GPU_HANDOFF_REQUIRE_CURRENT=1` — deliberately, because currency at HEAD
would go red on every unrelated PR — so a "36 tests, OK" run proved nothing
about the receipt. Read the RECEIPT line, or `--json` and the `receipt.state`
field (`bound` / `stale` / `absent`); do not infer binding from a green
selftest.

`check` names each stale row, its path, and the field-level correction, so a
drifted pin no longer has to be located by hand. `write` derives `revision`,
`object_id`, and `object_type` for every declared path from a single commit
(`--source-commit`, default `HEAD`, which must be an ancestor of `HEAD`), so a
regenerated ledger can never mix trees from different revisions. It refuses to
run against an unclean checkout, and it refuses to emit anything
`gpu_recipe_catalog.py` would reject, so the fail-closed validator stays the
authority on acceptance.

`check` reads the **working tree**, so an uncommitted edit to a pinned path
makes a correct ledger look broken. The pin still equals the blob at `HEAD` and
only the unstaged bytes differ, but the validator hashes what is on disk, so the
row is reported `stale`. `check` now says which paths are uncommitted before it
renders any verdict, and stops calling that state a contract violation
"regeneration cannot repair" — committing the path repairs it, so the old
wording was false as well as misleading. If `check` is red while
`git status` shows a pinned path modified, commit or restore it and re-run
before looking for a defect. `--json` carries the same fact as
`dirty_canonical_paths`.

Do not hand-edit these rows. Hand editing is what turned a one-row correction
into dozens of stale identities: the rows are denormalized, one path can appear
in several packages, and a path's owning revision moves whenever any commit
touches it.

**When you do regenerate — for an inventory change — land it as its own
commit, never as an amend.** The pinned revision is the commit that holds the
edited file, so amending changes that SHA and re-stales the pin you just fixed.

**The cascade that made this expensive is gone.** It used to run: a fix in
`core/` touches a skill-mapped source path, `skill_sync_check.py` demands a
SKILL.md edit, a SKILL.md is frequently a pinned path, so satisfying skill-sync
stales a handoff row, and the refresh commit touches the YAML — which is itself
mapped to *this* skill, re-arming skill-sync. A one-line source fix could
therefore require two skills and a ledger refresh. Both links are cut now: the
ledger and receipt are `generated_globs` in `tools/scripts/versioning.json`, so
touching them arms nothing, and an ordinary PR does not touch them in the first
place. Satisfy skill-sync with a real gotcha where you genuinely learned one,
and stop — there is no ledger step to chase.

`gates: ✓ all gates pass` still proves nothing about whether the 100+ identities
are *correct*. `gpu_handoff_pin_freshness.py` only asks whether this PR re-pins
and whether it orphaned a pinned path; only `gpu_handoff_provenance.py check`
verifies the identity fields, and no gate runs it. Run it yourself after any
regeneration you do land.

The drift check is also a ctest, `gpu-handoff-provenance-selftest`, so an
unregenerated ledger fails locally and in CI with the repair command in the
failure message rather than only as a stale-identity report.

**Write the receipt in the same run as the ledger, before you commit either.**
`write --receipt` stamps `source_commit` with whatever HEAD is when it runs, and
`test_published_receipt_binds_the_checked_in_ledger` re-derives the ledger from
that commit and compares bytes. A bare `write` that is then committed leaves no
way to fold the receipt into that commit afterwards: regenerating stamps the
commit you just made, and `--amend`ing the receipt into it orphans that SHA, so
the receipt names a commit that is no longer an ancestor of HEAD, and every
further amend repeats it. Regenerate from a clean tree with `--receipt` first,
then commit the ledger and the receipt together. To recover from a bare `write`
that already landed, `git reset --soft` back to the commit that owns the edited
files, restore the ledger, and regenerate with `--receipt`.

Nothing in the fast path catches that omission. `gates.sh`'s `gpu-handoff pin
freshness` gate asks only whether this PR re-pins and whether it orphaned a
pinned path, so a bare `write` alongside an inventory change turns it green, and
`gpu_recipe_catalog.py` validates the ledger's own identities and also reports
`OK`. The stale receipt surfaces only in
`gpu-handoff-provenance-selftest`. Run it directly before pushing:

```bash
python3 -B tools/scripts/test_gpu_handoff_provenance.py -k published_receipt
```

**A capability-registry change stales a handoff pin without touching any GPU or
Vellum file.** `tools/scripts/test_release_artifact_contents.py` is a pinned
path *and* one of the sites that hardcodes the control-registry digest, so
adding a row to `inspect/include/pulp/inspect/capability_definitions.inc`
re-pins the digest there and stales that row by pure transitivity. The
resulting `gpu-recipe-catalog-selftest` failure names a GPU catalog and a
release-artifact script, and nothing in either message mentions the capability
registry — so the natural reading is that the row belongs to another author's
change. Before dismissing it, check whether the flagged path is in your own
diff (`git diff --name-only <base>..HEAD -- <path>`) with a control grep that
must return non-zero; the answer is frequently yes.

**A pin makes two separable claims, and only one is enforced everywhere.**
Provenance is "the pinned revision is an ancestor of HEAD and still carries the
named blob and tree" — a fact about history that no later commit can falsify.
Currency is "the path at HEAD still holds that same blob" — a fact any commit
touching the path invalidates. One `require_current` switch selects between
them, and it has to be honored at all three layers a caller can enter through:
`validate_handoff_routing()` in `gpu_recipe_catalog.py`, and
`validate_with_catalog()` plus `resolve_identity()` in
`gpu_handoff_provenance.py`. The resolver is the easy one to miss, because it
reads as a lookup rather than a check — and a resolver that refuses stale-at-HEAD
input makes the strict answer leak back into every consumer above it.

Currency is off by default because the required per-commit gate validates every
pinned row, not just the rows a branch touched. Asserting currency there means
one commit landing on a pinned path turns that gate red for every other PR in
flight until each repins, which serializes concurrent work across the whole
pinned set. What catches a stale pin instead is
`gpu_handoff_provenance.py check` and the diff-scoped freshness guard, which
fires only for the branch that actually moved a pinned path and prints the
repair command with it.

## A red `gpu-handoff-provenance-selftest` names a fixture, not your file

The suite drives the generator's refusal paths on purpose, against a scratch
repository whose files are named `leaf.txt` and `nested/inner.txt`. Three of
those refusals print to the real stderr, so a run of this suite emits — while
passing — a null source commit reaching `git rev-parse`, an unclean-checkout
refusal naming `leaf.txt`, and a dozen contract-line violations from a fixture
ledger that cannot satisfy the closed v2 contract. None of it is yours.

Read the `FAIL:` line and nothing else. Two authors lost pushes to that text
because it appears above the single assertion that actually failed and reads
like a diagnosis. The control is cheap: the identical block appears in a green
run on a clean checkout.

Currency is also not this suite's to reject. The currency tier is opt-in behind
`PULP_GPU_HANDOFF_REQUIRE_CURRENT=1` and nothing in `.github` sets it, so a
stale ledger must not redden the required gate twenty minutes after a push —
`gpu_handoff_pin_freshness.py` catches the missing regeneration at push instead,
in under a second, with the repair command attached. A `gpu-handoff-provenance-selftest`
that goes red only because a branch edited a pinned path is a defect in the
suite, not a demand to regenerate.

One such defect is worth knowing because its shape recurs: `resolve` **writes**
its repair into the same two files `check` reads, so a `check` run after
`resolve` reports the repair and agrees with any verdict at all. Measured on a
tree that edits one pinned path without regenerating, `check` exits 1 naming two
drifts before `resolve` and 0 naming none after. Any cross-check of `resolve`
has to be read before it runs.

## The "Vellum freeze" CI job runs two checks, and the second is the one that fails

`.github/workflows/vellum-freeze-check.yml` runs `vellum_freeze_check.py` **and**
`vellum_expansion_watch_check.py` under the single job name `Vellum freeze`. A
`core/view/**` change routinely passes the first ("No transferred Vellum slice or
authority transition is affected") and fails the second, so reading the job name
sends you to the wrong script. Open the log and find which one raised.

A local **advisory** hint now surfaces this before the push rather than after
it. `tools/scripts/vellum_watch_preflight.py` runs from both `gates.sh` and
`.githooks/pre-push`; when the pushed range touches a watched glob it names the
affected families, prints the reproduce command, and repeats the commit-range
trap below. It **never blocks**, and it must not be promoted to blocking: the
authoritative check runs from a trusted root in two required contexts
specifically so a branch's own copy of the checker holds no veto, and it
compares against the merge-base for the same reason the reproduce command
below does. It is silent for a range that touches nothing watched, so silence
from it is not proof — run the checker by hand if you want a verdict on the
record.

Four non-obvious rules of the expansion-watch checker, none derivable from a
skim of the source:

- **Both `--base` and `--head` must be full 40-char SHAs.** A ref name fails with
  `base: expected full commit SHA`, which reads like a different bug than the one
  CI hit. Always `--base $(git rev-parse ...) --head $(git rev-parse HEAD)`.
- **Coverage is exact set equality**, not a superset test: the checker raises on
  `covered != affected`, so claiming an extra capability family fails exactly as
  hard as omitting one. Claiming the same family from two event files in one diff
  is also rejected (`duplicate capability-family claims`).
- **Verify a branch against its MERGE-BASE, not `origin/main`.** Diffing a branch
  that is behind main reports every watch event main has added since as a
  deletion, and the checker rejects it with
  `<event>.json: watch events are append-only`. That is an artifact of the
  comparison base, not a real append-only violation — `git merge-base origin/main
  HEAD` makes it disappear. CI compares against the PR base, so a branch that is
  merely stale never sees this.
- **It reads the COMMIT RANGE, never the working tree.** `_changes()` is a
  `git diff --name-status <base> <head>`, so an event file you have written but
  not committed is invisible to it: the run still reports
  `affected=[...] covered=[]`, identical to having written no file at all. That
  reads as "my JSON is malformed" and sends you to debug a correct file. Commit
  the event (amend is fine) and re-run — a green result is only meaningful once
  the file is in the range being measured. The same applies to `--head HEAD`
  after editing but before committing.

The event itself is an append-only JSON file directly under
`.github/vellum-expansion-watch-events/`, named exactly `<event_id>.json`, with
`capability_families` sorted and drawn from the known scope set. A change that
adds no authority carries `"disposition": "watch-only-no-authority"` and
`"authority_effect": "none"`.

## Validate the contract

Run the closed eight-case suite and projection validator:

```bash
python3 .agents/skills/pulp-vellum-change-routing/scripts/test_route_change.py
python3 .agents/skills/pulp-vellum-change-routing/scripts/routing_evidence.py \
  validate --projection .github/vellum-ownership.json --require-expansion
```

Only the push-to-main path of `.github/workflows/vellum-routing-contract.yml`
emits the digest-bound `pulp-vellum-routing-contract-execution` artifact. Pull
requests and manual dispatches validate the contract without publishing release
evidence. That receipt is evidence for Vellum's release verifier; do not
hand-author or replay it.

A3 v2 terminal acceptance is Pulp integration evidence. Its attribution may queue Vellum follow-up, but terminal proof must bind exact transferred routes and may not manufacture missing generic instrumentation in Pulp.

The trusted PR gate validates a deterministic synthetic merge tree, but event
time and emergency-expiry checks are bound to the real PR source head supplied
with `--source-head`. A synthetic merge intentionally carries a fixed historical
timestamp and must never become the provenance clock for a newly added event.

## Regenerate against the MERGED tree, not against the base you branched from

`write` derives every identity from one source commit, so a ledger regenerated
on a branch whose base has moved pins revisions the merged tree no longer agrees
with. The symptom is not a clear staleness report: it arrives as unrelated-looking
reds — three `test_gpu_recipe_catalog` failures and a
`test_gpu_handoff_provenance::test_check_reports_a_clean_ledger` assertion —
which read as a regression in the change set rather than as a base mismatch.

The order that works is the one the `ci` skill already prescribes for a pinned
path: land the file edits, bring the branch onto the merged tree, and only then
run `gpu_handoff_provenance.py write --receipt`. Regenerating before the rebase
means doing it twice.

**The regenerated ledger survives its own commit.** After committing the YAML
and receipt, `check` still reports `OK: every pinned identity matches` at the new
source commit, because the pin-refresh commit touches only paths the inventory
excludes. So the cascade above terminates after exactly one round — a second
regeneration is not needed, and running one only produces an empty diff.

## Merging `origin/main` preserves the pin; rebasing onto it orphans it

A receipt names one `source_commit`, and `check` requires that commit to be an
ancestor of HEAD. A **merge** of `origin/main` keeps the pinned commit in the
history, so the receipt stays valid and needs no repair. A **rebase**, an
`--amend`, or a squash rewrites it, and the receipt now names a commit that no
longer exists on the branch.

The repair that suggests itself is the one that cannot converge: regenerating
writes the commit the regeneration is about to create, which is not an ancestor
of HEAD either, so the next `check` fails the same way. Record a commit that
**already exists** — the newest commit reachable from HEAD that touched a pinned
path. `resolve_source_commit` names that commit in the failure text, so the
error is the answer rather than the start of a search:

```
source commit <sha> is not an ancestor of HEAD; identities generated from it
cannot satisfy the handoff validator. A rebase, amend, or squash of the pinned
commit is the usual cause; merging origin/main preserves the pin where rebasing
onto it does not. Record a commit that already exists rather than the one
regeneration is about to create, such as <sha>
```

Two mechanics that cost time on the way to that error:

- **`check | tail` reports the pipeline's status, not the checker's.** A run
  that prints `exit=0` under a pipe may have exited 1. Read `${PIPESTATUS[0]}`,
  or drop the pipe.
- **`write --source-commit` refuses on an unclean canonical path** (rc=2). So a
  repair cannot precede the merge commit that resolves the conflict: let the
  merge land, then regenerate from the merge sha. On a checkout without the
  merge driver below, "let the merge land" means taking `--theirs` on the
  generated ledgers by hand first.

A receipt conflict is also not always pointer churn. One case reported
`102 rows unchanged` while `handoff_sha256` moved to a value matching **neither**
parent — the ledger bytes were equal and the receipt hash was not, which means
the hash was computed over a tree that no longer existed. Regenerate from the
merge sha rather than picking a side.

## `regenerate-me` in a ledger is a resolved merge, not corruption

`.gitattributes` routes `docs/status/gpu-vellum-handoff.yaml` and
`docs/validation/gpu-handoff-provenance/receipt.json` to the
`pulp-gpu-ledger` merge driver, which `setup.sh` registers via
`tools/scripts/install-githooks.sh`. The driver reads all three sides — base,
ours and theirs — and overwrites, in each of them, every Pulp row's `revision`
and `object_id`, plus the receipt's `source_commit` and `handoff_sha256`, with
the literal `regenerate-me`. Those are exactly the fields `write` regenerates,
so poisoning them makes the re-pin churn byte-identical on every side, and an
ordinary three-way merge then runs over the result.

That split is the point. Identity churn cancels out; everything else — a row
one side added, a comment somebody re-bound — is content, and merges the way
content does. `vellum_paths` rows are constants pinned to a fixed foreign
revision and are never touched. When a re-pin was the only difference the merge
completes with no conflict markers and commits itself; when two authors edited
the same row it still comes back as conflict markers and a nonzero exit, which
is what you want.

**The value is invalid on purpose, and that is the entire mechanism.** Every
resolution that produces something *shaped* like an identity is accepted
somewhere: a stale-but-ancestral pin satisfies the always-on provenance tier, so
a driver that computed the merged value — or `merge=ours` — would hand Git a
wrong answer and Git would commit it without a word. `regenerate-me` cannot
survive. It fails the 40-hex check in `validate_handoff` *and* the blob
comparison in `validate_handoff_routing`, and
`tools/scripts/gpu_ledger_sentinel_check.py` rejects it from both the pre-push
hook and `gates.sh` before it can reach CI.

So when a ledger reads `regenerate-me`, nothing is corrupt: the merge is done
and the regeneration is owed. Regenerating is not optional politeness — the
identities are invalid until you do, and every tier says so:

```sh
python3 tools/scripts/gpu_handoff_provenance.py resolve   # preferred: decides the branch below
```

`resolve` runs the whole sequence from a committed merge: it regenerates pinned
to `HEAD` — a commit that already exists, because the receipt names its own
source commit and pinning to the commit the write is about to create cannot
converge — then decides whether anything actually moved, and proves the receipt
binds the ledger it was written beside with a control on a mutated ledger that
must come back False.

**The baseline is HEAD, not `origin/main`.** Regeneration always rewrites the
receipt's `source_commit`, so a diff is never by itself evidence of movement;
and a branch that already re-pinned its ledger differs from main *for a reason
that is not movement*, so taking main as the baseline calls an inert merge a
re-pin and commits the churn it was supposed to prevent. The question is only
whether regeneration changed what HEAD committed.

| Signal | Verdict | What `resolve` does |
|---|---|---|
| regeneration changes the ledger HEAD committed | `MOVED` | keeps it; reports `repaired N identity fields` |
| ledger unchanged, HEAD's receipt already binds it | `CHURN` | keeps HEAD's bytes; writes and commits nothing |
| ledger unchanged, HEAD's receipt does not bind it | `REBIND` | rewrites the receipt only — a text merge that took one side of the pair |
| ledger unchanged, receipt would change in a field that cannot move on an unmoved ledger | refuses (exit 3) | that is a human edit |

It also refuses (exit 2) while `MERGE_HEAD` is present or the index holds
unmerged entries, on an unclean canonical path, and (exit 3) when
`regenerate-me` survives regeneration — which means the driver poisoned a field
`write` does not rewrite, and no repair command clears it. Add `--commit` to
land the result, and `--json` for the verdict as data.

The manual form remains available and is what `resolve` performs:

```sh
python3 tools/scripts/gpu_handoff_provenance.py write --source-commit HEAD --receipt
git commit docs/status/gpu-vellum-handoff.yaml \
           docs/validation/gpu-handoff-provenance/receipt.json
```

Land that as **its own commit**: amending a commit that touches a pinned path
changes that path's owning revision and re-stales the row just repaired. Drop
`--receipt` and the ledger is repaired while the receipt stays bound to bytes
that no longer exist — green locally, red in CI.

**`check` answers the binding question, and it is the only thing that does.**
The identity tiers compare pins against Git; none of them reads the receipt, so
every pinned identity can match while the receipt names a ledger that no longer
exists. `check` therefore compares `sha256(ledger)` against the receipt's
`handoff_sha256` unconditionally — not behind a flag — and prints
`RECEIPT …` plus a nonzero exit when they disagree, or `the receipt binds this
ledger` when they agree. When no receipt is present it says so loudly rather
than exiting 0 on a claim it never examined.

Two limits worth knowing before trusting the driver:

- **A checkout that never ran `setup.sh` has no driver**, because Git resolves
  `merge=<name>` against *local* config that no clone carries — and it does not
  error on a name it cannot resolve, it falls back to the ordinary text merge in
  silence. A checkout bootstrapped *before* the driver landed is the same state
  and the likelier one: the attribute is there, so the automation looks
  installed while every sweep re-conflicts. `gpu_ledger_sentinel_check.py` now
  reports that directly — it reads the routed paths out of `.gitattributes` and
  asks Git whether the name resolves — so the pre-push hook and `gates.sh` both
  fail with `install-githooks.sh` as the repair. Re-running the installer is
  idempotent and takes a second.
- **GitHub's server-side merge does not run merge drivers.** A pull request can
  still show `CONFLICTING` on github.com while the same merge is clean locally.
  Merge `origin/main` into the branch, regenerate, and push.

## The watch-family selectors match PATHS, so a one-line include can demand an event

`tools/scripts/vellum_expansion_watch_check.py` decides which capability
families a change touches by globbing the changed path list against
`EXPECTED_SCOPES`. Nothing in that decision reads the diff. So adding
`#include <array>` to `test/test_browser_capture_tree.cpp` — a portability fix
that changes no capture behavior at all — matches `test/test_browser_capture*`
and makes `chromium-authoring-frontend` an affected family, which the trusted
base executor then requires a watch event to cover.

Two things make this expensive to find late:

- The failing check is **`Trusted base executor`**, which is not one of the five
  contexts branch protection requires, so a PR can sit `blocked` with that red
  while every required context is green and nothing names the cause.
- Its log buries the one useful line, `watch event family coverage differs;
  affected=[...] covered=[]`, under a full `Updating files:` checkout trace.

Reproduce it locally before pushing, and note that both arguments must be full
40-character SHAs — a ref name fails with `base: expected full commit SHA`,
which reads like a broken invocation rather than a real answer:

```bash
python3 tools/scripts/vellum_expansion_watch_check.py \
  --repo . --base "$(git rev-parse origin/main)" --head "$(git rev-parse HEAD)"
```

Commit the event file before you re-run. The checker measures the commit range,
so an uncommitted event scores `covered=[]` exactly like a missing one, and the
identical error message makes a correct file look broken.

The event is a new JSON file directly under
`.github/vellum-expansion-watch-events/`, named exactly for its `event_id`,
claiming the affected families sorted. Coverage is compared for **equality**,
not containment: claiming a family the diff does not touch fails the same way
omitting one does.

## Refreshing the ledger no longer pulls this skill into skill-sync

`docs/status/gpu-vellum-handoff.yaml` is mapped to this skill in
`tools/scripts/skill_path_map.json`, so touching it used to arm `skill-sync` on
top of everything else. A change that never intended to touch Vellum routing —
refreshing one registry-digest literal inside a pinned validator script such as
`tools/scripts/test_release_artifact_contents.py` — failed three gates in a
chain, each naming something further from the edit than the last: the ctest for
a stale row, the pin guard for an unrefreshed ledger, then `skill-sync` for the
refresh itself.

Both ends of that chain are cut. The ledger and its receipt are
`generated_globs` in `tools/scripts/versioning.json`, so `skill_sync_check.py`
filters them out before the map is consulted — the *generator*
(`tools/scripts/gpu_handoff_provenance.py`) is still mapped here, which is the
right place for the demand, since changing it is a routing decision and
regenerating its output is not. And the refresh itself has moved to the version
bot's bump commit, so an ordinary PR does not produce one. There is no
`Skill-Update: skip` ritual left to perform for a ledger refresh.

Files in this skill

  • SKILL.md8.2 KB
  • references/approved-exact-routes.v1.json70.8 KB
  • scripts/route_change.py2.1 KB
  • scripts/routing_evidence.py21.8 KB
  • scripts/test_route_change.py10 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…