Skip to content

ci: a standing caller for the required-set sweep, and five gates that declare the root file they read - #10011

Merged
os-warren merged 3 commits into
mainfrom
claude/family-9678-9979-required-contexts
Aug 19, 2026
Merged

ci: a standing caller for the required-set sweep, and five gates that declare the root file they read#10011
os-warren merged 3 commits into
mainfrom
claude/family-9678-9979-required-contexts

Conversation

@os-warren

Copy link
Copy Markdown
Collaborator

Fixes#9678
Fixes#9979

Family dispatch of two ruled cards that share one file (scripts/check-required-contexts.mjs). Per-card commits; both halves verified at head 9b479c512.


#9678 — a standing caller for --verify-required-set (ruled A+B)

--verify-required-set shipped with no caller: the only thing CI ran was its offline self-test, so the registry-vs-live diff existed and measured nothing.

A. .github/workflows/required-set-patrol.yml — scheduled 23 4,16 * * * (twice daily; the live set is changed by hand a few times a month), plus workflow_dispatch and a pull_request run scoped to the patrol and the script it calls. Follows half-state-patrol.yml's posture:

  • least privilege — contents: read only. The read's documented price is metadata=read, below anything a workflow can toggle, and this job writes no issue, label or comment;
  • findings never fail anything. A completed sweep exits 0 whether it found 0 or 40 disagreements;
  • the job goes red only on cannot-run / cannot-deliver. The script's exit 2 classifies the ENVIRONMENT, not the tree, and the exit code is captured with no pipe in between so the 0-swept / 2-environment split survives;
  • ⛔ never a required context, and structurally so — see the pins below.

Where the report lands, decided and stated: the run log and the run's step summary, and nowhere else. A new tracker is refused outright, and the one existing anchor (#9857) is owned end-to-end by half-state-patrol's generator — a second writer would make each patrol silently discard the other's findings. The honest cost of that choice is that a completed sweep finding drift exits 0 and shows a green tick, so the run emits a ::warning:: annotation (annotations render on the run list). It keys on the absence of the report's own clean mark, which fails toward a false alarm and never toward a false all-clear.

B. Header note in scripts/check-required-contexts.mjs — the live half is not runnable from a dev seat. Re-measured 2026-08-19 from this container, both documented spellings:

node scripts/check-required-contexts.mjs --verify-required-set
-> NOT VERIFIED — GET /repos/objectstack-ai/objectstack answered HTTP 401 (exit 2)
NODE_OPTIONS=--use-env-proxy node ... --verify-required-set
-> NOT VERIFIED — GET /repos/objectstack-ai/objectstack answered HTTP 403 (exit 2)

The 401 is the known proxy trap and the script already explains it. The 403 is the finding: with the documented remedy applied, the seat's own egress policy refuses api.github.com. The proxy hint correctly goes silent once the flag is set, so what a dev actually sees is a bare 403 with no explanation of its seat class. The header now says so, and says that a NOT VERIFIED there is the environment, not the tree. The earlier "the required SET is readable to an ordinary agent seat" paragraph is corrected in place rather than deleted — it was true for the seat class that measured it.

Pins added, and why they are presences. The existing self-test block asserted only absences (lint.yml / ci.yml / no package script runs the flag), and an absence cannot tell "deliberately off the required path" from "wired nowhere at all" — which is what this mode actually was. It now also sweeps all of .github/workflows (not the two files sources carries — a second caller in some third workflow is exactly what the absences cannot see) and pins: exactly one caller and it is the patrol; the patrol declares no merge_group: trigger — the mechanical proxy for "can never be validly required-ized", since assertion 6 of this pin is that every required context's workflow carries one; no REQUIRED_CONTEXTS row names it; and it still keys its annotation on the clean mark this file renders (one exported constant, so the report and its reader cannot diverge).


#9979 — five more gate families declare the repo-root file they read

Applying the pattern #9978 landed, per gate, with zero changes to extractWatchHints or hintCovers — the bare-*.md widening stays refused, and the new cases pin that these declarations bought no part of it.

gateroot file declaredwhere
check:pm-skill-id-lintAGENTS.mdEXTRA_FILES
check:pm-governed-mergesAGENTS.md, CLAUDE.mdthe two exact rows
check:pm-governed-proseAGENTS.mdPROSE_SURFACES
check:docs-audit-scopeAGENTS.mdAGENTS_REL
check:required-contextsAGENTS.mdINSTRUCTION_SURFACES
check:doc-anchorsREADME.md, ARCHITECTURE.mdEXTRA_SOURCES

Every declaration is provenance, never a lookup key: each gate opens its real constant, and the glob spelling appearing there would send it reading a path that does not exist — silently in doc-anchors' case, since existsSync filters the extra sources out. Both halves are pinned per file: the declaration covers the real population, and nothing declared is a value the gate looks up.

check:doc-anchors' "still has to reach this gate by judgment" docblock now points at its declaration. Its COVERAGE is unchanged: 251 fragment links across 399 sources, identical before and after.

Derivation, measured before → after (node scripts/pm/dispatch-gates.mjs FILE):

card surfacebeforeafter
AGENTS.md17 (6 in lint.yml + the #9678 patrol family)
README.md01
ARCHITECTURE.md01
CLAUDE.md01
LICENSE (control)00
examples/AGENTS.md (control)00

Total families 114 → 115 across 26 → 27 workflow files; the new one is the patrol from #9678.


Reverse verification — directions predicted in writing first

(i) Empty check-doc-anchors' declaration (the REQUIRED gate, the repo's only fragment coverage). Predicted: README/ARCHITECTURE derivation returns to zero; doc-anchors reddens in its self-test, not its sweep; dispatch-gates reddens on the two positive halves only. All three observed:

-- README.md -- No check family names the given paths in its own source...
-- ARCHITECTURE.md -- No check family names the given paths in its own source...
❌ check-doc-anchors --self-test: every extra source declares a root-file watch hint: README.md, ARCHITECTURE.md vs
✗ the doc-anchors gate reaches the repo-root file it declares (README.md)
✗ the doc-anchors gate (ARCHITECTURE.md half) reaches the repo-root file it declares (ARCHITECTURE.md)
✗ dispatch-gates self-test: 2 of 332 case(s) failed.

Note the direction is not uniform, and was predicted as such: dispatch-gates' self-test reports more failures while README.md's derivation reports fewer families. Every negative half stayed green — the declaration bought its own file and nothing else. Restored via git checkout HEAD --, both gates re-run green.

(ii) Delete the patrol workflow. Predicted red on the new presence assertions, and predicted the AGENTS.md derivation drops to 6. The derivation behaved as predicted — and the first run of this ablation found a real defect in the code added by this branch: the block died on an uncaught ENOENT instead of reporting, so the exactly one workflow failure was recorded and never printed, every later assertion never ran, and !/merge_group/ on an unread file is vacuously true — a false green about a workflow that does not exist. Fixed in 9b479c5; re-measured under the same ablation:

✗ check-required-contexts --self-test — 4 failure(s)
• wiring: exactly one workflow runs the live read and it is required-set-patrol.yml — found [] (#9678)
• wiring: the standing caller .github/workflows/required-set-patrol.yml is missing — the live mode is wired nowhere again (#9678)
• wiring: required-set-patrol.yml must declare no merge_group trigger — ...
• wiring: required-set-patrol.yml must key its drift annotation on the clean mark ✅ this file renders (#9678)

(iii) The workflow, validated mechanically — GitHub Actions cannot be executed locally, and this repo has no actionlint; its workflow gates are check:workflow-status-functions, check:node-version, check:shard-attestation and check:required-contexts, all green below. Beyond those: the file parses under the repo's own yaml package (triggers / permissions / jobs / steps inspected, merge_group absent), all four run: blocks pass bash -n, and the annotate step was executed as real bash against real rendered reports in both directions:

=== clean === ::notice::required-set sweep: the live required set and REQUIRED_CONTEXTS agree in both directions.
=== drift === ::warning::required-set sweep found a registry-vs-live DISAGREEMENT. ...

Gates — all at head 9b479c512

Union derived by node scripts/pm/dispatch-gates.mjs with no arguments (15 families), every one run, exit codes captured before any pipe:

gateexitverdict line
check:cross-package-test-inputs0OK: 12 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.
check:doc-anchors0✅ check-doc-anchors: 251 internal #fragment link(s) across 399 source file(s) all resolve to a real heading
check:docs-audit-scope0✓ check-audit-scope self-test: 24 cases pass.
check:node-version0check-node-version: OK (30 setup-node step(s) across 27 workflow(s), all on Node 22).
check:pm-dispatch-gates0✓ dispatch-gates self-test: 332 cases pass.
check:pm-governed-merges0✓ check-governed-merges --self-test: 81 assertions ...
check:pm-governed-prose0✓ check-governed-prose: 2 instruction surface(s) name all 5 registered governed surfaces ... and claim no others.
check:pm-skill-id-lint0✓ check-skill-id-lint: 15 file(s) clean (pattern /#[0-9]{3,}/g).
check:required-contexts0✓ check-required-contexts: 6 required context name(s) pinned across 2 workflow(s); 5 instruction surface(s) scanned against 2 retired name(s)
check:shard-attestation0✓ check-shard-attestation: 2 aggregate gate(s) count 3 declared leg(s) across 3 attesting job(s).
check:workflow-status-functions0check-workflow-status-functions: OK (scanned 27 workflow file(s), 46 job(s), 24 job-level if: expression(s) ...)
check:nul-bytes0check-nul-bytes: OK (scanned 6341 text file(s) ... no raw ASCII control bytes).
node scripts/check-cross-package-test-inputs.mjs0same verdict as its pnpm form
node scripts/check-shard-attestation.mjs0✓ check-shard-attestation: 2 aggregate gate(s) ...
node scripts/docs-audit/check-affected-docs.mjs0completed, reported its unreachable-row census

Self-test counts, before → after: dispatch-gates 314 → 332 · check-required-contexts 115 → 124 · check-governed-merges 77 → 81 · check-governed-prose 24 → 28 · check-audit-scope 22 → 24 · check-skill-id-lint 10 → 14 · check-doc-anchors 17 → 22 assertions (that file's self-test prints no case counter; counted from its source).

Scope

Not governed — scripts/** and .github/workflows/** only. Held to the dispatch exclusion list: no extractor / hintCovers change, the new workflow is never required or merge-blocking anywhere, doc-anchors' coverage is unchanged, and nothing under .claude/ is touched. No changeset — nothing here is published; skip-changeset applied.


Generated by Claude Code

`--verify-required-set` (#9642) shipped with no caller: the only thing CI ran
was its offline self-test, so the registry-vs-live diff existed and measured
nothing. Ruled A+B on the card.
A. `.github/workflows/required-set-patrol.yml` — scheduled twice daily
(`23 4,16 * * *`), plus workflow_dispatch and a pull_request run scoped to
the patrol and the script it calls, following half-state-patrol.yml's
posture: least privilege (`contents: read` — the read's price is
`metadata=read`), findings never fail anything, and the job goes red ONLY
when the sweep could not run or its report could not be delivered. Exit code
captured with no pipe in between, so the 0-swept / 2-environment split
survives. The report lands in the run log and step summary and nowhere else:
a new tracker is refused, and the existing anchor (#9857) is owned
end-to-end by half-state-patrol's generator. A completed sweep that finds
drift exits 0 and would show a green tick, so it emits a `::warning::`
annotation keyed on the ABSENCE of the report's own clean mark — which fails
toward a false alarm, never a false all-clear.
B. Header note in `scripts/check-required-contexts.mjs`: the live half is not
runnable from a dev seat. Re-measured 2026-08-19 — bare run answers HTTP 401
(the #7412 proxy trap), and with the documented `--use-env-proxy` remedy it
answers HTTP 403, the seat's own egress policy. The proxy hint correctly
goes silent once the flag is set, so a dev sees a bare 403 with no
explanation of its seat class. NOT VERIFIED there is the environment, not
the tree.
The self-test's "live mode stays OFF the required path" block gains the
presence half it lacked: an absence cannot tell "deliberately off the required
path" from "wired nowhere at all". It now sweeps all of .github/workflows for
callers (not just the two files `sources` carries), pins that the only one is
the patrol, that the patrol declares no `merge_group:` trigger (the mechanical
proxy for "can never be validly required-ized" — assertion 6), that no
REQUIRED_CONTEXTS row names it, and that it still keys its annotation on the
clean mark this file renders. 115 -> 119 assertions.
Part of #9678
…read
#9964 established the per-gate escape hatch for a population that is a
repo-root FILE: dispatch-gates' extractor requires a path separator, so a bare
`AGENTS.md` literal is a WORD and contributes no watch hint — the gate reads
the file while naming nothing that can match it. The remedy is to declare the
subtree spelling (`<file>/**`), which `extractWatchHints` already accepts and
`collapseHint` reduces back to that one path.
Measured under #9979, nine of those (family, root file) pairs are genuine and
all but the ratchet's were still invisible. Each gate now declares its own, in
its own idiom, pinned by its own self-test:
check:pm-skill-id-lint AGENTS.md (EXTRA_FILES)
check:pm-governed-merges AGENTS.md, CLAUDE.md (the two `exact` rows)
check:pm-governed-prose AGENTS.md (PROSE_SURFACES)
check:docs-audit-scope AGENTS.md (AGENTS_REL)
check:required-contexts AGENTS.md (INSTRUCTION_SURFACES)
check:doc-anchors README.md, ARCHITECTURE.md (EXTRA_SOURCES)
Every declaration is PROVENANCE, never a lookup key — each gate opens its real
constant, and the glob spelling appearing there would send it reading a path
that does not exist (silently, in doc-anchors' case, since `existsSync` filters
the extra sources out). Both halves are pinned per file: the declaration covers
the real population, and nothing declared is a value the gate looks up.
Zero changes to `extractWatchHints` or `hintCovers` — the bare-`*.md` widening
stays refused, and the new cases pin that these declarations bought no part of
it (`examples/AGENTS.md` and the cross-root pairs stay unreached).
Derivation, measured before -> after:
AGENTS.md 1 -> 7 families (6 in lint.yml + the #9678 patrol)
README.md 0 -> 1
ARCHITECTURE.md 0 -> 1
CLAUDE.md 0 -> 1
LICENSE 0 -> 0 (control)
examples/AGENTS.md 0 -> 0 (control)
Self-tests: dispatch-gates 314 -> 332, skill-id-lint 10 -> 14,
governed-merges 77 -> 81, governed-prose 24 -> 28, check-audit-scope 22 -> 24,
required-contexts 119 -> 123, doc-anchors 17 -> 22 assertions.
check:doc-anchors' "by judgment" docblock now points at its declaration; its
COVERAGE is unchanged (251 fragment links across 399 sources, same as before).
Part of #9979
…s missing
Found by reverse verification of the block added in this branch: deleting
required-set-patrol.yml made the self-test die on an uncaught ENOENT instead of
reporting. Two consequences, both the #4690 family:
- the `exactly one workflow runs the live read` failure was recorded and
never printed, and every assertion after it never ran — a stack trace where
the gate's own authored verdict belongs;
- `!/merge_group/` on an unread file is vacuously TRUE, so the assertion that
the patrol can never be validly required-ized would have passed about a
workflow that does not exist.
The file is now read through `existsSync`, its absence is its own named
assertion, and each downstream case carries `patrolPresent` so none of them can
go green on a file nobody read. Re-measured under the same ablation: 4 named
failures, no throw. 123 -> 124 assertions.
Part of #9678
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci/cdsize/lskip-changesetPR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants

@os-warren@claude