Repository navigation
Required checks count only from the App or workflow pinned for their name [PROTECTED: owner merges] - #788
Conversation
…or its name, not any same-name check Readiness, the check of main after a merge, fix-forward, the fast checks and the main watch matched a required check by name alone, so anything that can post a check run or a commit status under that name (another App on the repo, a workflow a PR adds, a run posting onto another commit through the API, a status) could turn it green. Governance mode (#704) would gate PRs nobody here wrote on that. Now every judgement reads GitHub's checks through one place (PinnedChecks in src/check-source.ts) and keeps a check only from its name's pinned source: config.yaml required_check_sources ({ app_id } or { workflow: .github/workflows/<file> }, the workflow run in this repo and for the very commit judged), else the App GitHub's branch protection or ruleset requires for it, else none, and the check is refused (missing) with a doctor line naming it. A commit status never satisfies a pin. The main watch reads the pins committed with the required checks at a merge's first parent. FixForward's checks reader is now required, so nothing judges GitHub's raw answer; a test pins that only check-source.ts reads it. Test (fails without): test/required-check-source-coordinator.test.ts (a PR whose check is posted by another App is never ready; the merge flow's checks of a commit drop it) and the main-watch test "a merge whose required checks came from another App or workflow than their pins, or a status, is unaccounted"; test/required-check-source.test.ts covers the stand-in GitHub API answers. Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
…hose import tree now reaches it src/backlog/types.ts, in the guard's import tree, takes CommitCheck's source types from src/check-source.ts, so that module is part of the tree and test/review.test.ts requires it on the guard's own list (locked as self-modification, L3). It decides which checks count, so the lock fits. Test (fails without): test/review.test.ts "the guard's own list (src/hook-imports.ts) is exactly the tree's modules no other glob locks" (red on #788's CI run 39199536673, shard 3/4). Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
Evaluator verdict: CHANGES NEEDED (round 1/2)Reviewed head Acceptance criteria
Fail-without (measured)
Every judging path goes through
|
| Option | This PR's head (all 9 green by name) | Effect on this install |
|---|---|---|
| (a) fail closed, as built | ready=false: all 9 missing (measured) |
No PR becomes ready. Every merge is unaccounted (stop plus revert PR) until a hand-merged, pre-accepted config PR pins them (inferred). A machine with an updater canary would roll back and hold every engine install that contains #788, because the canary instance's PR never becomes ready (inferred from scripts/machine/worklane-update.cjs:72-77) |
| (b) learn from main's tip | 8 of 9 learnable, each from exactly one workflow file (ci.yml, or gitleaks.yml for scan). new-deps isn't posted on main at all: it runs on pull_request only (.github/workflows/new-deps.yml). Result: ready=false, new-deps missing (measured) |
It still blocks every PR here unless it also learns from main's workflow files or falls back to (c) for names it can't learn. The pins would also have to live outside the repo, since agents can't write .worklane/**, so a local state file would hold a security setting nobody reviews (inferred) |
| (c) bare name as today, with a doctor warning | ready=true, the same as today (measured) |
No change until pinned. Pins set in config, or named by GitHub, apply at once |
- Does any option reopen a route an existing install had closed? None of them, with one exception that all three share (measured):
- Once a name is pinned, a failing same-name run from another App no longer blocks. The fixture: an unpinned App's
failurelisted before the pinned App'ssuccess. Name only givestest=fail ready=false; pinned givestest=pass ready=true. - Under (a) and (c), a GitHub-derived pin applies this on upgrade with no owner action.
- Change 1 removes it in every option.
- Once a name is pinned, a failing same-name run from another App no longer blocks. The fixture: an unpinned App's
- I recommend (c) (inferred):
- Keep GitHub-derived and config pins enforced, since they only tighten where GitHub or the owner already named a source.
- Have doctor print a ready-to-paste
required_check_sourcesblock, learned the (b) way from main's runs plus the workflow files, as a suggestion and not recorded automatically. - Keep
governanceRefusalas Governance mode: the engine runs only its evaluator, merge policy and locks in front of PRs other agents or people open, with no agents of its own; external reviews and CI are policy inputs, never the decision #704's gate. - This meets the issue's acceptance item. It costs nothing for the engine's own PRs, whose agents can't add or edit workflows (above). The security goal that motivates Required checks: pin each one to the App or workflow that posts it, before governance mode (today a same-name check from anywhere counts) #727, gating foreign PRs in Governance mode: the engine runs only its evaluator, merge policy and locks in front of PRs other agents or people open, with no agents of its own; external reviews and CI are policy inputs, never the decision #704, stays intact.
Protection
Measured, from the PR metadata and diff:
- It touches engine paths (
src/**,templates/**), self-modification (src/coordinator/merge-flow*,src/main-watch*,src/github/main-watch-api*), the guard's own list (src/hook-imports.ts) and engine-guards (test/main-watch.test.ts). - It is marked correctly: the title has
[PROTECTED: owner merges], the body has**Risk class:** protected, it carries theneeds-ownerlabel, and it has realWhat this can never doandCompromised componentsections. - The "can never do" claims:
- "Turn a check that was … failed … into a pass" is false as built (measured, the fixture above).
- "canary.ts reports, never gates" is inaccurate: the updater keeps or rolls back an install on the canary's verdict (inferred,
scripts/machine/worklane-update.cjs:72-77). - The rest hold (inferred): the new calls are GETs only, and the updater is untouched.
Scope, hygiene, CI
- Scope: nothing beyond the issue (inferred). The template, doctor and
hook-importschanges are needed for what it adds. Setup: one setup PR, with required checks pre-filled from the tip of main and the starter-CI choice made inside the setup step #730's pre-fill isn't done; it is its own issue. - Hygiene (measured):
- both commits are signed off, with no Co-Authored-By or "Generated with" in the commits or the body;
- no new dependency and no
.claude/settings.json; - no product name in the new
src/lines; - every test runs on temp dirs.
- CI (measured,
gh pr checks 788): 19 pass, 7 skipping, 0 failing or pending.
Required changes
- Pinning must only ever turn a match into missing. This is the issue's constraint and this PR's own "can never do". Today it can also turn a fail into a pass (measured above). Fix it as Engine updater counts a required check only from its pinned workflow or App [PROTECTED: owner merges] #789's updater does with its all-green check over every run: a same-name non-pass from any source still blocks. One way: a check is
passonly when it passes both by name, as today, and from its pin. Apply it in the merge flow (requiredOutcomesoverpinned.of) and in the main watch (outcomesAtover the filtered runs). Add a test with that fixture, for readiness and for the main watch, that fails on the current head. - Make a raw read of GitHub's checks impossible, not grep-able. The predicate misses 11 of 12 raw reads (measured). Preferred:
PinnedChecks.ofreturns a branded type, andprReadiness,headChecks,mayMerge,rulesetHoldandFixForwardDeps.checksaccept only that type, so a rawBacklog.checksresult doesn't compile. At minimum: a TypeScript AST scan that flags anychecksproperty access on aBacklog-typed expression, tested with fixtures like those above passed to the scanner's own function. - Close or prove the API-made check run case for workflow pins. Guess: a check run created through the API with a workflow's
GITHUB_TOKENmay be filed under an existing Actions suite on the target commit, and that suite maps to the pinned file. If so, "a run posting onto another commit through the API" passes the pin. Proving the negative needs a write to GitHub, so add the cheap positive check instead:- require an Actions check run's
html_urlto be/<repo>/actions/runs/<the mapped workflow run's id>/job/<this check run's id>. On this repo a genuine run's check-run id is its job id, e.g..../actions/runs/39197227793/job/117883963166(measured); - or check the run's jobs with
GET /actions/runs/{id}/jobs; - do it in both
src/backlog/github.tsand the main watch API, with a test where a suite mapped toci.ymlcarries a check run whosehtml_urlis/runs/<id>, and that run ismissing.
- require an Actions check run's
- Correct the claims.
- "What this can never do" must match the code after change 1.
- Fix "canary.ts reports, never gates": it decides whether an install stands. Either route it through
PinnedChecksor say so. - If (a) stays, add the canary rollback to "Existing installs".
… a raw read can't be judged Review round 1 and the owner's choice for existing installs (option c, 2026-10-19): - A pinned check passes only when its pinned source passes and the check as judged by name (any source) passes too: another App's failing or pending same-name run still blocks it, in readiness, the merge flow and the main watch. Before, pinning could turn a fail into a pass. - A check nothing pins (no config.yaml entry, no App named by GitHub's rules) is judged by name, exactly as before, so an existing install changes nothing on upgrade. - Every judging function (readiness, requiredOutcomes, headChecks, mayMerge, prReadiness, fix-forward's reader) takes JudgedChecks, which only PinnedChecks.of makes: a raw Backlog.checks answer doesn't compile there. test/raw-check-reads.ts scans src/ by type for raw reads elsewhere (check-source, the main watch, the canary, each with why); the regex check stays. - A workflow pin also needs the check run to be one of its workflow run's own jobs (its page /<repo>/actions/runs/<run>/job/<id>), so a check run made through the API into that suite doesn't count. - doctor prints a paste-ready required_check_sources block (src/check-pin-block.ts): each name mapped to the workflow file that posts it, confirmed against main's tip where it can be, ambiguous ones listed for a manual choice. auto-config (init, add-project) writes it into a new config. Test (fails without): test/required-check-source-coordinator.test.ts (another App's failing run blocks a pinned pass; an unpinned install's PR is ready as on main), test/main-watch.test.ts (#727 cases: fail-to-pass, API-made run, unpinned as before), test/required-check-source.test.ts (raw answer doesn't compile, the 12 raw-read spellings), test/auto-config.test.ts (new config pinned), test/check-pin-block.test.ts. Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
Round 1: changes at 65fa328Each claim is labelled measured, inferred or guess.
Verification (measured):
The full fail-without table is in the body. |
… own PR, keep doctor's plain line #788 grew to about twice the 600-line limit, so the doctor pin block (src/check-pin-block.ts) and the pins auto-config writes into a new project's config move to a PR stacked on this branch. What stays: the engine pinning, option (c) matching, the JudgedChecks type, the job-page check for workflow pins and the corrected claims. doctor keeps its plain line (checkSourceDoctor): each required check with no pin, judged by name, and how to pin it. Test (fails without): test/required-check-source.test.ts "doctor names each required check with no recorded source, judged by name, and how to pin it". Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
…page on its run's host; no __proto__ pin From an adversarial pass against the owner's decision guide: - GitHub's protection and rules never answered (an error from the start): a check config.yaml doesn't pin was judged by name, though the rules may pin it. Now it is missing until GitHub answers, and the main watch waits to judge a merge (never unaccounted for a failed read). - A check run's page with the right path on another host, or over http, passed the job-page check. Now it must be the workflow run's own page (its html_url) plus /job/<id>, over https on that host. - A config.yaml pin keyed __proto__ was dropped by the record, leaving that check judged by name. Now the config is refused. Test (fails without): test/required-check-source.test.ts (unknown rules: missing; the other-host and http pages; the __proto__ pin) and test/main-watch.test.ts (a merge waits while the rules are unknown). Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
Split, and fixes from an adversarial pass (head d614853)
|
Evaluator verdict: CHANGES NEEDED (round 2)I reviewed head Round-1 required changes
Option (c): an install without pins behaves as main
Adversarial checks (measured)
The brand and the raw-read scan (principle 4)Fixtures were passed as data to the PR's own
Principle 9: the raw-read allowlist
Fail-without (measured)
Rule 10, size, hygiene, protection, CI
Required changes
A genuinely new trade-off for the ownerShould a pinned name also be blocked by any same-name non-pass, from any source, not only the one judging by name picks? Inferred: this is stricter than main, and it has a new cost. Any App on the repo, or a cancelled duplicate suite of the pinned workflow (push and |
…edChecks caught; no dropped __proto__ rule
Review round 2:
- While GitHub's rules have never answered, the main watch returned before judging at all, so a direct
push or a bot's merge waited too. Now judge() waits only at the required checks (need 'rules', no
call); everything before them is judged as on main.
- A cast reached a judging function past the brand (`as never`, `as unknown as JudgedChecks`), and a
raw read spelled with a const key, Reflect.get or a structural alias went unseen. The scan now flags
any type assertion into JudgedChecks outside check-source.ts, those raw-read spellings, and any src/
import from test/.
- githubCheckApps built a {}, so a check GitHub's rules pin under the name __proto__ lost its pin.
It now has no prototype.
- The claim that another source's non-pass always blocks a pinned check is corrected: pinning never
turns a check that isn't a pass, as judged by name, into a pass. The CHANGELOG names the window
before GitHub's rules first answer.
Test (fails without): test/main-watch.test.ts (a direct push and a bot's merge while the rules are
unknown are unaccounted), test/required-check-source.test.ts (the cast and raw-read fixtures; the
ruleset's __proto__ pin).
Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
Round 3: changes at 5c4ef10Each claim is labelled measured or inferred.
Verification (measured):
#803 and #794 have merged this head (signed off, no rewrite), and both build. |
Evaluator verdict: CHANGES NEEDED (round 3)I reviewed head Two things block this round:
Everything round 2 asked for is in place. Round-2 required changes
Option (c) and the pins (measured,
|
| Fixture | Compiles | Raw-read hits | Cast hits |
|---|---|---|---|
n1 (backlog as any).checks(sha) passed to readiness |
yes | 0 | 0 |
n2 const b: any = backlog; b.checks(sha) passed to readiness |
yes | 0 | 0 |
n3 structural alias through a parameter: g = (b: { checks(s): Promise<any> }) => b.checks(sha); g(backlog) |
yes | 0 | 0 |
n4 const raw = (await backlog.checks(sha)) as never, then checks: raw |
yes | 1 | 0 |
n5 other GitHub data as never into a const, then checks: raw |
yes | 0 | 0 |
n6 other data through JSON.parse (type any), no cast at all |
yes | 0 | 0 |
n7 a generic id<JudgedChecks>(x) whose body is x as T |
yes | 0 | 0 |
n8 a type predicate (x): x is JudgedChecks => true |
yes | 0 | 0 |
n9 Object.entries(backlog), the member called through as any |
yes | 0 | 0 |
n10 backlog as unknown as { checks(s): Promise<any> } |
yes | 0 | 0 |
- The cast scan: it looks at the assertion's target type and its contextual type. A cast to
any, toneverin a const with no annotation, or to a type parameter has neither. - The raw-read scan finds nothing in n1, n2, n3, n9 or n10:
- an
anyreceiver has no member to trace; as unknown ashides theBacklogtype before the second cast;- a parameter alias isn't a variable declaration or an assertion.
- an
- So two claims are false (measured):
- "What this can never do: Let a raw GitHub checks answer reach a judging function and compile";
- the scan "lists every read of
Backlog.checks… insrc/".
- Cost of the gap (inferred):
src/is protected, so the owner reviews any such change. These are deliberate spellings, not slips. The judging still needs the brand on the normal path.
Fail-without (measured)
- Method:
- a scratch worktree at
d6148530, withtest/required-check-source.test.ts,test/main-watch.test.tsandtest/raw-check-reads.tsfrom5c4ef109; npx tsc, thennode --import ./dist/test/isolate.js --test dist/test/required-check-source.test.js dist/test/main-watch.test.js.
- a scratch worktree at
- Result:
# tests 63 # pass 61 # fail 2, matching the builder.main watch (#727): while GitHub's rules have never answered, a direct push and a bot's merge are still unaccounted:expected: 1 actual: 0.a check GitHub's ruleset pins under the name __proto__ keeps its pin: failed.
- On the head:
- every compiled test except
mcp-*andplugin-fetchgives# tests 3539 # pass 3482 # fail 0 # skipped 57; npm run typecheck: exit 0;npm run check:denylist:denylist: 1283 files clean.
- every compiled test except
Rule 10, hygiene, protection, CI, merge (measured)
- Rule 10: the only removed test lines this round are two
githubCheckAppsasserts, now made over a spread copy, plus import lines. Nothing is weakened. - DCO:
node scripts/check-dco.mjs origin/main 5c4ef109givesDCO: all commits signed off. - No attribution: no Co-Authored-By or "Generated with" in the commits or the body.
- Protection: the title has
[PROTECTED: owner merges], the body has**Risk class:** protected, the PR carriesneeds-owner, and it has real "What this can never do" and "Compromised component" sections. One "can never do" line is false (change 2). - Adversarial pass: present, with the round-3 attacks.
- CI on
5c4ef109(gh pr checks 788): 19 pass, 7 skipping, 0 failing. - Merges (
git merge-tree --write-tree, pairwise):- clean with origin/main
3d24fc34; - clean with Engine updater counts a required check only from its pinned workflow or App [PROTECTED: owner merges] #789, doctor prints a paste-ready required_check_sources block; a new project's config starts pinned (after #788) [PROTECTED: owner merges] #803 and Pin this repo's required checks to the workflow files that post them (after #788 and the engine upgrade) [PROTECTED: owner merges] #794;
- doctor prints a paste-ready required_check_sources block; a new project's config starts pinned (after #788) [PROTECTED: owner merges] #803 with Pin this repo's required checks to the workflow files that post them (after #788 and the engine upgrade) [PROTECTED: owner merges] #794, and each of them with Engine updater counts a required check only from its pinned workflow or App [PROTECTED: owner merges] #789, also clean.
- A single merge of all four plus main wasn't built (rule 12). It is inferred clean: every pair is clean, and doctor prints a paste-ready required_check_sources block; a new project's config starts pinned (after #788) [PROTECTED: owner merges] #803 and Pin this repo's required checks to the workflow files that post them (after #788 and the engine upgrade) [PROTECTED: owner merges] #794 touch disjoint files on top of this branch.
- clean with origin/main
Required changes
-
Owner decision (2026-10-19, relayed by the orchestrator): any same-name non-pass, from any source, blocks a pinned check.
- What blocks: any App, any workflow, and any run of that name on the commit, including a cancelled duplicate.
- Where: readiness, the merge flow (the check of main after a merge, fix-forward, the PR batch's fast checks, the docs-only hold) and the main watch.
- The owner accepts the cost: this is stricter than main, so another App or a cancelled duplicate run can hold a pinned PR.
- Unpinned checks stay exactly as main (option c).
- Statuses: say whether a same-name commit status that isn't a pass also blocks. "Any source" and principle 1 suggest yes. Test whichever you choose.
- Tests to add, each failing on
5c4ef109:- readiness: App 1111's pinned pass listed first, then App 2222's same-name failure: not ready (
test fail). Today it givespass. - readiness: a pinned workflow's pass plus a cancelled duplicate run of the same name, in another suite on the same head: not ready.
- main watch: a newer pinned pass with an older same-name failure from another App: unaccounted. Today it is
accounted. - main watch: a pinned pass plus a cancelled duplicate: unaccounted.
- one merge-flow site, through
pinnedChecks: the same pair blocks. - option (c) is unchanged: an unpinned check equals the by-name judgement over subsets and orders, as the 256/256 fixture shows.
- readiness: App 1111's pinned pass listed first, then App 2222's same-name failure: not ready (
- Update the claims: the body (intent, "How it works", "What this can never do", "Compromised component: Another App"), both CHANGELOG entries,
docs/design.mdand thesrc/check-source.tscomment.
-
Close the
anyandneverroute into a judging function (principles 4 and 5; the class of round 2's change 2). Extendtest/raw-check-reads.tsso that every n1–n10 fixture above is flagged, and add them as fixture tests. For example:- outside
check-source.ts, flag any expression whose contextual type isJudgedChecksand whose own type isanyornever(n1–n6, and round 2'sas never); - flag a type predicate or a type argument that names
JudgedChecks(n7, n8); - for
rawCheckReads, flag any value whose type has a raw member when it is asserted, annotated or passed as a type that isn't a raw owner,anyandunknownincluded (n1, n2, n3, n9, n10).
If any part stays open, narrow the two claims to what is caught.
- outside
For the owner (inferred; not a required change)
On this repo, new-deps.yml runs on edited with cancel-in-progress: true. On all four PR heads here it ran twice on the same commit (measured, filter=all), and filter=latest, which the engine reads, keeps both (measured on bdb92eac). So once new-deps is pinned (#794), a PR body edited while new-deps runs leaves a cancelled duplicate on the head. Under change 1, that holds the PR until the next push. This is the cost you accepted; it names where it will show first.
… source's non-pass blocks it The owner's rule (2026-10-19), in readiness, the merge flow and the main watch: - For a pinned check's own source (its App or workflow), only the latest run on the head counts, by the runs' own order: the later workflow run, then the later check run (a re-run attempt). A later run of that source supersedes its earlier or cancelled ones. A run for another head is not the pinned source, so it never supersedes. - Any check of the same name from any other source that isn't a pass blocks it: another App, another workflow, a run for another head, a commit status that isn't success. The main watch now reads the statuses of a pinned merge for that. - Unpinned checks stay exactly as main (option c). Also, from review round 3: the scans in test/raw-check-reads.ts now flag any `any` or `never` value, generic return or type predicate where JudgedChecks is expected, and a Backlog put into an `any`, `unknown`, empty or structural type (a cast, an annotation or an argument). The claims say this is a lint, not a run-time guarantee. Test (fails without): test/required-check-source.test.ts (cancelled-then-passed and failed-then-passed are ready; a passed pin plus another App's failure is not; run order, not list order; another head never supersedes; a status blocks; n1 to n10), test/main-watch.test.ts (the same cases at merge time), test/required-check-source-coordinator.test.ts (the merge flow's checks). Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
Round 4: changes at e8107ebEach claim is labelled measured or inferred.
Verification (measured):
|
Evaluator verdict: CHANGES NEEDED (round 4)I reviewed head Where it stands:
The refined rule (measured,
|
| Case | Readiness | Main watch |
|---|---|---|
| Owner 1: cancelled, then passed, same pinned workflow | ready | accounted |
| Owner 2: failed, then passed, same pinned workflow | ready | accounted |
| Owner 3: pinned pass + another App's same-name failure (workflow pin; App pin) | ci: fail |
ci fail |
| Passed, then a later own run failed / still running | fail / pending | fail / pending |
| A later run of the pinned file for another head: its pass over the head's failure; its failure under the head's pass | fail; fail | fail; fail |
| Re-run attempt (same workflow run, newer check run): pass over fail; fail over pass | ready; fail | accounted; fail |
Same-name status failure / error / pending / success beside a pinned pass |
fail / fail / pending / ready | same |
Foreign App posts a newer same-name pass over the pinned failure (workflow pin; App pin; with a forged /actions/runs/100/job/999 page) |
fail | fail |
An API-made Actions check in the pinned run's suite (/runs/<id> page), newer pass |
fail | fail |
| Another workflow file's newer pass over the pinned file's failure (workflow pin) | fail | fail |
GitHub's rules pin ci to the Actions App (15368): evil.yml's later pass over ci.yml's failure |
ready | accounted |
ci.yml run 100 re-run (attempt 2, the newest check run) fails after run 200 passed |
ready | accounted |
- Order independence: every case gave a single verdict across all list orders, so the ordering is by the runs' own ids, never list order.
- Unpinned equals main: 326 of 326 subsets and orders of a 5-check pool (two Apps, a running Actions job, two statuses) give the same readiness through
PinnedChecks.ofas by name. The builder's 65-subset test also passes. - The supersede comes only from the pinned source (inferred, code):
- an App pin needs
c.app === app_id; - a workflow pin needs the suite's workflow run (Actions slug,
check_suite_id), that path, repo and head, and the run's own/job/<id>page. - A foreign App's run therefore lands in "other" and can only block. The one exception is the Actions-App pin (change 2).
- an App pin needs
JudgedChecks: lint or run-time brand (item 2)
The lint on the 16 reviewer fixtures (measured, scan.mjs): all 16 are caught (round 2's six and round 3's n1 to n10). src/ itself is clean.
My own 8 fixtures, each a src/ file fed as data (measured): every one compiles, and every one gets 0 raw-read hits and 0 cast hits.
| # | Route into a judging call |
|---|---|
| e1 | // @ts-expect-error over other data (this repo has no ts-comment ban) |
| e2 | Object.assign(otherData, judged) (type CommitCheck[] & JudgedChecks) |
| e3 | a generic returning T & {}, with T inferred from context |
| e4 | an overload whose implementation returns any |
| e5 | a judged array mutated through a CommitCheck[] cast |
| e8 | Array.prototype.push.apply(judged, otherData) |
| e6 | withTestRuns(judged, new Set(), otherData) |
| e7 | new PinnedChecks({ backlog: { checks: () => otherData }, … }).of(sha) with no checkApps (so judged by name) |
Is a run-time brand cheap? Yes (measured, sync-brand.mjs):
PinnedChecks.ofis async (AsyncFunction), but the brand is made by the private, syncjudged(), and the exportedwithTestRunsis sync.- A test helper needs no async construction. One top-level
awaitintest/helpers.tsmakesEMPTY = await new PinnedChecks({ backlog: { checks: async () => [] }, … }).of(sha). ThenasJudged = (c) => withTestRuns(EMPTY, new Set(), [...c])stays sync with the same signature. On a 3-check fixture it gave the same items in the same order, and the samereadiness. - So no test call site changes. The package is ESM (
"type": "module", NodeNext, ES2023), so top-level await works (inferred). - Where the check goes: every judging path ends in
readinessorrequiredOutcomes(src/pr-watch.ts), sinceprReadiness, fix-forward's three calls,mayMergeand the merge flow's fast-check, hold and flaky-gate calls all go through them (measured,grep), pluswithTestRuns'githubinput. That makes it about three checks insrc/.
A brand stops e1 to e5, e8 and all 16 reviewer routes at run time (inferred). e6 and e7 go through the real makers, so they need a narrow lint or a named claim (change 1).
Fail-without (measured)
- Method: a scratch worktree at
5c4ef109, withtest/required-check-source.test.ts,test/main-watch.test.tsandtest/required-check-source-coordinator.test.tsfrome8107eb4.test/raw-check-reads.tsstays at5c4ef109's, since the lint is the fix for the n-fixtures. Thennpx tscandnode --import ./dist/test/isolate.js --teston the three files. - Result:
# tests 71 # pass 67 # fail 4, matching the builder.- main watch:
expected: 'accounted' actual: 'ci fail'; - merge flow: the pinned App's later run didn't supersede;
- readiness:
'test pass', expected'test fail'; caught: n1 backlog as any, read, pass.
- main watch:
- On the head:
- every compiled test except
mcp-*andplugin-fetch:# tests 3543 # pass 3486 # fail 0 # skipped 57; npm run typecheck: exit 0;npm run check:denylist:denylist: 1283 files clean.
- every compiled test except
Size (item 5, measured: git diff --shortstat origin/main...e8107eb4)
- Totals: +1,317/−48 in 27 files.
src/is +449 in 14 files. 316 of those lines are incheck-source.ts(209, all new) andmain-watch.ts(98); the rest are call-site edits of 2 to 49 lines.- Tests are +858, including the 182-line type-flow lint.
- Judgement: reviewable as one PR. It is one feature with one module and narrow call sites. The hardest part to review is the lint. Change 1 makes that lint secondary to a few lines of run-time check, which helps.
Usual checks (measured)
- Rule 10: this round's removed test lines are the lint's old
hidesRawandjudgedCastsbodies, and the new ones keep each old condition as a branch. Nothing is weakened. - DCO:
node scripts/check-dco.mjs origin/main e8107eb4givesDCO: all commits signed off. - Attribution: no Co-Authored-By or "Generated with" in the commits or the body.
- Protection: the title has
[PROTECTED: owner merges], the body has**Risk class:** protected, the PR carriesneeds-owner, and it has real "What this can never do" and "Compromised component" sections. Some of their lines are false (change 3). - Adversarial pass: present, with round-4 rows.
- Rule A: the tests use stand-ins and temp dirs (inferred, as round 3).
- CI on
e8107eb4(gh pr checks 788): 19 pass, 7 skipping, 0 failing. - Merges (
git merge-tree --write-tree, pairwise): clean with origin/main178c44e6, Engine updater counts a required check only from its pinned workflow or App [PROTECTED: owner merges] #789, doctor prints a paste-ready required_check_sources block; a new project's config starts pinned (after #788) [PROTECTED: owner merges] #803 and Pin this repo's required checks to the workflow files that post them (after #788 and the engine upgrade) [PROTECTED: owner merges] #794, and every pair among those is clean. A single merge of all four plus main wasn't built (rule 12); it is inferred clean.
Required changes
- Add the run-time brand (principle 4; cheap, measured above). Keep the lint (rule 10).
- A module-private
WeakSetinsrc/check-source.ts, filled only byjudged(), which also freezes the array. readiness,requiredOutcomesandwithTestRuns(on itsgithubinput) throw on any array not in it. That fails closed; judging is on no OFF path (inferred; confirm it in the Adversarial pass).test/helpers.tsasJudgedgoes throughwithTestRunsover one top-level-awaitedEMPTY, so no call site changes.- Tests:
- a cast,
anyor@ts-expect-errorvalue atreadinessand atrequiredOutcomesthrows; - pushing onto a judged array throws.
- a cast,
- For e6 and e7: add a callers allowlist to the lint (
withTestRunsonly inci-none.ts,new PinnedChecksonly inmerge-flow.ts), or name both routes in the claims. - Fix the false claims:
check-source.ts:20says "which only PinnedChecks.of makes";- the body's "can never … compile without a cast or an
any" is false: e1 and e2 have neither.
- A module-private
- An Actions-App pin must not supersede across workflow files (principle 1; the owner's rule says "the latest run of that workflow").
- Measured: with GitHub's rules pinning
cito App 15368,ci.ymlfailed (run 100) and a PR-addedevil.ymlpassed (run 101) gives ready and accounted. So doesci.ymlstill running withevil.ymlpassed. - This also makes the body's "Compromised component: Agent … can't make its PR ready with a workflow it adds" false for such pins.
- Guess: GitHub's protection UI often records the Actions App as a check's source, so this may be common on existing installs.
- Fix: under an App pin, take the latest run per workflow file (an Actions run whose workflow can't be read is its own group), and every group's latest must pass. Do this in
pinnedChecksandpinnedAt. - Tests: the two cases above, each failing on
e8107eb4.
- Measured: with GitHub's rules pinning
- Correct the "never looser" claims (principle 5).
- Measured (
vs-main.mjs): the owner's case 1 is looser than main. Withcilisted cancelled first, then passed, main's by-name readiness givesci: cancelledand this head gives ready. - False now:
- the body's "It is never looser" and "Turn a check that isn't a pass into a pass";
- CHANGELOG's "stricter than judging by name";
src/check-source.ts:12's "Pinning only ever turns a match into missing".
- Say instead: a pinned source's later run on the head supersedes its own earlier failed or cancelled runs, which judging by name could block. That is the owner's decision (2026-10-19), and it is the only way it is looser.
- Measured (
For the owner (a genuinely new trade-off; not a required change)
Which run counts as "latest"? The specified order is workflow run, then check run.
- The case: run 200 passed, then someone re-ran the older run 100, and attempt 2 (the newest check run) failed. That gives ready / accounted (measured above).
- Fail-closed alternative: require the latest by run order and the newest check run of that source to pass.
- Re-running needs write access (inferred).
Resolved from round 3
Round 3's owner note on new-deps' cancelled duplicates no longer applies. A same-workflow cancelled run is now superseded by the later run (owner case 1, measured above).
… re-run blocks (#727) - JudgedChecks is now checked at run time: judged() records each array it makes in a module-private WeakSet and freezes it and its checks, and readiness, requiredOutcomes and withTestRuns throw on an array not made there or since changed. Test fixtures go through the real makers (test/helpers.ts asJudged, one top-level await). - The lint flags any src/ use of the two makers (withTestRuns, PinnedChecks) outside ci-none.ts and merge-flow.ts, including through an alias, a namespace import or a dynamic import (makerUses). - A pinned source's own runs are grouped by workflow file (one group for an App that isn't Actions). In every group both the latest run and the newest check run must pass, so one workflow file never supersedes another under an Actions-App pin, and a re-run of an older run that failed after a newer run passed blocks. Same in the main watch. - CHANGELOG and design.md name the one loosening the owner decided: a pinned file's later run supersedes its own earlier failed or cancelled runs. Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
|
Round 5 at bdfcc29 (one signed-off commit on e8107eb, no rewrite).
Fail-without (measured): the new test files on e8107eb gave
With e8107eb's own scans, e6 and e7 got 0 hits each. After (measured):
The body has a new Adversarial pass, round 5 section: other files, other Apps, statuses, re-runs, other commits and API-created runs. |
Evaluator verdict: CHANGES NEEDED (round 5)I reviewed head Where it stands:
1. The owner's rule and the tightening (measured,
|
| Case | Readiness | Main watch |
|---|---|---|
| Owner 1 / 2: cancelled or failed, then passed (workflow pin) | ready | accounted |
| Owner 3: pinned pass + another App's same-name failure (workflow, App and Actions-App pins) | ci: fail |
ci fail |
Actions-App pin: ci.yml failed or still running, then a PR-added evil.yml passed later |
fail / pending | fail / pending |
Actions-App pin: ci.yml failed in the newest run, evil.yml passed; ci.yml passed, evil.yml failed |
fail; fail | fail; fail |
| Actions-App pin: both files passed | ready | accounted |
| Re-run of the older run fails or is still running after the newer run passed (workflow pin; Actions-App pin) | fail / pending | fail / pending |
| Re-run of the older run passes; a normal re-run of the latest run passes (older run cancelled) (workflow pin) | ready | accounted |
Round 4's set: a later own run failed or is running; another head's run; a re-run attempt; statuses failure/error/pending/success; a foreign App's newer pass (with a forged job page); an API-made run in the suite; another file under a workflow pin |
as round 4 | as round 4 |
Owner 1 / 2 under an App pin (app_id: 1111) |
ready | ci fail, unaccounted |
| Owner 1 / 2 under an Actions-App pin (GitHub's rules name 15368) | ready | ci fail, unaccounted |
| A re-run attempt of the latest run passes over its failure (App pin; Actions-App pin) | ready | ci fail, unaccounted |
| Actions-App pin: a normal re-run of the latest run passes, older run cancelled | ready | ci fail, unaccounted |
App pin: the App fails ci, then passes another check (lint) later |
ci: fail |
ci fail |
- Unpinned equals by-name: 326 of 326 subsets and orders of a 5-check pool (measured).
- Does the newest-check-run rule block a legitimately green PR? Not in readiness. A normal re-run of the latest run that passes is ready (table).
- Real data: I took 120 commits of this repo where a workflow ran twice or more (read-only
gh api,realids.mjs). They give 439 (file, check name) pairs over two or more runs. In 0 of them is the newest check run outside the latest workflow run (measured). So on this repo the rule has never blocked a green repeated run.
- Real data: I took 120 commits of this repo where a workflow ran twice or more (read-only
Why the main watch differs (inferred, code; confirmed by the measurement above):
- The watch asks for
sourcesonly when some required name has a workflow pin (main-watch.ts:335). - Under an App pin,
f.sourcesstays undefined. Every check run has a suite, soworkflowOf(:337) returnsnull, andpinnedAtmakes each run its ownunread <id>group (:229-233).- The main watch reads
filter=all(main-watch-api.ts:53), so a failed first attempt is there too. - Each own run must then pass, and no later run supersedes anything.
- The main watch reads
- Consequences:
- An unaccounted merge "trips (stop, alert)" and opens a revert PR (
main-watch.ts:848-855). The harness merges such a PR on readiness, and then its own main watch trips on it. - Existing installs: GitHub's rules naming the Actions App (or any App) is enough to hit this, with no config change. Main judged these merges by name and accounted them, so the upgrade adds false trips (loss of function).
- Guess: GitHub's protection UI often records "GitHub Actions" as a check's source.
- Inferred: this repo isn't hit today. Its ruleset names no
integration_id, andbranches/mainprotection lists no checks (gh api).
- An unaccounted merge "trips (stop, alert)" and opens a revert PR (
- Also inferred: even when
sourcesis read because another name has a workflow pin, a run from an App that isn't Actions has a suite missing fromsources. So it still goes tounread(:337), not to the oneappgroup thatownVerdictuses. - The new test
main watch (#727): an Actions-App pin never lets one workflow file supersede anothersetss.gh.sources, but the watcher never asks for them under that pin (inferred). Itsci failcases pass because each run is its own group, not because of per-file grouping.
2. The brand
Forgery attempts (measured, brand-788.mjs, against a real judged array whose ci failed):
| Attempt | Result |
|---|---|
structuredClone (changed or not), a Proxy over it, Object.create(it), Reflect.construct with its constructor, a subclass instance, my own array frozen (elements frozen too) |
each throws checks to judge must come from PinnedChecks.of |
Object.setPrototypeOf, Object.defineProperty(…, 'find'), real[0] = …, length = 0 |
the forging step itself throws TypeError (frozen) |
A second module instance (check-source.js?second) |
its arrays throw at the first instance's readiness (own WeakSet) |
withTestRuns(real, {ci}, a pass) (e6), a fresh PinnedChecks over a stand-in backlog (e7) |
READY: through a maker, as the body says |
headChecks (ci-none.ts) with project: { ci: 'none' }, a tests mapping a tier to ci, and made-up testrun.finished events |
READY: a third maker, callable from any src/ file |
e7 with the backlog returning a Proxy whose map returns another Proxy |
READY, and judged() recorded the attacker's Proxy itself. That is still e7's route (not required, see below) |
WeakSet.prototype.has patched to return true |
READY. In-process code can always patch builtins (not required) |
My 8 fixtures:
- At run time (measured,
require-788.mjs):- e1, e2, e3 and e4 throw;
- e5 and e8 throw
TypeError: Cannot add property 1, object is not extensible; - e6 and e7 are READY, through the makers.
- Through the lints (measured,
scan.mjs): all 8 compile and are flagged bymakerUses.- e6 and e7 are flagged for the maker call.
- e1 to e5 and e8 are flagged only because the fixture imports both makers. The brand is what stops them.
- Real callers (measured): every compiled test except
mcp-*andplugin-fetchgives# tests 3548 # pass 3491 # fail 0 # skipped 57.- No
src/code mutates a judged array or check (inferred,grepfor assignments). mayMergeand fix-forward go throughrequiredOutcomes(inferred, code).- So the brand changes no real caller's behaviour.
- No
Is makerUses complete? (measured, scan.mjs; virtual src/ files passed as data)
| Route | Hits |
|---|---|
m1 renamed import; m2 named re-export in a barrel; m7 literal import('./check-source.js'); m8 destructured from a dynamic import of a barrel |
3, 3, 2, 1 (caught) |
m3 export * from './check-source.js' barrel + namespace import + computed key |
0 |
m4 export * as cs from './check-source.js' barrel + computed key |
0 |
m5 dynamic import of an export * barrel + computed key |
0 |
m6 createRequire(import.meta.url)('./check-' + 'source.js') + computed key |
0. At run time require(esm) returns the same instance (withTestRuns === import's: true), and withTestRuns through it gives READY |
m10 Object.values over an export * barrel's namespace |
0 |
m9 headChecks over made-up events (above) |
0 |
src/ has no export * and no createRequire today (grep; testrun/affected.ts only names them in a regex). So these are routes a future change could take, which is what the lint is for.
Fail-without (measured)
- Method: a scratch worktree at
e8107eb4, withtest/main-watch.test.ts,test/required-check-source.test.ts,test/helpers.tsandtest/raw-check-reads.tsfrombdfcc294. Thennpm run buildandnode --import ./dist/test/isolate.js --teston the first two. - Result:
# tests 72 # pass 68 # fail 4, matching the builder:- main watch, Actions-App pin:
+ 'accounted' - 'ci fail'; - readiness, Actions-App pin:
+ 'test pass' - 'test fail'; - "latest" fails closed:
+ 'test pass' - 'test fail'; - the brand:
Missing expected exception: a cast.
- main watch, Actions-App pin:
- Adding
required-check-source-coordinator.test.jsgives 76 / 72 / 4, the same four failures. - On the head: suite as above;
npm run typecheckexit 0;npm run check:denylist:denylist: 1283 files clean.
Usual checks (measured)
- Rule 10: this round's removed test lines are three import lines, now with the new names.
asJudgedwent from a cast to the real makers (stronger). Nothing is weakened. - DCO:
node scripts/check-dco.mjs origin/main bdfcc294:DCO: all commits signed off. - Attribution: none in the commits or the body.
- Protection:
[PROTECTED: owner merges],**Risk class:** protected,needs-owner, and real "What this can never do" and "Compromised component" sections. Two of the body's lines are false (changes 1 and 2). - Adversarial pass: present, with round-5 rows.
- Rule A: stand-ins and temp dirs (inferred, unchanged).
- CI on
bdfcc294(gh pr checks 788): 19 pass, 7 skipping, 0 failing. - Merges (
git merge-tree --write-tree):- clean with origin/main
178c44e6, Engine updater counts a required check only from its pinned workflow or App [PROTECTED: owner merges] #789, doctor prints a paste-ready required_check_sources block; a new project's config starts pinned (after #788) [PROTECTED: owner merges] #803 and Pin this repo's required checks to the workflow files that post them (after #788 and the engine upgrade) [PROTECTED: owner merges] #794, and every pair among them is clean; - with Release notes v0.5.3: every install-affecting change since v0.5.2, the v0.5.2 NVML correction, notes the release keeps [PROTECTED: owner merges] #809's head
456d4682: a conflict inCHANGELOG.mdonly, in two hunks.
- clean with origin/main
- The Release notes v0.5.3: every install-affecting change since v0.5.2, the v0.5.2 NVML correction, notes the release keeps [PROTECTED: owner merges] #809 conflict is mechanical. Release notes v0.5.3: every install-affecting change since v0.5.2, the v0.5.2 NVML correction, notes the release keeps [PROTECTED: owner merges] #809 rewrites the v0.5.3 notes and renumbers neighbouring entries (Remove the browser owner terminal, its sudoers rule and root helper (deprecate, then remove; keep the loopback break-glass and the device gate) #742→Remove the browser owner terminal, its sudoers rule and root helper; a dashboards.sh rerun deletes them; the device gate stays (#742) [PROTECTED: owner merges] #752, Remove the desktop app, its required CI check and its Tauri lockfile (deprecate, then remove) #744→Updater: a retired required check (desktop) that no longer runs doesn't block updates; ships before the desktop job's removal (#744) [PROTECTED: owner merges] #755, Updater: keep the root updater itself in step with the engine it installs, so updater fixes need no root rerun of updates.sh #760→Updater: replace itself from a verified release tag after a healthy install, never with an older one [PROTECTED: owner merges] #761, Updater: refuse a release whose signed tag object names a different version than its ref (downgrade by re-pushing an old signed tag) #764→Updater: a release installs only under its own signed name, as the one tag object verified, with replace refs off [PROTECTED: owner merges] #773). Take Release notes v0.5.3: every install-affecting change since v0.5.2, the v0.5.2 NVML correction, notes the release keeps [PROTECTED: owner merges] #809's side, and insert this PR's two Required checks: pin each one to the App or workflow that posts it, before governance mode (today a same-name check from anywhere counts) #727 entries:
- "Required checks can be pinned…" in the notes list;
- "A pinned required check is matched by its source…" under guardrail changes.
- Don't keep this side's neighbouring lines: they carry main's old numbers.
Required changes
- Main watch under an App pin (principle 1 as the owner's rule defines it, and principle 3's spirit for existing installs; this is a loss of function, not a loosening).
- Read the workflow runs when any required name is pinned to the Actions App (configured
app_id: 15368, or GitHub's rules naming 15368), as readiness and Engine updater counts a required check only from its pinned workflow or App [PROTECTED: owner merges] #789 do. - In
judge()'sworkflowOf:- a run whose App isn't Actions has no workflow (the one
appgroup, asownVerdictdoes by slug); - only an Actions run whose suite isn't in
sourcesisunread.
- a run whose App isn't Actions has no workflow (the one
- Tests that drive the watcher's real need loop (no
sourceshanded in when the watcher wouldn't ask). Under an Actions-App pin and under anapp_idpin:- owner cases 1 and 2 are accounted;
- a passing re-run attempt of the latest run is accounted;
evil.ymlpassing afterci.ymlfailed or while it runs is not accounted.- Each must fail on
bdfcc294.
- Correct the body's "The main watch does the same at merge time (
pinnedAt), with the same groups" (principle 5).
- Read the workflow runs when any required name is pinned to the Actions App (configured
- Close
makerUses' gaps, or narrow its claims (principles 4 and 5). The body says "Uses through an alias or re-export count, and so do a namespace import and a dynamic import ofcheck-source.ts". m3 to m6 and m10 aren't caught.- Flag in
src/:- any
export */export * aswhose module resolves (through the program, not a regex) tosrc/check-source.ts; - any namespace or dynamic import of a module that re-exports it;
- any
require/createRequire(...)(...)call.
- any
headChecks: add it to the makers (allowed only insrc/coordinator/merge-flow.ts), or stop it taking a caller'stestsand events. Name it incheck-source.ts's header, which says "The two makers are PinnedChecks.of and withTestRuns".- Fixtures: m3 to m6, m9 and m10 as data, each flagged.
- Flag in
Not required (noted)
judged()maps the input with its own.map, so a stand-in backlog that returns aProxygets its ownProxybranded (measured above).- This is e7's route, which the lint covers.
Array.from(c, …)would always make a plain array.
- Patching
WeakSet.prototype.hasorObject.isFrozendefeats any in-process brand. That's out of scope.
For the owner (a genuinely new trade-off; not a required change)
A pin to an App that isn't Actions is one group (the builder's "what's left").
- Measured (readiness): the App posts
cifailed (check run 11), thencipassed (12). That is ready, as the owner's rule says for an App source. A different check name from the same App never supersedes:cifailed, thenlintpassed, staysci: fail. - The hole: a third-party CI App that runs pipeline config from the PR's own head (as CircleCI or Buildkite can; guess for any given App) lets a PR add a second pipeline posting the same name. Its later pass then supersedes the real pipeline's failure. That is the
evil.ymlcase, for an App the engine can't split by file. - Fail-closed alternative: under a non-Actions App pin, every run of the name must pass (no supersede). That gives up the owner's cases 1 and 2 for that App.
- doctor prints a paste-ready required_check_sources block; a new project's config starts pinned (after #788) [PROTECTED: owner merges] #803's block proposes such an
app_idpin only when no workflow file posts the name and exactly one provable App does.
… App pins fail closed; one maker (#727) - Readiness and the main watch judge a pinned name through one function, pinnedVerdict, over the same facts: GitHub's latest check run per check suite and name, the workflow run behind each Actions suite, and the latest status per context. The watch builds them as they stood at merge time (checksAt), and now reads the workflow runs whenever a pinned name has an Actions run, so under an Actions-App pin (or an App pin) the owner's cases and a normal re-run are no longer split into one group per run. pinnedAt is gone. - An Actions run is told by its App id (15368) on both sides. - A pin to an App that isn't Actions fails closed: every one of that App's check runs of the name on the head must pass (a re-run GitHub lists in place of a failure, in the same suite, is the one there is). - The deciding non-pass is the same whatever the list order (a finished one before a running one, then the highest id). - PinnedChecks is the one maker: under No CI it reads the event log's test runs itself. withTestRuns and ci-none's headChecks, which took a caller's checks or events, are gone; asJudged in the tests goes through PinnedChecks.of. - makerUses finds uses through the compiler's module resolution: barrels, export *, export * as, namespace and dynamic imports of a module that exposes the maker, unresolvable dynamic imports, require and createRequire. Fixtures for each route. - Readiness reads every page of the combined status, and the main watch every page of check runs and statuses; later pages are marked extra and only pinned names are judged with them (by name, exactly as before). - One table test: 22 cases under a workflow pin, an Actions-App pin and a non-Actions App pin, through readiness's real requests and the watch's own request loop; both give the expected verdict. Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
…788 brings in #788 makes src/backlog/types.ts, a module the guard already reaches, import its types from src/check-source.ts, so the guard's import tree is the 56 pinned modules plus that one (57). #788 lists it in src/hook-imports.ts, and the merge keeps it there; this adds it to the pinned tree in test/review.test.ts. One addition, nothing removed. src/bundle.ts stays out: the guard still doesn't reach it. Test (fails without): the static pin test, "the agents' tool guard's import tree is exactly its pinned modules", and "every module in the agents' tool guard's import tree is self-modification, L3", both named src/check-source.ts as outside the pin on the merged tree before this change. Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
No conflicts. GUARD_TREE keeps #839's list, which now has src/check-source.ts, and this PR's src/os/own-env.ts: 58 modules. Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
…s pinned-source verdict The owner's decision on #834 x #788, option (c). The dashboard makes no check read: on a stop the default branch caused, Clear and resume records a request (merge.resume_requested, with an acknowledgement if typed) and answers 202 with the rule and the coordinator's last measurement, or that there is none. The coordinator answers the request in watchMerges: it measures main's tip with headChecksOf(sha, requiredChecks()) and requiredOutcomes (JudgedChecks, so a pinned check from another source is missing), records each look (merge.main_measured), and resumes only if green or on a recorded acknowledgement. A request for an older stop, or a stop that changes while main is measured, resumes nothing; a pending request is measured again every RESUME_MEASURE_MS. Tests: green only from an unpinned same-name check does not resume, green from the pinned source does, a request not yet measured stays stopped, and the dashboard's backlog sees no check read. Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
…in-required-checks.md Main's CHANGELOG.md holds released sections only (#870). The two bullets this PR added are the fragment's, byte for byte, under the same headings; CHANGELOG.md is main's exactly. Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
This PR's only note was its edit of #788's Upgrading bullet (the paste-ready block). With notes in fragments (#870), that edit is made in changelog.d/788-pin-required-checks.md, byte for byte as before; CHANGELOG.md is #788's exactly, and this PR adds no fragment of its own (it adds no note line of its own). Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
…loads 57 modules Since #870, release notes live in changelog.d/, one file per PR, and CHANGELOG.md holds only released sections. This PR's one note, under "Guardrail changes: what each one does", moves to changelog.d/839-guard-loads-only-its-modules.md word for word, with one change the stack-delta review asked for: the guard loads 57 modules, not 56. Stacked on #788, whose src/check-source.ts is in the guard's tree through src/backlog/types.ts, the pinned tree is 57 modules. Test (fails without): test/changelog-fragments.test.ts and test/changelog.test.ts read every fragment; the note itself changes no behaviour. Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
The combined check of 2026-10-20 (main f9422c4, clean dist with build:web) measured the queue in loop order: #881, #788, #803, #789, #839, #844, #834 and #878 merged give a 1,931,687-byte tarball, under 1,950,000; adding #769 gives 1,951,238, over by 1,238 (the full queue is 1,960,374). Per the owner's practice the raise goes on the PR that crosses, at the new size plus 5%, rounded up to 10,000: 1,951,238 * 1.05 = 2,048,800, so 2,050,000. The unpacked budget stays at 6,400,000. Part of #726. Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
doctor prints a paste-ready required_check_sources block; a new project's config starts pinned (after #788) [PROTECTED: owner merges]
Intent: a required check can be pinned to its source: the GitHub App that posts it, or the Actions workflow file that runs it. For a workflow pin, the run must be in this repo and for the very commit judged, and the check run must be one of that run's own jobs: its page is the run's page plus
/job/<id>, over https on the run's host.pinnedVerdict) over the same facts.GET /branches/<b>answered that way means classic protection names no App, so readiness and the main watch give main's verdict.Risk class: protected.
src/**is engine;src/backlog/**,src/config/schema.ts,src/pr-watch.ts, the merge path andsrc/hook-imports.tsare self-modification;docs/design.mdis engine-guards. The owner merges.Closes #727
How it works
config.yamlrequired_check_sources: { <name>: { app_id: N } | { workflow: .github/workflows/<file>.yml } }. It is a separate key, so the dashboard'srequired_checkseditor, which writes a list of names, can't drop a pin. A key__proto__is refused.app_id, fromGET /branches/{b}) or a ruleset (integration_id, fromGET /rules/branches/{b}) names for it.-1, null or absent means any source; two different Apps for one name means none. The answer is cached for 10 minutes, and an error keeps the last answer. Never answered: unknown, so an unpinned check is missing.src/check-source.ts):app.id.path, the project's repo andhead_shaequal to the commit. The check run'shtml_urlmust be that run'shtml_urlplus/job/<its id>, over https on that host (isJobOfRun).pinnedVerdict(src/check-source.ts) is the one judgement of a pinned name.ownVerdictgroups them by workflow file (file <path>). An Actions run whose workflow run wasn't read is a group of its own. A non-Actions App is oneappgroup.latestOwn: workflow run id, then check run id) and the newest (highest check run id) must pass.appgroup: every run must pass.pinnedChecks, fromPinnedChecks.of) and the main watch (judge()) both call it.filter=latest; the watch reduces itsfilter=alllist the same way), the workflow run behind each Actions suite, and the latest status per context.checksAt). It reads the workflow runs whenever a pinned name has an Actions run, as readiness does.pinnedAtis gone.total_countsays.extra, and only a pinned name is judged with them.PinnedChecks.of(sha, { judging })passespinnedto the backlog, andjudge()asks for runs and statuses withpinned.GitHubBacklog.checksreads exactly as main: 9 pages of check runs judged as read, the combined status's first page (a 403 or 404: none), and no workflow runs.GitHubMainWatchApireads one page each, as main.ci-metrics, which callscheckRunswith no option, is back to main's one page as well". That was false.ci-metricsreads through its ownGitHubCiMetricsApi(src/github/ci-metrics-api.ts), which this PR has never changed, so it never read more than one page here (the reviewer, measured).TooManyPages).pinnedAt), with the same groups". That was false.PinnedChecks.githubApps()isnull, so an unpinned check is missing. The main watch passesrulesUnknownintojudge(), which waits only at the required checks; everything before them is judged as on main.checkAppsstill reads the rules when the branch read is refused for permission or 404, then throwsBranchDeniedcarrying what the rules name. A rate limit or a 5xx throws as before.PinnedCheckskeeps that apart from an answer (asked again after 10 minutes).githubAppsFor(judging, configured)returns the rules' answer only when no judged name is pinned by config.yaml or those rules; otherwisenull, as before.PinnedChecks.of) and the main watch (pins.github(judgedBy, sources), wired inmerge-flow.ts) both ask through it.judged()records every array it makes in a module-privateWeakSetand freezes the array and each check.readinessandrequiredOutcomescallassertJudgedfirst. It throws on an array not made incheck-source.ts, or one no longer frozen. A cast, anany,JSON.parsedata, a@ts-expect-errorvalue,Object.assignor a copy all throw. A push or a changed check throwsTypeError.judged()copies withArray.from, so it always makes a plain array.mayMergeand fix-forward'schecksreachrequiredOutcomes(inferred, from code).test/helpers.tsasJudgedisPinnedChecks.ofover a stand-in backlog (now async).PinnedChecksis the only maker.local: the live config and the coordinator's log), in place of any check of their names.withTestRunsand ci-none'sheadChecks, which took a caller's checks or events, are gone. Neither can be imported (tests).git diff --stat bdfcc294 961ecbad): about 70 changed lines insrc/(check-source.ts,ci-none.ts,merge-flow.ts) and about 115 in tests. The tests' share is mostlyasJudgedbecoming async.new PinnedChecksover a stand-in backlog (e7).PinnedChecksbuild its own backlog (from the instance's repo and token) would take it off the coordinator's backlog. Every coordinator test that feeds checks through aFileBacklogwould then need a GitHub stand-in.makerUsesflags these routes toPinnedChecksinsrc/outsideMAKERS_ALLOWED(merge-flow.ts), through the compiler's own symbols and module resolution:export *orexport * as, or a namespace or dynamic import, of any module that exposes it;require()call;createRequirenamed as such.headChecks) no longer compile.createRequirereached by a computed key onnode:module;process.getBuiltinModule;new Function;new (this.pinned.constructor)(standIn)in aMergeFlowsubclass.GitHubBacklogisn't a one-line fix.PinnedCheckstakes the coordinator'sFileBacklog(no GitHub: judged by name) and the tests' stand-ins, so it would need the backlog-building refactor above. Not done.makerUsesflags "everysrc/use ofPinnedChecks" and "anyrequire()orcreateRequire". Both were too broad.test/raw-check-reads.ts, fixtures as data):judgedCastsflags, outsidecheck-source.ts:JudgedChecks;anyorneverwhereJudgedChecksis expected (JSON.parsedata,(b as any).checks(…), anas neverconst);JudgedChecks.testImportsflags anysrc/import fromtest/.rawCheckReadscatches a raw read however spelled: aconstkey,Reflect.get, a structural alias. It also catches aBacklogput intoany,unknown, an empty type or a structural type, by cast, annotation or argument.src/itself is clean.githubCheckAppsbuilds its map with no prototype, so a check GitHub's rules pin under the name__proto__keeps its pin.readiness,requiredOutcomes,mayMerge,prReadinessand fix-forward'scheckstakeJudgedChecks, a branded type that onlyPinnedChecks.ofmakes (under No CI with the event log's own test runs in it).test/raw-check-reads.ts, TypeScript's checker, input as data, Tests: every lint and guard takes its input as data, so its fail-without is a fixture test and never a planted violation #771 shape) lists every read ofBacklog.checksorMainWatchApi.checkRunsinsrc/.requiredAtreads the pins committed at a merge's first parent.judge()judges by name, then pinned names by source.required check sourceswarning naming each unpinned required check and how to pin it.governanceRefusalis Governance mode: the engine runs only its evaluator, merge policy and locks in front of PRs other agents or people open, with no agents of its own; external reviews and CI are policy inputs, never the decision #704's gate. Gating PRs nobody here wrote is Governance mode: the engine runs only its evaluator, merge policy and locks in front of PRs other agents or people open, with no agents of its own; external reviews and CI are policy inputs, never the decision #704's job.Exemptions (decision guide 9)
RAW_READS_ALLOWED. The scan is new in this PR, so no existing check is loosened.src/check-source.ts:PinnedChecks.ofis the one place that makesJudgedChecks.src/main-watch.ts: it reads runs at a merge and judges them by pins injudge().src/canary.ts: the updater keeps or rolls back an install on its verdict, but a pass needs the PR merged by the canary instance (whose merge judged its checks by their pins) and every check green, so a spoofed check can only turn a pass into a fail (inferred).judgedCasts) exemptssrc/check-source.ts(JUDGED_CASTS_ALLOWED):PinnedChecks.ofmakesJudgedChecks, so the brand is its to give. The scan is new, so this loosens no existing check.canary.ts,check-source.ts).makerUses, round 5) exemptscheck-source.tsitself andMAKERS_ALLOWED, which since round 6 is one entry. The scan is new, so this loosens no existing check.src/coordinator/merge-flow.tsmay constructPinnedChecks. It is the one place the coordinator reads GitHub's checks, and it builds the maker over the coordinator's own backlog, config and event log.ci-none.ts's round-5 exemption is gone withwithTestRuns.test/helpers.tsasJudgedis not a cast. Since round 6 it isPinnedChecks.ofover a stand-in backlog, so fixtures pass the run-time brand as real checks do.required-check-source.test.tsandrequired-check-source-coordinator.test.ts. Each now also asserts that a same-suite re-run passes. That is the coordinator's change 2, a tightening.headCheckscall, becauseheadChecksno longer exists. The makers test now asserts that it can't be imported.Existing installs (owner rule: nothing reopens a route)
config.yamlthrough a PR. doctor prints a paste-ready required_check_sources block; a new project's config starts pinned (after #788) [PROTECTED: owner merges] #803 makes that a paste.Adversarial pass (decision guide)
Fixtures and stand-ins only, on this branch's build (
scratchpad/work/issue-727/adversarial.mjsand the tests).test pass, ready; nowtest fail, not readypinnedChecks, main watch)/o/r/runs/<id>)isJobOfRun)https://evil.example/o/r/actions/runs/700/job/1), or overhttp:isJobOfRunreturnedtrue; nowfalse, the check is missinghtml_urlplus/job/<id>, over https on that hostnullcrashed (Cannot convert undefined or null to object)githubApps()returnsnulluntil GitHub answers__proto__in config.yamlpinOf null, the spoof was kept: judged by name); now the config is refusedconstructorpinOf {"app_id":1111}, spoof dropped)Backlog.checksanswer passed to the 6 judging callsJudgedChecks).bind, destructuring,?.,['checks'], other receivers,MainWatchApi.checkRuns)writers required_checks daily_budget_usd workers_count workers_model ci(fromsrc/config-change.tsWHERE); norequired_check_sourcesrequired_check_sourcesmakes the config invalid, and the last good one keeps running, as for any invalid config (inferred, code)expected: 1 actual: 0); now each is unaccounted, as on main, and only the checks waitjudge(…, { rulesUnknown }))raw as neveroras unknown as JudgedChecksat a judging call; a structural alias; aconstcomputed key;Reflect.getjudgedCastsand the reads byrawCheckReads(reviewer's fixtures: 6 of 6).src/has no cast intoJudgedChecksoutsidecheck-source.tsand no import fromtest/__proto____proto__gavepass; nowmissingObject.create(null))test pass/ accounted; nowtest fail/ unaccounted (the owner's rule)ci failfor cancelled-then-passed (it took the latest started)as any, ananyvar, a structural parameter,as neverconsts,JSON.parse, a generic identity, a type predicate,Object.entries,as unknown as {…})caught: n1 …failed); now each is (the reviewer's script: raw-read or cast hits ≥ 1 for all 16)testto App 15368),ci.ymlfailed and a PR-addedevil.ymlpassed laterready/ main watch accounted (the later Actions run superseded the other file); nowtest fail/ci fail, unaccountedownVerdict,pinnedAtgroups)ci.ymlstill running, or the PR-addedevil.ymlfailingfail/ unaccounted. The owner's cases (cancelled or failed, then a later run passed) stay ready / accountedJSON.parse, a@ts-expect-errorvalue,Object.assign, a copy,withTestRunsover raw data, a straywithTestRunscall, a straynew PinnedChecks)makerUsesflags them, as it does an alias, a namespace import with a computed key and a dynamic import. e8107eb's scans gave 0 hits for both e6 and e7app_id: 1111) on the owner's cases 1 and 2, a normal re-run, a re-run still runningfailwhere readiness saidpassorpending: every run its own group); now 22 of 22 agreepinnedVerdict,checksAt, sources read for any Actions run of a pinned name)fail. A same-suite re-run that passed is readyexport *with a computed key,export * as, dynamic import of a barrel,createRequire, literal dynamic import, destructured dynamic import,headChecksover made-up events,Object.values)makerUsesmissed m3 to m6, m9 and m10; now each is flagged, or no longer compiles (m9, e6)JudgedChecks, and the (a) refusal test became (c) as the owner decided. In round 5,asJudgedstopped being a cast. Round 6: two App-pin assertions now expect the fail-closed result, and oneheadChecksline went withheadChecks(Exemptions above)Adversarial pass, round 5: what else could make a pinned check pass
These are the ways a pinned check could pass from something other than its pinned workflow file's own latest run on the head. Each is checked on bdfcc29.
pinnedChecks) and in the main watch (pinnedVerdictsince round 6; tests) (measured).fromPinnedSourceneeds the workflow run'shead_shato equal the commit. A run for another head is another source, so it never supersedes, and its failure blocks (tests, round 4) (measured).isJobOfRun: the run's own page plus/job/<id>, https, same host), so it is another source (tests, round 1) (measured on fixtures);/<repo>/runs/<id>), and the check fails closed either way.pull_requestruns use from the PR. That is Governance mode: the engine runs only its evaluator, merge policy and locks in front of PRs other agents or people open, with no agents of its own; external reviews and CI are policy inputs, never the decision #704's to never auto-merge, and agents can't push it.Adversarial pass, round 6: every place that judges a pinned check gives the same answer
grepoversrc/forpinOf,pinnedVerdict,pinnedChecks,readinessandrequiredOutcomes):prReadiness);mayMerge, and the merge flow's own fast check);PinnedChecks.of's checks (pinnedChecks→pinnedVerdict) and judge them withrequiredOutcomes. The main watch callspinnedVerdictitself, overchecksAt's facts at merge time.src/canary.ts) needs every check green by name, which is stricter (Exemptions).test/main-watch.test.ts, measured): 22 rows, each the same facts under a workflow pin, an Actions-App pin and a non-Actions App pin.PinnedChecks.ofoverGitHubBacklog's real requests.filter=latest. The watch readsfilter=alland keeps the newest check run per suite started by merge time.latestis the newest per check suite and name is measured by the reviewer: on 17 of 17 of this repo's commits it is exactly the highest id per (suite, name) offilter=all.ci fail(the reviewer, measured).Adversarial pass, round 7: every GitHub read on the readiness and main-watch paths
A truncated or refused read can only end as wait (readiness: not ready this tick; the watch: asks again), not ready, or unaccounted. It never ends as ready or accounted.
Readiness (
src/backlog/github.ts, throughPinnedChecks.of):total_countfilter=latest, 100 a page)total_countno longer trusted)ciwas READY)The main watch (
src/github/main-watch-api.ts,judge()):filter=all, 100 a page)TooManyPages: a merge with a pinned required check is unaccounted (a trip, an alert); by name judged as before (961ecba threw every step and never judged, holding every later merge)null)pins.github)Measured (
test/pin-pages.test.tsandtest/main-watch.test.ts, each failing on 961ecba):TooManyPagescarrying the 1000 read);Left, inferred:
Adversarial pass, round 8: a config with only bare names, against main
Method (measured,
scratchpad/work/issue-727/bare8.mjs):GitHubBacklog.checksandreadinesson main, againstPinnedChecks.of(required: ['ci'], nothing pinned, GitHub's rules naming no App) andreadinesshere.GitHubMainWatchApiandjudge(), driven through the need loop for a harness merge ofci.cipassing or failing on page 1, failing on page 10, or absent;Results:
The other judging paths: fix-forward, the PR batch's fast checks,
mayMerge, the ruleset hold and the flaky gate all read through the samePinnedChecks.of(headChecksOf) (inferred, code; the round-6 table of judging places). Itsrequiredcovers:So a pin on any name those paths judge turns the pinned reads on, and with none they are main's.
Left, justified:
backlog.checkswith no option, so it reads as main (inferred).Round 9: two fail-open edges closed
checkAppsnow reads every rules page.ci: missing, where ff971e5 was READY). From the start, the rules stay unknown.required(), such as fix-forward's episode names (the reviewer, measured at the API level).Round 10 replaced that, which went past its brief: the pinned reads turn on when a name being judged is pinned, and fix-forward passes its episode's names (PinnedChecks.ofalso asks for the pinned reads whenever any pin exists.checks(sha, judging)).PinnedChecks.ofandrequiredOutcomes(['ci']), the judgement fix-forward makes, withrequired: ['lint']:ci.ymlfailed andevil.ymlpassed givesfail(ff971e5:pass);ownVerdictdirectly.Adversarial pass, round 9: every read whose error becomes a reassuring answer
Each place where a GitHub error turns into "none", "no pin", "no rulesets" or "no statuses", and why it is safe (inferred, code, with the tests named):
firstStatusPage)statusPages)GET /rules/branches/{b}(checkApps)GET /branches/{b}(checkApps)BranchDenied: for a judgement with no pinned name, classic protection names no App; with one, unknown (unpinned checks missing)workflowRuns, readiness;runSources, the watch)workflowisnullpins.github, fromPinnedChecks.githubAppsFor)checkAppsprotection(), the watch's modeNo other read on these paths turns an error into an answer. Every other one throws: readiness isn't judged, and the watch asks again.
Round 10: the pinned reads only for a judged name's pin
PinnedChecks.of(sha, { judging })makes the pinned reads only when a name being judged is pinned, or GitHub's rules haven't answered.merge-flow.tsno longer adds every config.yaml pin.checks(sha, judging)). The fast checks, the ruleset hold and the flaky gate pass theirs.ownVerdict's round-9 change stays: an Actions run with no workflow read waits, never passes.test/pin-pages.test.ts):ciwithotherpinned by the ruleset or config.yaml is READY with main's reads, on a page-1 status 403, 301 status contexts and 1,001 check runs;judging: ['ci']) still blocksci.ymlfailed thenevil.ymlpassed, and withoutjudgingthe unread runs wait;Adversarial pass, round 10: a bare-names config with one unrelated pin, against main
Method (measured):
bare8.mjs, main 178c44e built in a scratch worktree at that commit, and the same 288 scenarios as round 8.other, a check nothing requires, is pinned to App 1111, by GitHub's ruleset or by config.yaml.PinnedChecks.of(sha, { judging: ['ci'] }), against main'srequiredOutcomesover the backlog.bare8-789.cjs(135 scenarios), with the branch rules orupdates.jsonpinningother.Results:
otherin the rulesetotherin config.yamlPackage size (the owner's practice, 2026-10-19, as revised late that day)
This PR doesn't change
PACKAGE_BUDGET: it fits main's{ size: 1_950_000, unpackedSize: 6_060_000 }(#823), sotest/package-files.test.tsis main's file exactly.npm pack --dry-run --jsonafter a cleannpm ci,npm run buildandnpm run build:web, in throwaway worktrees under.worktrees/, since removed):dist/src/check-source.js, new: +16,061.backlog/github.js+7,602,main-watch.js+7,104,github/main-watch-api.js+3,840,config/schema.js+1,514,coordinator/merge-flow.js+1,460.pr-watch.js+334,canary.js+333,doctor.js+285,hook-imports.js+27,coordinator/fix-forward.js+21,ci-none.js−460.templates/project/config.yaml+439 (the commentedrequired_check_sourceskey).check-source.js; no sourcemap, test or fixture is packed.How it was verified
test/pin-pages.test.tsandtest/main-watch.test.tsfrom ebc4998, in a throwaway worktree. cf32770 has nogithubAppsFor, so its copy of the watch test is wired as cf32770'smerge-flow.tswires the watch (github: () => pinned.githubApps()). Result:# tests 59 # pass 57 # fail 2:expected: 'ready' actual: 'ci: missing; lint: missing';expected: 'accounted' actual: 'no verdict'.# tests 11 # pass 11 # fail 0,main-watch.test.js# tests 48 # pass 48 # fail 0. The broad suite (every compiled test exceptmcp-*andplugin-fetch, with a scratch HOME) gave# tests 3869 # pass 3812 # fail 0 # skipped 57. Typecheck is clean,denylist: 1313 files clean, and every commit is signed off.CHANGELOG.mdwas the only conflict. I took main's side (-X theirs), sogit diff origin/main -- CHANGELOG.mdis empty.CHANGELOG.mdhas the samegit patch-idbefore and after (8e19724c).changelog.d/788-pin-required-checks.md(eabf661, 7 lines), byte for byte under the same headings (grep -Fx).changelog,changelog-fragments,changelog-fragments-selfcheckandpin-pagesgave 50 of 50, and typecheck is clean.## v0.5.4 (unreleased)section: one under "Upgrading (from v0.5.3)", one under "Guardrail changes: what each one does", ended by an empty### Guardrail changes.git diff origin/main -- CHANGELOG.mdshows only those added lines, andchangelog,package-filesandpin-pagesgave 28 of 28 (measured).test/package-files.test.ts, resolved to main's file (above). The pin tests withpackage-filesgave# tests 98 # pass 98 # fail 0(measured).expected 0, actual 1(made ready); the main watch accounted it; the merge flow[];expected 1, actual 0); main watchci missing, deploy missing;# tests 59 # pass 53 # fail 6:'test pass', expected'test missing');githubApps()gave{}, expectednull;__proto__pin was accepted (true !== false);null;urlon the run.# tests 63 # pass 61 # fail 2:expected: 1 actual: 0;__proto__pin:'__proto__ pass', expectedmissing.# tests 71 # pass 67 # fail 4:ci fail, expectedaccounted;'test pass', expected'test fail';test/main-watch.test.ts,test/required-check-source.test.ts,test/helpers.tsandtest/raw-check-reads.tsfrom bdfcc29. It was removed with--force.# tests 72 # pass 68 # fail 4:expected: 'ci fail' actual: 'accounted';ci.ymlfailed andevil.ymlpassed, and it was not blocked;Missing expected exception: a cast.test/. With e8107eb's ownraw-check-reads.ts,makerUsesdoesn't exist, and its scans give e6 and e7 zero hits each.main-watch,required-check-source,required-check-source-coordinator,ci-none,pr-watch,pr-batch,live-config,helpersandraw-check-reads. It was removed with--force.# tests 85 # pass 76 # fail 9:expected 'pass' actual 'missing', with the stand-in sending no App slug, which bdfcc29 needed);withTestRunsstill exported);localoption).test/pin-pages.test.tsandtest/main-watch.test.tsfrom ed9eb36. 961ecba has noTooManyPages, so the scratch copies declared a stand-in class of that name; 961ecba's watch rethrows it like any error. The worktree was removed with--force.# tests 50 # pass 45 # fail 5:actual: 'ready';Missing expected rejection;Missing expected rejection;Error;more than 10 pageson every step instead of judging.mcp-*andplugin-fetch(never run on this machine), with a scratch HOME:# tests 3554 # pass 3497 # fail 0 # skipped 57.npm run typecheckis clean,denylist: 1284 files clean, and DCO givesall commits signed off.cl-check.mjs: no v0.5.3 bullet dropped, and the trailing heading is present.# tests 3867 # pass 3810 # fail 0 # skipped 57. Typecheck is clean,denylist: 1313 files clean, andDCO: all commits signed off.test/pin-pages.test.tsfrom 46f6f02:# tests 10 # pass 8 # fail 2. The failures are the unrelated-pin readiness test (it waited), and the unread runs read forrequiredalone (they gavefailfrom the pinned reads, notpending).# tests 10 # pass 6 # fail 4. The rules-paging test fails there, as do round 9's three.--force.test/pin-pages.test.tsfrom 71c83cd, gave# tests 8 # pass 5 # fail 3. The three failures are the rate-limited rules read, the unread Actions-App case throughPinnedChecks.of, andownVerdict. The worktree was removed with--force.CHANGELOG.mdwas resolved mechanically: main's side, with this PR's two Required checks: pin each one to the App or workflow that posts it, before governance mode (today a same-name check from anywhere counts) #727 bullets, one in Upgrading and one under "Guardrail changes: what each one does", above the trailing### Guardrail changesheading.cl-check.mjs,scripts/lib/changelog.mjs'ssectionForin memory): no v0.5.3 bullet of either side dropped (this side's Intake: GitHub-sourced bug reports get the checked contract (repro: true); coordinator and checkDraft agree on which done_when block counts #643 is main's intake: a GitHub-sourced report is worked from its checked draft; one done_when block per body [PROTECTED: owner merges] #671), and the trailing heading is present.changelog,no-clock-helpers,reviewand the pin tests passed on the merge.src/: nothing patches a built-in prototype) then flagged this PR's round-2 schema refusal, which named the prototype's key in a string. The refusal is now found by parsing: anyrequired_check_sourceskey the record would drop is refused. The round-2 test passes unchanged.Invalid input: expected record, received string, the same as before (the reviewer, measured).test/pin-pages.test.ts,test/main-watch.test.ts,test/required-check-source.test.tsandtest/required-check-source-coordinator.test.tsfrom ff971e5. It was removed with--force.# tests 84 # pass 75 # fail 9:more than 10 pages of check runs … not judged);# tests 3556 # pass 3499 # fail 0 # skipped 57.npm run typecheckis clean,denylist: 1284 files clean, andDCO: all commits signed off. The bare-name comparison with main, re-run on ff971e5, gave 288 of 288.no-clock-helperslint (Tests: no result depends on today's date; five intake tests go red on main ~2026-11-02 (fix before 2026-11-01) #687) onpin-pages.test.ts's rate-limit stand-in. It now uses a fixed reset. In a scratch merge with origin/main, those tests gave 80 of 80 (measured).git diff --shortstat bdfcc294 961ecbad: 495 lines added and 244 removed in 18 files this round.git diff --shortstat 961ecbad ed9eb363(round 7): 217 lines added and 38 removed in 9 files.git diff --shortstat origin/main...ed9eb363: 2,007 lines added and 111 removed in 28 files.git merge-tree):CHANGELOG.mdconflicts with Single machine: policy.yaml is root's, and the engine refuses one that isn't (#726 part 1) [PROTECTED: owner merges] #769 and Dashboard policy changes go through root's helper (set-policy), never a loosening (#726 part 2) [PROTECTED: owner merges] #770, which already conflict with main there.What this can never do
ownVerdict/pinnedVerdictand their tests):readinessorrequiredOutcomeswithout a throw, however it was cast. The run-time brand refuses any array not made incheck-source.ts, or one changed since. The one maker,PinnedChecks, takes no checks, runs or events from a caller.PinnedChecksbuilt over a stand-in backlog outsidemerge-flow.tsis not refused at run time. The lint (makerUses) flags the routes it lists; m11 to m14 are named gaps.withTestRunsandheadCheckstook a caller's checks or events, andmakerUsesmissed m3 to m6, m9 and m10 (the reviewer, measured).any". That was a lint-only claim: a cast or ananycompiled, and only the test-suite scan flagged it.Compromised component
expected 'ci fail', actual 'accounted').pull_requestrun uses the PR's copy. Agents can't push that change, and a foreign PR doing it is Governance mode: the engine runs only its evaluator, merge policy and locks in front of PRs other agents or people open, with no agents of its own; external reviews and CI are policy inputs, never the decision #704's to never auto-merge.required_check_sources(measured, above).required_checks.New dependencies
None.