Skip to content

ci(governed-guard): install the generator toolchain so the queue leg can lift a pure regeneration - #14069

Merged
os-sam merged 1 commit into
mainfrom
claude/issue-14063-guard-job-deps
Sep 1, 2026
Merged

ci(governed-guard): install the generator toolchain so the queue leg can lift a pure regeneration#14069
os-sam merged 1 commit into
mainfrom
claude/issue-14063-guard-job-deps

Conversation

@os-sam

@os-sam os-sam commented Sep 1, 2026

Copy link
Copy Markdown
Collaborator

Fixes #14063

What this changes

The merge_group leg of Governed Surface Queue Guard installed no dependencies, so every #11705 register row — the generator-owned files inside skills/** — failed closed there: they recompute by running the generator's own --check through pnpm … exec tsx, and with no toolchain the recompute answered "the generator toolchain is not available in this environment" on every run. A spec PR carrying its regenerated references/_index.md was therefore still governed at queue time and needed a pinned maintainer approval, while the seat-side --test lifted the identical diff in a dev container. Two tools, one register, two answers.

The guard's own header filed that trade as "not taken" because no ruling covered it. The maintainer ruled it (2026-09-01, verbatim and untranslated):

纯生成的指针行(spec 源变更后再生成的 references/_index.md) 不需要我审核吧

So the job now acquires pnpm through the shared ./.github/actions/setup-pnpm composite, restores the pnpm store from the same cache key lint.yml uses, and runs pnpm install --frozen-lockfile.

Three decisions inside that are load-bearing:

  • No --filter. The packages the recompute needs are GENERATED_SURFACE_EXCEPTIONS[].verify.pkg; a filter list in the YAML would be a second copy of that set, going stale silently the day a row is added — and in the direction that reads as compliance (the new row's generator fails to spawn, the path stays governed, and nothing says the install was the reason). A full install restates nothing, so a register row reaches this job for free. The self-test pins it: every verify.pkg in the register must be covered by the install line.
  • No build step, and that is measured rather than assumed — both verify rows run through tsx off their TypeScript sources and their relative ./lib/* imports; nothing consults any package's dist/.
  • Every toolchain step is continue-on-error: true. This is the half that keeps the guard's existing promise. Without it, a registry outage, a cold cache or a PR with an out-of-sync lockfile would newly redden merge groups that touch nothing governed — on the one check whose entire job is to refuse. With it, a broken install degrades to exactly the behaviour this job had before the block existed: the generator cannot spawn, the recompute states why, and the path stays GOVERNED. Fail-closed on the governed path, unaffected on the clear one, and the verdict stays inside the script where the exit contract lives.

Both legs install, deliberately. The pull_request leg is a forecast of the queue verdict ("the merge-queue run of this same check will REFUSE it"), so a leg that cannot recompute forecasts a refusal that will not happen. Its behaviour is otherwise untouched — still exit 0 on every approval state, still no head read, still byte-identical wording.

Nothing about the exemption is softened: the recompute is still the register's own byte-exact one against this build's own tree, never a stored baseline; every error path still refuses with its existing exit code; and hand-authored governed content — including a hand edit sitting in the same commit as a certified regeneration — still needs the GOVERNED_APPROVERS approval pinned to the current head. The workflow name: / check context Governed Surface Queue Guard is unchanged, and the self-test still asserts that literal.

Measured cost of the install

Container measurements, shared box (parallel agents on the same cores, so read these as an order of magnitude rather than idle-box figures), warm pnpm store, fresh worktree at 7d02199a:

step wall how
pnpm install --frozen-lockfile 7s fresh worktree, warm store, through scripts/pm/os-verify-lock.sh
the skill-refs generator's --check (what the recompute runs) 1s pnpm --filter @objectstack/spec exec tsx scripts/build-skill-references.ts --check --generated-manifest=…, exit 0, 9 declared outputs
any build 0s none is needed — measured by running the generator above in a worktree where nothing had been built

What that means for a queue build, stated honestly: I cannot measure GitHub's runners from here. The added CI work is the setup-pnpm composite (network-free on a warm Corepack cache), a store-cache restore, and the install — the same preamble five other jobs in this repo already run. It does not move the queue build's critical path: this job is parallel with jobs that take minutes, and it keeps its timeout-minutes: 10. The real price is runner minutes per queue build and per PR run, not queue latency. The first build after a pnpm-lock.yaml change pays a cold store restore.

Verification — all at 7d02199a, the final commit

  • node scripts/pm/check-governed-queue-guard.mjs --self-test✓ … 129 cases pass (exit 0, captured before any pipe).
  • Gate family derived from the real diff, not recalled: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack → 31 families + 2 convention-triggered (this diff edits gate scripts). All 32 harvested commands run; 30 exit 0. The two non-zero are NOT MEASURED, not failures, and each says so in its own words:
    • node scripts/check-test-completeness.mjs → exit 3, its own "nothing was measured" code: "there is no local log to hand it, so the local reading for this gate is NOT MEASURED".
    • node scripts/pm/check-governed-queue-guard.mjs (the live CI invocation, run with no event payload) → exit 1 could not read GITHUB_EVENT_PATH. The local reading for this script is its --self-test, above.
    • Green includes the ones this diff most disturbs: pnpm check:pnpm-acquisition (census now records this job at governed-surface-guard.yml:155 under cached · composite, 24 sites), check:node-version, check:required-contexts, check:pm-governed-merges, check:pm-dispatch-gates, bare-root-worklist --self-test, check:self-test-wired, check:self-test-workflow-commands, check:workflow-status-functions.
  • pnpm lint (repo-wide eslint . --no-inline-config) → exit 0 in 81s. No narrowing, so no narrowing argument is owed.
  • node scripts/check-nul-bytes.mjs → OK over 7678 files; plus a direct control-byte scan of the three edited files, zero hits.
  • Governed-surface self-check on this very diffnode scripts/pm/check-governed-merges.mjs --test over the three changed paths: ✅ NOT governed, 0 of 3 paths hit the register, exit 0. Ordinary queue landing applies.

Reverse verification — the new pins were made to fail on purpose

Each leg mutated the tree from the committed state, proved the mutation reached disk (occurrence counts plus a git hash-object differing from the HEAD blob), ran the self-test, restored with git checkout HEAD -- …, and proved the restore (git diff HEAD empty and blob equal to the HEAD blob). Predicted direction: red. All five went red, each naming its own assertion:

mutation self-test
delete the Install dependencies step exit 1 — the-job-installs-the-workspace-dependencies-the-recompute-runs-on: (no pnpm install step) + the ordering pin
drop continue-on-error from the install exit 1 — every-toolchain-step-DEGRADES-…: missing continue-on-error: Install dependencies
add --filter @objectstack/objectui to the install exit 1 — the-install-covers-the-register-row-without-restating-it for both register rows
move the install after the live judgment exit 1 — the-toolchain-is-in-place-BEFORE-the-live-judgment…: {"selfTest":2,"install":7,"judgment":6}
make the lift fail OPEN (stillGoverned = new Set()) exit 1 — the fail-closed end-to-end cases flip to {"conclusion":"clear","exitCode":0} and catch it

The last one is the important one: it is what shows the new end-to-end cases are measuring the refusal rather than restating it.

New self-test coverage (129 cases)

Behaviour, run through the whole decision — lift, then runGuard, then the exit code — over the #13794 diff shape (a spec source edit plus its four regenerated pointer files in one commit):

  • a certified recompute ⇒ CLEAR, exit 0, zero approvals and zero API calls (measured with spies that throw, the same way the existing ordering guarantee is), and the notes name the ruling that lifted each of the four files;
  • an uncertified recompute ⇒ still REFUSED (exit 3), for both real reasons: no toolchain, and generator drift — each with its stated reason in the log;
  • a hand-authored skills/*/SKILL.md beside a certified regeneration ⇒ still REFUSED, and the refusal names the hand-authored path without naming the lifted ones;
  • a recompute that throws ⇒ propagates into EXIT_CANNOT_RUN rather than reading as a lift.

Environment, pinned against the YAML on disk (a green self-test over a workflow that had silently lost its install would be the loudest possible false negative): the install exists and is --frozen-lockfile; pnpm comes from the shared composite; the toolchain sits after the self-test and before the live judgment; the install covers every register verify.pkg without restating one; and every toolchain step carries continue-on-error — with a non-emptiness assertion so that pin cannot pass vacuously if the steps are ever reordered.

Scope notes

Generated by Claude Code


Generated by Claude Code

…can lift a pure regeneration

The merge_group leg of Governed Surface Queue Guard installed no
dependencies, so every #11705 register row (generator-owned files inside
skills/**) failed closed there: a spec PR carrying its regenerated
references/_index.md stayed governed at queue time and needed a pinned
maintainer approval, while the seat-side --test lifted the identical diff
in a dev container. The script header filed that trade as not taken; the
maintainer took it (2026-09-01).

The job now acquires pnpm through the shared composite and runs a plain
pnpm install --frozen-lockfile - no --filter, so the register's verify.pkg
set is not restated in YAML, and no build, because both generators run
through tsx off their TypeScript sources. Every toolchain step is
continue-on-error: a broken install degrades to exactly the fail-closed
behaviour that preceded it instead of newly making ordinary queue builds
depend on the registry.

Nothing about the exemption is softened: the recompute is still the
register's own byte-exact one, every error path still refuses, and
hand-authored governed content still needs the pinned approval.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015adLit3ZYASJiXwxKG78Wi
@claude claude Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 1, 2026
@os-sam
os-sam marked this pull request as ready for review September 1, 2026 02:49
@os-sam
os-sam requested a review from hotlong as a code owner September 1, 2026 02:49
@os-sam
os-sam enabled auto-merge September 1, 2026 02:49
@os-sam
os-sam added this pull request to the merge queue Sep 1, 2026
Merged via the queue into main with commit c56592b Sep 1, 2026
34 checks passed
@os-sam
os-sam deleted the claude/issue-14063-guard-job-deps branch September 1, 2026 03:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cd size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

给 governed-surface-guard 的 merge_group job 装依赖,启用生成物豁免重算 —— 纯再生成的 skills 指针行不再需要维护者批准

2 participants