Skip to content

Engine updater counts a required check only from its pinned workflow or App [PROTECTED: owner merges] - #789

Merged
jayden1711 merged 25 commits into
mainfrom
issue-727-updater-pins
Oct 21, 2026
Merged

jayden1711 merged 25 commits into
mainfrom
issue-727-updater-pins

Conversation

@jayden1711

@jayden1711 jayden1711 commented Oct 19, 2026 •

Copy link
Copy Markdown
Owner

Intent: the root updater (scripts/machine/worklane-update.cjs, evaluateChecks) counts a pinned required check only from its source: the workflow file or GitHub App pinned for its name.

  • The engine's own checks are pinned by the updater itself, so no machine needs a manual step.
  • A check nothing pins is judged by name, exactly as before. This is the owner's choice for existing installs, option (c), 2026-10-19.
  • The owner's rule (2026-10-19) for a pinned required check, as tightened in rounds 5 and 6:
    • its pinned source's Actions runs on the commit are grouped by workflow file (a pin to GitHub Actions' App reads the workflow runs too, so it is grouped by file);
    • round 8, the owner's decision: under a pin to an App that isn't Actions, that App's latest check run of the name on the commit (the highest id) decides, in whatever check suite. A later pipeline or re-run of that App supersedes its earlier ones, so the owner's cases 1 and 2 hold for such an App across suites. A later failing run refuses, and another source's failure still refuses.
      • Correction: round 6 had every one of that App's runs, as listed, pass (a failure in another suite refused). That was the coordinator's tightening, true through a8211ed's parent b3dae4a and now replaced.
      • Through 457e89b the round-6 text also said only that a re-run in the same suite replaces a failure;
    • in every group the latest run (the later workflow run, then the later check run, as a re-run attempt is) and the newest check run must both pass (ownVerdict). So a later run of a file supersedes that file's earlier failed or cancelled runs, one file never supersedes another, and a re-run of an older run that failed after a newer run passed refuses;
    • every run of its name from any other source must be a pass, and a commit status of its name that isn't success refuses it (pending waits; new in round 5);
    • a run for another commit is never its source.
  • The one loosening, which the owner decided: a pinned file's later run on the commit supersedes its own earlier failed or cancelled runs, which judging by name would have refused.
  • Unpinned checks, and every other name, are judged as before: the latest run of each name must be green.
  • The owner's decision (a), 2026-10-19, as in Required checks count only from the App or workflow pinned for their name [PROTECTED: owner merges] #788: with no required check pinned (by updates.json, ENGINE_CHECK_SOURCES or what GitHub's rules name), a branch or rules read GitHub refuses for permission (a 403 that isn't a rate limit), or answers 404, means "no App named". So a bare-names install installs as on main.
    • A 5xx or a rate limit (primary or secondary) still waits, because it is transient.
    • With a required check pinned, any refusal waits, as before.
    • realDeps gains fetchRead, which tells a permission refusal from a transient one as the engine's classifyGitHubError does: it takes the body as JSON when it parses, else as text, as the engine's req does (round 10; measured: 0 of 704 cases differ from the engine).
      • Correction: at bced758 a body that wasn't JSON (or a JSON string) gave no message. So a 403 whose rate limit showed only in its text, with no x-ratelimit-remaining: 0, was read as a permission refusal, and a bare install installed where the engine waits (the reviewer, measured: 10 of 924 cases; my grid, 9 of 704 on 0b6e7a9).
    • The rules read pages as the engine's checkApps does (round 10): 100 a page, the next page while one is full, up to 10. A later page refused, not a list, or past the cap leaves the rules unread, so it waits. Decision (a) applies to page 1 only.
      • Correction: through 0b6e7a9 the updater read one page with no per_page, so with page 1 full a ruleset's pin on page 2 went unread, and a bare install installed (the reviewer, measured). Deps without it (tests' stand-ins) read every refusal as transient.
    • The rules read is covered too, for bare installs only: a token refused the branch is refused the rules the same way, so otherwise the decision would change nothing here (the coordinator accepted this extension, 2026-10-19). In Required checks count only from the App or workflow pinned for their name [PROTECTED: owner merges] #788 a page-1 permission 403 or 404 on the rules is "no rulesets" for every install; here a pinned install still waits (inferred, code).
  • This is the updater part of 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, split from Required checks count only from the App or workflow pinned for their name [PROTECTED: owner merges] #788 (the engine) to keep each PR near 600 lines.

Risk class: protected. scripts/machine/ is a root helper; scripts/ci/updater-drain-root.mjs runs as root in CI. The owner merges.

Part of #727 (#788 closes it).

How it works

  • The pin for a required check, in order:
    1. /etc/worklane/updates.json required_check_sources: {"<name>": {"workflow": ".github/workflows/<file>"}} or {"<name>": {"app_id": N}}. It is root's, and the machine helper writes only enabled and channel. An invalid entry refuses the commit (invalid pin for "x") and never falls back to the name.
    2. Else ENGINE_CHECK_SOURCES: the updater's own fixed list of the engine's CI checks and the workflow file that runs each. It is never read from the commit judged.
    3. Else the App the repo's branch protection (GET /repos/{o}/{r}/branches/{b}) or ruleset (GET /repos/{o}/{r}/rules/branches/{b}) requires. These are read once per run, and only when some required check has no pin from 1 or 2. If GitHub doesn't answer them (a rate limit, say), the commit waits (noAnswer). It is neither judged by name nor remembered as a refusal.
    4. Else judged by name, as before.
  • Matching:
    • An App pin needs the check run's app.id.
    • A workflow pin needs the check run posted by GitHub Actions: app.id 15368 (isActions). Correction: through 457e89b this said app.slug, which round 6 replaced. Its check suite's workflow run (GET /repos/{o}/{r}/actions/runs?head_sha=, one call per commit judged) must have that path, the repo the updater follows and head_sha equal to the commit.
    • New in round 1: the check run's html_url must be /<repo>/actions/runs/<that run's id>/job/<its own id> (isJobOfRun). The run id is kept from the same actions/runs answer, so this needs no extra call.
    • No answer for the workflow runs: wait, never a pass.
  • Statuses (round 5):
    • when any required check is pinned (always on a real machine, through ENGINE_CHECK_SOURCES), the updater reads the commit's combined status (GET /repos/{o}/{r}/commits/{sha}/status?per_page=100, the latest status per context);
    • a same-name status that isn't success refuses a pinned required check, and pending waits;
    • no answer, or still a full page past 10 pages, waits (GitHub gave no answer for its commit statuses).
  • Pagers (round 6): every pager reads the next page while a page is full, whatever total_count says, so a missing or wrong total_count never makes a full page the last. That covers the check runs, the workflow runs (paged since round 6) and the statuses (pagesOf).
    • Correction: through db5ad78, a full page of statuses with no total_count was taken as the last. The reviewer measured a failing status on page 2 installing.
    • Round 7: all three wait when still full past 10 pages, while a required check is pinned.
    • Round 8: checkRunsOf keys on checkPins' pinned, which is true when some required check is pinned. Unread rules count as pinned, and the commit waits anyway.
      • With every required check a bare name, it reads exactly as main: it stops where total_count says, and past the cap it keeps the newest pages. forSha then reads nothing and takes no shortcut.
      • Correction: through 5520710 this said "without pins it keeps main's newest pages", which held only for tests' stand-in deps. On a real install, where pinChecks is always on, a bare-name config waited past 10 pages where main installs (the reviewer, measured).
    • Correction: through 457e89b the check-run pager kept the newest pages while pins were judged. A failing scan of another source on page 11 installed (the reviewer, measured).
  • The read cost (round 6): the workflow runs and statuses aren't read for a commit its check runs already rule out. Its verdict then comes from the check runs alone (hopeless, red, running in forSha). That happens when:
    • a pinned required check has no passing run of its name at all;
    • a check judged by name ended red;
    • a check judged by name is still running.
  • Both decision sites use it: main-channel chooseCommit and the release channel's check of the release's main commit.
  • realDeps sets pinChecks: true, and a test pins that. Stand-in deps in older tests don't set it and judge by name, as before. Two fixtures that spread realDeps (test/update-redact.test.ts and scripts/ci/updater-drain-root.mjs) pin their test check to the App that posts it. In round 5 they also answer the status read with an empty statuses list. Without it, the status read got no answer and the commit waited: the broad suite at 1b515c3 failed the two update-redact tests (measured). Fixed in db5ad78.

Existing machines (owner rule: nothing reopens a route)

  • No manual step, and nothing stops installing (measured and inferred):
    • Every name in UPDATE_DEFAULT_CHECKS is in ENGINE_CHECK_SOURCES, pinned to the workflow file that posts it on main. I measured the mapping on batched main commit 720a807 by joining its check runs to its workflow runs by check_suite_id; the reviewer's run of chooseCommit on that commit's real answers gave CHOSEN.
    • A test (test/update-checks.test.ts) keeps the list in step with the workflows.
    • A check added with --add-check that isn't the engine's is judged by name, as before, until root pins it (measured: the "judged by name, exactly as before" test compares with and without pins).
  • How it reaches a machine: the updates.sh rerun this release already asks for, or the release channel's self-update (Updater: keep the root updater itself in step with the engine it installs, so updater fixes need no root rerun of updates.sh #760) after that.

Package size (the owner's practice, 2026-10-19, as revised late that day)

This PR doesn't change PACKAGE_BUDGET, and its package-size delta is 0 / 0.

  • Measured against main d5674d0 (npm pack --dry-run --json after a clean npm ci, npm run build and npm run build:web in throwaway worktrees, since removed): this branch at 0b6e7a9 packs exactly as main, 1,902,158 / 5,934,411, with the same 512 files.
  • Against main 6e0d7d8 (merged in as 8a417d1): git diff --name-only origin/main HEAD lists CHANGELOG.md, docs/separate-users.md, scripts/ci/updater-drain-root.mjs, scripts/machine/worklane-update.cjs and three tests. None of them is in package.json's files, so the pack is main's (measured: the list; the pack itself is inferred from it).

How it was verified

  • Round 10 (measured):

    • Fail-without, by the new method: the committed test file (its blob matches the head's) against the unchanged code at 0b6e7a9 gave # tests 24 # pass 16 # fail 8 (the reviewer, measured).
      • Two failures show the bugs: the plain-text rate-limited 403 (expected: 'waits' actual: 'installs') and the paging test (no rules URL asked for per_page=100).
      • The other six fail only because the stand-ins now answer only the paged URL (?per_page=100), which the unchanged code doesn't ask for.
      • Correction: this said # tests 24 # pass 22 # fail 2. That came from an earlier version of the test file: written before the fix, so before the stand-ins moved to the paged URL, and before the page-2 pin's expected verdict changed from "refused" to "waits".
    • After, at 5f746c2: update-check-sources, update-checks, engine-update and update-redact gave # tests 75 # pass 74 # fail 0 # skipped 1.
    • Three stand-ins in test/update-check-sources.test.ts now answer the paged URL (?per_page=100); they answered /rules/branches/main before. No assertion was loosened.
    • The engine comparison (classify-cmp.mjs, a scratch script): realDeps.fetchRead against classifyGitHubError, as checkApps reads it, over 704 cases. The cases are 11 statuses × 16 bodies (JSON, text, no message, a JSON string) × 4 header sets. 5f746c2: 0 differ. 0b6e7a9: 9 differ, all a 403 with rate-limit text in a plain-text or JSON-string body.
  • Changelog fragments (Release notes: one changelog.d file per change, moved into the dated section by the changelog step [PROTECTED: owner merges] #870) (measured): origin/main ec3fd42 is merged in as cb50dd5, signed off, with no rebase.

    • The resolution: CHANGELOG.md was the only conflict. I took main's side (-X theirs), so git diff origin/main -- CHANGELOG.md is empty.
    • No other change: this PR's own diff outside CHANGELOG.md has the same git patch-id before and after (d9ff458d).
    • Where the notes live: its two notes are now changelog.d/789-updater-pins.md (f3b6a29, 7 lines), byte for byte under ### After install and ### Guardrail changes: what each one does (grep -Fx).
    • changelog, changelog-fragments, changelog-fragments-selfcheck and update-check-sources gave 63 of 63, and typecheck is clean.
  • origin/main 71316eb (v0.5.3 released, Release v0.5.3: changelog and version [PROTECTED: owner merges] #828) merged in (8e899bc; a signed-off merge, no rewrite, no conflicts). Git put 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 into the released v0.5.3 notes, so 56d2191 moves them, unchanged, to a new ## v0.5.4 (unreleased) section: one under "After install", one under "Guardrail changes: what each one does", ended by an empty ### Guardrail changes. git diff origin/main -- CHANGELOG.md shows only those added lines, and changelog, package-files and update-check-sources gave 41 of 41 (measured). When this and Required checks count only from the App or workflow pinned for their name [PROTECTED: owner merges] #788 meet in CHANGELOG.md, both PRs' 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 stay.

  • origin/main 6e0d7d8 merged in (8a417d1; a signed-off merge, no rewrite, no conflicts). cl-check.mjs: no v0.5.3 bullet dropped (ours 47, theirs 48, merged 50), and the trailing heading is present. Typecheck is clean, and denylist: 1310 files clean.

  • The owner's decision (a) (measured):

    • The test drives chooseCommit through realDeps' own reads over a stand-in GitHub (globalThis.fetch), with ci and lint passing from App 2222. A bare install and one with ci pinned in updates.json are each run under a permission 403, a 404, a 500 and a rate-limited 403. Each case runs twice: once with the rules read answered, once with it refused the same way.
      • Bare: installs for the 403 and the 404, waits for the 500 and the rate limit.
      • Pinned: waits in every case.
    • Fail-without on 12148c1, with test/update-check-sources.test.ts from bced758, in a throwaway worktree (since removed): # tests 23 # pass 22 # fail 1. The failure is the new test's first bare row: expected: 'installs' actual: 'waits'.
    • After, at bced758: update-check-sources gave # tests 23 # pass 23 # fail 0.
  • origin/main d5674d0 merged in (0b6e7a9; a signed-off merge, no rewrite, no conflicts).

    • Measured with cl-check.mjs: no v0.5.3 bullet was dropped (ours 42, theirs 45, merged 47), and the trailing heading is present.
    • Typecheck is clean, and denylist: 1309 files clean.
  • Round 1 fail-without (measured):

    • Setup: a scratch worktree at the old head 0ae5196, with the new test/update-check-sources.test.ts checked out from dcf0577 (npx tsc: 0 errors). Run as node --import ./dist/test/isolate.js --test dist/test/update-check-sources.test.js, with a scratch HOME and state dirs and a fake claude. The worktree was removed with --force.
    • Result: # tests 9 # pass 6 # fail 3:
      • a same-name check from another workflow, ...: case a check run in the pinned suite that is not one of its jobs, true !== false (the commit was chosen);
      • a required check nothing pins ... judged by name, exactly as before: it was refused instead (chosen: false);
      • the branch's rules unread (a rate limit, say): ... waits: no wait (actual: '').
    • The fail-to-pass test passes on the old head too, as it should: the all-green loop over the latest run of every name was already there.
  • Adversarial-pass fail-without (measured): the new test file against dcf0577 gave # tests 10 # pass 9 # fail 1. The the right path on another host case: true !== false, the commit was chosen.

  • Round 5 fail-without (measured).

    • Setup: a scratch worktree at 71b2f2c with the new test/update-check-sources.test.ts from 1b515c3. It was removed with --force.
    • Result: # tests 16 # pass 13 # fail 3:
      • an Actions-App pin, ci.yml failed and evil.yml passed later: actual: 'installs';
      • run 200 passed, then run 100 was re-run and failed: actual: 'installs';
      • a same-name failing commit status: actual: 'installs'.
    • The owner's failed-then-passed fixture now gives each check run a realistic id: the later run's job has the higher id, as GitHub numbers them. The list order is still reversed, and the assertion (installs) is unchanged. With ids following list order, that fixture was a failed re-run of the older run, which now refuses.
  • Round 8 fail-without (measured).

    • Setup: a scratch worktree at 5520710, with test/update-check-sources.test.ts from a8211ed. It was removed with --force.
    • Result: # tests 22 # pass 20 # fail 2:
      • the bare custom with 11 full pages wasn't chosen (it waited);
      • the non-Actions App's later suite didn't supersede its earlier failure (round 6's fail-closed rule).
    • The new status-refusal test passes there too: a refused status read already waited.
  • The owner's status decision in the updater: with a required check pinned, realDeps.fetchJson gives null for any HTTP error. So a refused status read, a page-1 403 or 404 included, gives no answer and the commit waits (measured, test). With none pinned, statuses aren't read, as on main.

  • Round 7 fail-without (measured): a scratch worktree at 457e89b, with test/update-check-sources.test.ts from 5520710, gave # tests 20 # pass 19 # fail 1. The failure: past page 10, actual: 'installs'. The worktree was removed with --force.

  • Round 6 fail-without (measured).

    • Setup: a scratch worktree at db5ad78 with the new test/update-check-sources.test.ts from 457e89b. It was removed with --force.
    • Result: # tests 19 # pass 16 # fail 3:
      • the pagers: the failing status on page 2 installed (chosen was the commit);
      • the read cost: /actions/runs was read for a commit missing a pinned check;
      • the non-Actions App pin: a later suite's pass superseded the first suite's failure.
  • The updater's unauthenticated REST reads, per run (measured).

    • Method: scratchpad/work/issue-727/calls2.cjs runs the whole run() on the main channel, compare included, at each revision. The file is loaded from git show; work is the working tree. It uses the 15 UPDATE_DEFAULT_CHECKS, pins on as realDeps runs them, a stand-in GitHub, and a busy machine.
    Case (one run) main 178c44e round 5 db5ad78 this head 457e89b
    waiting on a green tip 2 (compare, check runs) 4 (+ workflow runs, status) 4
    first sight of 20 red commits (a pinned check failed in its only run) 21 61 21
    first sight of 20 idle commits (the slow checks never ran) 21 61 21
    20 commits red on a check that isn't required 21 61 21
    20 commits, a check that isn't required still running 3 h after landing 21 61 21
    20 commits, scan passed in gitleaks.yml's first run and failed in its later run (worst case) 21 61 61
    • Per hour (inferred, from the counts and the 10-minute timer, OnUnitActiveSec=10min, scripts/setup/updates.sh:184):
      • a machine waiting on a green tip makes 24 reads an hour, where main makes 12;
      • an installed tip makes none (:1528, the reviewer).
    • The first sight of a streak: 21 reads for 20 red or idle commits, as on main.
    • A single run can still use the whole 60-an-hour budget: 61 reads, when 20 commits each pass every check by name and still fail a pinned check (a pinned file's later run failed). Past GitHub's 60 an hour per IP, each read gets no answer and the update waits, for up to an hour. It never installs wrongly. The CHANGELOG says this too. This cost is the owner's trade-off.
  • Round 4 fail-without (measured): the new test file against 703db4e gave # tests 13 # pass 11 # fail 2:

    • cancelled-then-passed expected installs, got required check "test (ubuntu-latest, 22)" ended cancelled;
    • the re-run attempt's newer pass wasn't chosen (expected: true actual: false).
  • Round 3 fail-without (measured): the new test file against a59a426 gave # tests 11 # pass 10 # fail 1. a check the branch's rules pin under the name __proto__ keeps its pin failed with expected: false actual: true.

  • Round 0 fail-without (measured, unchanged): against origin/main's updater, # tests 16 # pass 9 # fail 7. Among them, a spoofed workflow's commit was chosen for install, and the release channel installed on a spoofed scan.

  • After (measured): I ran every compiled test except mcp-* and plugin-fetch, with a scratch HOME: # tests 3532 # pass 3475 # fail 0 # skipped 57 (at a8211ed). At b3dae4a one run failed queue.test (a queued run surviving its session's kill), which this PR doesn't touch; run alone it passed 3 times out of 3 (measured). npm run typecheck is clean, npm run check:denylist gives denylist: 1280 files clean, and node scripts/check-dco.mjs origin/main HEAD gives DCO: all commits signed off.

  • Guess, not measured: a check run made through the API into an Actions suite has a page other than its run's job (/<repo>/runs/<id>). The job-page check fails closed either way.

  • Overlap (measured with git merge-tree):

Adversarial pass (decision guide)

Fixtures and stand-ins only (test/update-check-sources.test.ts).

Attack Principle Result (measured) Fixed
A check run in the pinned workflow's own suite that isn't one of its jobs (page /runs/<id>, or another run's job) 1 was installable on 0ae5196; now refused (hasn't run on this commit from its pinned source) round 1
The right job path on another host, or over http: 1 installable on dcf0577 (the right path on another host: true !== false); now refused yes: the page must be the run's own html_url plus /job/<id>, over https on that host
Another App's newer failing run of a pinned check 3 refused (ended failure) on both heads: the all-green loop over the latest run of every name not needed
Round 3: the branch's rules pin a check named __proto__ 1 on a59a426 the pin was dropped, and another App's run installed (expected: false actual: true); now refused yes (Object.create(null) in ruleApps)
Round 3: an older failing run of a pinned name from another App, under a newer pinned pass 3 was installed (the green loop looked at each name's latest run); now refused (from another source ended failure), by the owner's rule yes (round 4)
Round 4: cancelled-then-passed and failed-then-passed by the same pinned workflow; passed then cancelled in the later run; a re-run attempt; another commit's run 1 the first two install, the third refuses, a re-run's newer attempt counts, another commit's run never supersedes. On 703db4e cancelled-then-passed was refused (ended cancelled) yes
Round 4: an unpinned --add-check name with an older failure from another App 3 installs exactly as without pins (compared) n/a
The branch rules unread (a rate limit) with an unpinned --add-check name 1 was a final refusal cached for an hour; now waits (no answer for the branch's rules) round 1
An invalid updates.json pin 1 refuses (invalid pin for "scan"), never falls back to the name round 1
Turning updates off with an invalid pin and junk in updates.json 2 run returns disabled and makes no GitHub read not needed
Can a coordinator or the dashboard loosen a pin? 6 the machine helper writes only enabled and channel (scripts/machine/worklane-machine.cjs:463,465); updates.json pins are root's not needed
Round 5: an Actions-App pin (rules pin lint to 15368), ci.yml failed and an evil.yml added in the commit passed later 1, 3 on 71b2f2c installed; now refused ("lint" ended failure). ci.yml still running waits; the added file failing refuses; both passing installs yes (ownVerdict per file; the workflow runs read for an Actions-App pin)
Round 5: run 200 passed, then run 100 was re-run and its newest attempt failed (or was cancelled) 1 on 71b2f2c installed; now refused. The owner's cases (cancelled or failed, then run 200 passed) still install yes (the newest check run must pass too)
Round 5: a same-name commit status (failure, error, pending), or no answer for statuses 1, 3 on 71b2f2c installed (statuses weren't read); now failure/error refuse, pending and no answer wait, success and another name's status install yes (combined status read)
A check or test removed or weakened? 7 none. Two fixtures that spread realDeps gained a pin, so they keep testing the same path with pinning on. In round 5 one fixture got realistic check run ids, with its assertion unchanged (above) n/a

Adversarial pass, bced758 (decision (a)) and round 10

  • What I tried at bced758: an unpinned install and a pinned one, each under a permission 403, a 404, a 500 and a rate-limited 403 with JSON bodies, with the rules read answered and refused (measured, tests). It installed only for unpinned installs under the 403 and the 404.
    • What I missed: a refusal whose body isn't JSON. I matched the engine's headers, but not how its req reads the body. The reviewer found 10 of 924 cases where the updater installed and the engine waits.
    • Also missed: the rules read's paging. I extended decision (a) to the rules read without checking that it read every page as the engine does. It read one page, so a pin on page 2 was dropped.
  • Round 10, what I tried (measured):
    • A grid of 704 refusals against the engine's classifier: 0 differ after the fix, and 9 before it.
    • Plain-text bodies for a rate limit, a secondary limit and a permission 403, under an unpinned install and a pinned one.
    • The rules read with page 1 full and then: an empty page 2 (installs, as main); a pin on page 2 (waits for the pinned App); page 2 refused for permission, a 404, a 500, or not a list (each waits); every page full past 10 (waits).
    • Decision (a) on a later page: a page-2 permission 403 waits, because only page 1's refusal means "no rulesets".
  • Left, inferred: a network error on any rules page throws from fetch, as every other read does, so chooseCommit throws and nothing is installed.

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 commit. Each is checked at 1b515c3.

  • Another workflow file:
    • under a workflow pin (the engine's own checks), its run is another source, and a non-pass of it refuses (round 0 tests);
    • under an Actions-App pin, each file is its own group, and every group's latest run must pass (test above). Through 71b2f2c a later file superseded another (measured).
  • Another App: another source. It never satisfies a pin, and a non-pass of it refuses (tests) (measured).
  • Statuses:
    • never a source;
    • through 71b2f2c a failing same-name status was not read at all (measured: installed);
    • now non-success refuses and pending waits.
  • Re-runs:
    • a re-run attempt of the latest run decides (the owner's case);
    • a re-run of an older run that ends worse refuses, because the newest check run must pass (test).
  • Other commits: the workflow run's head_sha must be the commit. A later run of the file for another commit never supersedes, and its failure refuses (round 4 tests).
  • API-created check runs:
    • one made into the pinned run's suite isn't one of that run's jobs (isJobOfRun), so it is another source (round 1 tests);
    • its real page is a guess, and the check fails closed either way.
  • What is left:
    • the same-file supersede, which the owner decided;
    • a change to a pinned workflow file on main, as today;
    • under a non-Actions App pin, a later run of that App supersedes its earlier ones, in any suite: the owner's decision (round 8). Round 6 had refused a failure in another suite; the owner loosened that. A later failing run, or another source's failure, still refuses (tests).

Adversarial pass, round 6: every pager, a full page with a missing or wrong total_count

Each pager in the updater was checked (measured, tests):

  • Check runs (checkRunsOf): a full page whose total_count says 100, with a failing check on page 2, is refused. It used to stop at page 1, and so does main.
  • Workflow runs (pagesOf, new): a full page of 100 workflow runs is followed by page 2.
  • Statuses (pagesOf): a full page with no total_count, or with total_count: 100, and the pinned name's failure on page 2, is refused. On db5ad78 it installed.
  • Past the cap: workflow runs and statuses get no answer and wait (inferred, code). Check runs keep main's documented newest-pages behaviour; a required check among the older pages reads as not run, so it isn't installed. That covered the required check itself, not another source's failure on an unread page. Round 7: check runs wait too while pins are judged (below).

Round 9: origin/main merged in, and every read whose error could look like an answer

Adversarial pass, round 8: a config with only bare names, against main

Method (measured, scratchpad/work/issue-727/bare8-789.cjs):

  • chooseCommit with required_checks: [custom], no updates.json pin, and branch rules naming no App, with pinChecks on as on every real install.
  • The same stand-in GitHub serves main 178c44e and each head.
  • 135 scenarios: 1, 10 or 11 pages of check runs; total_count right, missing or wrong; custom passed, failed, running, absent, or only on page 11; and nothing else, another check red on page 1, or no answer for page 2.
  • Compared: the verdict and the reads of the commit's checks.

Results:

Head The same verdict The same verdict and reads
5520710 49 of 135 49 of 135
b3dae4a 135 of 135 135 of 135

Two kinds of difference on 5520710, both fixed:

  • Past the cap: a bare custom passed with 10 full pages waited, where main installs.
  • The red shortcut: it ran with nothing pinned, so a bare config whose custom was running or absent was refused on another check's failure, where main waits on custom.

Left, justified: with any required name not pinned in updates.json or ENGINE_CHECK_SOURCES, each run reads the branch protection and rules (2 GETs, more while the rules' pages are full, since round 10). That is how a pin from GitHub's rules is found, and it has been in this PR since round 0. If they get no answer, the commit waits (rulesUnread). Main reads neither.

Inferred: on a real install the engine's own default checks are pinned by ENGINE_CHECK_SOURCES, so a bare-names-only config is one whose required_checks were all replaced with names that aren't the engine's.

Adversarial pass, round 7: every GitHub read on the updater's path

A truncated or refused read can only end as wait (no install now; the tip is asked again next run) or refused. It never ends as an install.

Read Full page, missing or wrong total_count Past 10 pages Any HTTP error (403 incl. rate limit, 404, 5xx) A network error Ends as
Check runs (checkRunsOf) next page no answer while a required check is pinned (457e89b kept the newest pages: page 11's failure installed); with every required check a bare name, the newest pages, as on main (round 8; through 5520710 this keyed on pinChecks, true on every install) fetchJson gives null: no answer chooseCommit throws: nothing installed wait
Workflow runs (pagesOf) next page no answer no answer throws wait
Combined status (pagesOf) next page no answer no answer (unlike the engine, a 403 for permission waits too: the updater reads unauthenticated, and a public repo's statuses are readable) throws wait
Branch protection and rules rules: next page while full (round 10; through 0b6e7a9 one page, no per_page) rules: unread (round 10) rulesUnread: waits; a permission 403 or 404 on the branch or rules page 1, with no required check pinned, is "no App named" (the owner's decision (a), bced758; page 1 only since round 10) throws wait
Compare (main channel) n/a n/a as main as main as main: no install without it (inferred)
A skipped read (hopeless, red, running) n/a n/a n/a n/a the check runs alone already rule the commit out: refused or wait (the reviewer: 0 installs in 20,000 scenarios)

Measured:

  • page 10 refuses and page 11 waits (check runs still full past CHECK_PAGES_MAX pages wait…, which installs on 457e89b);
  • statuses and workflow runs with a missing or wrong total_count (round 6 tests).
  • Inferred, from code: the release channel reads the same pagers for the release's main commit, and no answer there leaves no runs, so the required checks haven't run: not installed.

What this can never do

  • Make a commit installable that wasn't before, except the one loosening the owner decided: a pinned workflow file's later run on the commit supersedes that same file's earlier failed or cancelled runs, which judging by name would have refused (inferred, from ownVerdict and its tests).
    • Otherwise a pinned required check needs, in every workflow-file group of its source, the latest run and the newest check run to pass. Every other source's runs of its name must pass, and every same-name status must be success.
    • Every other name's latest run must still be green, as before, and an unpinned check is judged as before.
    • Correction: through 71b2f2c this line had no exception, which was false. Under an Actions-App pin it was also looser than that: a later run of any workflow file superseded another file's failure.
  • Read anything from the commit being judged to decide a pin. The list is in the installed file, and updates.json is root's.
  • Write to GitHub. The new calls are unauthenticated GET reads.

Compromised component

  • A coordinator / the dashboard: can't set required_check_sources; the machine helper writes only enabled and channel. No new reach.
  • Someone with push access to the engine repo: could change a pinned workflow file on main, as today.
  • Another App on the engine repo, or a PR's workflow posting onto main's commit (through the API, or into a pinned suite): its check no longer satisfies a pinned check, and any non-pass of it refuses the commit. It can hold an install; it can't make one.
  • GitHub's rate limit:
    • A commit its check runs don't already rule out costs two more unauthenticated reads than main: its workflow runs and its combined status.
    • A waiting machine makes 24 reads an hour instead of 12.
    • One run can still reach 61 reads, past the 60-an-hour limit (measured, the table above).
    • An unanswered read waits (inferred).
    • Correction: through db5ad78 this left out the compare call and the first-sight burst.
  • Agents, other instances, hub viewers: no new reach.

New dependencies

None.

… or App, the engine's own pinned built in

The root updater installed a commit once every required check name had a green run, from any source:
another App installed on the engine repo, or a workflow run posting a check run onto main's commit
through the API, could make a commit installable.

evaluateChecks now takes each required check only from its pinned source: updates.json
required_check_sources (root's), else ENGINE_CHECK_SOURCES, the updater's own fixed list of the
engine's checks and the workflow file that runs each (measured on a batched main commit), else the App
the repo's branch protection or ruleset requires; a check nothing pins refuses the commit. A workflow
pin needs GitHub Actions to have posted it from that file's run, in the repo, for that very commit.
Every check on the commit must still be green, whatever its source. The default checks are pinned
without a manual step on any machine. realDeps turns it on; tests' stand-in deps without it judge by
name as before, and a test pins that the installed updater always has it.

Test (fails without): test/update-check-sources.test.ts (a spoofed same-name check from another
workflow, commit, repo or App never makes main's commit or a release's commit installable; an unpinned
check is refused) and test/update-checks.test.ts (each default check pinned to the file that posts it).

Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
@jayden1711

Copy link
Copy Markdown
Owner Author

Evaluator verdict: CHANGES NEEDED (round 1/2)

Reviewed head 0ae51961, the updater part of #727. Every claim is labelled measured (a command I ran, or CI output I cite), inferred (reasoned from code, not run) or guess.

What it must do (from #727, for the updater)

Item Status Evidence
A same-name check from another App or workflow, another commit or another repo doesn't make a commit installable met, with a gap (required change 1) measured: update-check-sources.test.ts test 2 passes on the head and fails on main (another workflow file true !== false). guess: the API-made check run case, see change 1
The engine's own default checks need no manual step met measured: see the ENGINE_CHECK_SOURCES section below
A required check nothing pins is refused met measured: test 3
The release channel judges the same way met measured: the release-channel test (run doesn't install on a spoofed scan). inferred: update() calls checkPins and evaluateChecks(…, pins) on v.base
No answer for the workflow runs: wait, never pass met measured: test 5
Only ever refuses more met inferred: the all-green loop over latest (every source, latest per name) is unchanged (worklane-update.cjs:613-616), so a same-name non-pass from any source still refuses. Required names are judged only among pinned runs. RETIRED_CHECKS now keys off judged, but an unpinned desktop run still has to be green in that loop

Fail-without (measured)

  • Method: in my worktree I put origin/main's scripts/machine/worklane-update.cjs back with the PR's tests, then npx tsc (0 errors) and node --import ./dist/test/isolate.js --test dist/test/update-check-sources.test.js dist/test/update-checks.test.js, run with a scratch HOME and state dirs and a fake claude on PATH.
  • Result: # tests 16 # pass 9 # fail 7, the same as the builder's count.
  • On the head: all 17 updater test files (ci-batch through updates-add-check) give # tests 184 # pass 183 # fail 0 # skipped 1. npm run typecheck is clean. npm run check:denylist gives denylist: 1280 files clean.

ENGINE_CHECK_SOURCES

  • It is right for main today (measured):
    • all 15 UPDATE_DEFAULT_CHECKS names (scripts/setup/lib.sh:776) are in the table;
    • I ran the head's own chooseCommit with pinChecks: true and the 15 defaults on the latest batched main commit 720a807b's real check runs and workflow runs (the latest ci-slow.yml dispatch on main). The result is CHOSEN, and no rules call was needed;
    • on main's tip 0b294b53, which isn't batched, it isn't chosen: "Claude sign-in probe (real users)" hasn't run on this commit from its pinned source, as expected for an unbatched commit.
  • Its test keeps it in step (inferred). The new update-checks test derives each default check's workflow file from the committed workflows, the same checks() parser the batch tests use, and runs in CI on every PR. A renamed job or a moved file fails it.
  • The updates.json override is root-only (inferred):
    • updates.sh writes the file only when it is absent, as root:root 0644 (scripts/setup/updates.sh:149-158);
    • --add-check only appends required_checks;
    • the machine helper writes only enabled and channel (scripts/machine/worklane-machine.cjs:463,465);
    • an invalid entry pins nothing (measured: test 3).
  • The release channel uses the same pins (measured, the release-channel test).

Protection

  • Marking (measured, from the PR metadata and diff): it touches the updater (scripts/machine/*-update*, which is self-modification and a root helper) and scripts/ci/updater-drain-root.mjs. The title has [PROTECTED: owner merges], the body has **Risk class:** protected, it carries the needs-owner label, and it has real What this can never do and Compromised component sections.
  • The claims:
    • "Make a commit installable that wasn't before" holds (inferred, above).
    • "Read anything from the commit being judged to decide a pin" holds (inferred: the table is in the installed file, and updates.json is root's).
    • "Another App … or a PR's workflow posting onto main's commit … now its check doesn't count" depends on change 1 (guess).
  • Existing machines: nothing reopens (inferred). A machine whose only required checks are the defaults keeps installing (measured, above). A non-engine --add-check name is refused until pinned, which fails closed.

Scope, hygiene, CI

  • Scope: only the updater, its docs and CHANGELOG, and two fixtures that spread realDeps (inferred).
  • Hygiene (measured):
    • the commit is signed off, with no Co-Authored-By or "Generated with" in the commit or the body;
    • no new dependency.
  • Real state: the new tests use temp dirs and stand-in fetchJson; realDeps() is only built, with temp file paths, never run (inferred).
  • CI (measured, gh pr checks 789): 24 pass, 3 skipping, 0 failing or pending.

Not blocking

  • A rules read that fails gives a final refusal (inferred). fetchJson returns null on any non-OK answer, for example an unauthenticated rate limit. ruleApps(null, null) is then {}, so a non-engine check gets a final "no pinned source: refused", which is kept as a red verdict for an hour. It should wait instead (noAnswer). This only affects --add-check names outside the table, and it fails closed.

Required changes

  1. Close or prove the API-made check run case for workflow pins, which every machine now uses by default.
    • The test models a check run made through the API as one whose suite has no workflow run. That is an assumption, not a measured answer.
    • Guess: GitHub may file a check run created with a workflow's GITHUB_TOKEN under an existing Actions suite on the target commit, for example ci.yml's push suite. Its suite would then map to ci.yml, in this repo, for this commit, and it would pass the pin. That is the very spoof this PR names: "a PR's workflow posting onto main's commit".
    • Proving the negative needs a write to GitHub, so add the cheap positive check instead: require the check run's html_url to be /<repo>/actions/runs/<the mapped workflow run's id>/job/<the check run's id>. On this repo a genuine one is exactly that, e.g. .../actions/runs/39197227793/job/117883963166 for check run 117883963166 (measured).
    • That needs no extra call: keep the workflow run's id from actions/runs beside path.
    • Add a test where a check run in a ci.yml-mapped suite has an html_url of /runs/<id>, and the commit isn't installable.

…nned names as before; unread rules wait

Review round 1 and the owner's choice for existing installs (option c, 2026-10-19):
- 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>, the run id kept from actions/runs), so a check run made
  through the API into that suite doesn't count.
- A required check nothing pins is judged by name, exactly as before, so a machine with an
  --add-check name keeps updating. An invalid updates.json pin still refuses the commit.
- A rules read that GitHub didn't answer (a rate limit, say) waits instead of judging by name or
  being remembered as a refusal for an hour.

Test (fails without): test/update-check-sources.test.ts (a check run in the pinned suite that isn't
one of its jobs; an unpinned check judged as before; unread rules wait; another App's failing run
still refuses).

Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
@jayden1711

Copy link
Copy Markdown
Owner Author

Round 1: changes at dcf0577

Each claim is labelled measured, inferred or guess.

  1. A workflow pin needs one of its run's own jobs. The check run's html_url must be /<repo>/actions/runs/<the mapped run's id>/job/<its id>. The run id comes from the same actions/runs answer, with no extra call.
    • Measured: a check run in the ci.yml-mapped suite whose page is /runs/<id>, or another run's job, makes the commit not installable. On 0ae5196 it was chosen (true !== false).
    • Guess: that a real API-made check run has such a page.
  2. The owner's option (c): a required check nothing pins is judged by name, exactly as before, so a machine with an --add-check name keeps updating.
    • Measured: the result with pins equals the result without, for a passing, a failing and a missing run. On 0ae5196 it was refused.
    • An invalid updates.json pin still refuses and never falls back to the name.
  3. Non-blocking point: an unanswered rules read (a rate limit) now waits (noAnswer). It is no longer a final refusal cached for an hour. Measured: on 0ae5196 it didn't wait.
  4. A pin never turns a fail into a pass: another App's newer failing run of a pinned check refuses the commit. Measured: this passes on the old head too, since the all-green loop was already there.

Verification (measured):

  • every compiled test except mcp-* and plugin-fetch gives # tests 3519 # pass 3462 # fail 0 # skipped 57;
  • typecheck is clean, denylist: 1280 files clean, and DCO: all commits signed off.

…ost, over https

From an adversarial pass against the owner's decision guide: 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, kept from actions/runs) plus /job/<id>, over https on that host. Also tested: turning
updates off works with an invalid pin in updates.json and reads nothing from GitHub.

Test (fails without): test/update-check-sources.test.ts "the right path on another host" and "the
right page over http".

Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
@jayden1711

Copy link
Copy Markdown
Owner Author

Fix from an adversarial pass (head a59a426)

  • Fixed: a job page with the right path on another host, or over http, made the commit installable (measured on dcf0577: the right path on another host: true !== false). Now the page must be the workflow run's own html_url plus /job/<id>, over https on that host.
  • Also tested, no change needed: turning updates off with an invalid pin and junk in updates.json returns disabled and makes no GitHub read.
  • The full table is in the body.
  • After (measured):
    • every compiled test except mcp-* and plugin-fetch gives # tests 3520 # pass 3463 # fail 0;
    • typecheck is clean, and DCO gives all commits signed off.

@jayden1711

Copy link
Copy Markdown
Owner Author

Evaluator verdict: CHANGES NEEDED (round 2)

I reviewed head a59a426d, the updater part of #727, against the round-1 verdict and the owner's option (c). Every claim is labelled measured (a command I ran, or CI output I cite), inferred (reasoned from the code, not run) or guess. The fixtures use a stand-in fetchJson, a scratch HOME and state dirs, and a fake claude on PATH. No repo file was edited.

Round-1 required change and the owner's decision

Item Status Evidence
A workflow pin needs the run's own html_url plus /job/<id>, over https on that host met measured: for scan (pinned to gitleaks.yml), the genuine job page is INSTALLABLE. Another host, http: and /runs/<id> are each refused with hasn't run on this commit from its pinned source
The real engine commits still install met measured: the head's chooseCommit with pinChecks: true and the 15 UPDATE_DEFAULT_CHECKS on round 1's real answers. Batched main commit 720a807b is CHOSEN. Main's tip 0b294b53, which isn't batched, waits on Claude sign-in probe (real users) from its pinned source
Option (c): a name nothing pins is judged as before met measured: an --add-check name extra from App 2222 with the rules answered: INSTALLABLE pinned and by name; failing, the same refusal (ended failure) both ways
Unread rules wait, never judge by name met measured: branch: null gives waits (rules unread)
Turning updates OFF works with a broken pin met measured (the builder's test, passing on the head): run returns disabled and makes no GitHub read. A wait can't block OFF, since disabled is decided before any GitHub read (inferred)
A pin never turns a fail into a pass met in the code, but the claim overstates it (change 1) measured: another App's newer failing scan gives refused: check "scan" ended failure, pinned and by name

Adversarial checks (measured)

  • updates.json pin keys (from JSON.parse, which makes each an own key): __proto__, constructor, toString and hasOwnProperty, each pinned to App 1111, with App 2222 posting the check. All four are refused (hasn't run on this commit from its pinned source).
  • Pinned only by the branch's ruleset:
    • constructor and toString are refused;
    • __proto__ is INSTALLABLE from the spoofing App, because ruleApps builds a plain {} and out["__proto__"] = id is ignored (change 2). This is no looser than main, which judged by name, but the class the builder fixed for __proto__ is still open on this path.
  • Unicode lookalikes: names match exactly (hasOwn and ===), so a lookalike is a different, unrequired check (inferred; the same code shape is measured on Required checks count only from the App or workflow pinned for their name [PROTECTED: owner merges] #788).
  • Job page:
    • userinfo (https://x@github.com/...) is accepted;
    • inferred harmless: html_url is computed by GitHub, and an App can set only details_url.

Fail-without (measured)

  • This round:
    • Method: a scratch worktree at dcf05776, with test/update-check-sources.test.ts from a59a426d, then npx tsc (0 errors) and node --import ./dist/test/isolate.js --test dist/test/update-check-sources.test.js.
    • Result: # tests 10 # pass 9 # fail 1, matching the builder.
    • Key line: the right path on another host, true !== false.
  • On the head:
    • the 13 updater test files plus ci-batch give # tests 141 # pass 141 # fail 0;
    • npm run typecheck is clean;
    • npm run check:denylist gives denylist: 1280 files clean.

Size, hygiene, protection, CI

  • Size: reviewable (measured: +371 / -18 in 7 files).
  • Hygiene (measured):
    • DCO: DCO: all commits signed off;
    • no Co-Authored-By or "Generated with" in the commits;
    • no new dependency;
    • no file deleted against main;
    • no U+2028, U+2029 or bidi character in any changed file (0 hits in 7 files).
  • Real state: the tests use stand-in fetchJson and temp paths; realDeps() is only built, never run (inferred).
  • Protection (measured): the title has [PROTECTED: owner merges], the body has **Risk class:** protected, the PR carries needs-owner, and it has real What this can never do and Compromised component sections. The "Adversarial pass" section is present, with 8 attacks.
  • Rule 10 (measured, diff): two fixtures that spread realDeps gained a pin, so they still test the same path. Nothing is removed or weakened.
  • CI (measured, the check runs on a59a426d): 25 success, 2 skipped, 0 failing.
  • Merge (measured, git merge-tree): clean with origin/main c339175a, and clean with Required checks count only from the App or workflow pinned for their name [PROTECTED: owner merges] #788.

Required changes

  1. Correct the overclaim (principle 5). The claim appears as:

    • "Every check on the commit, whatever its source, must still be green" (intent);
    • "The all-green check over every run, any source, is unchanged" (What this can never do);
    • "a failing one still refuses the commit" (Compromised component);
    • the matching comment above evaluateChecks.

    The all-green loop covers only the latest run of each name, any source. Measured: a pinned scan started later with a pass, and App 2222's earlier failing scan, gives INSTALLABLE, both pinned and by name, as on main. This is not looser than main. Say what it does: "the latest run of every name, from any source, must be green, so a pin never turns a fail into a pass".

  2. Fix the __proto__ class for rule-named pins too.

A genuinely new trade-off for the owner

None for this PR alone. The question of whether a pinned name should be blocked by any same-name non-pass, not only the latest run, is raised on #788 and applies here too.

…claim says latest run per name

Review round 2:
- ruleApps built a {}, so a check the branch's rules pin under the name __proto__ lost its pin and
  was judged by name. Its map now has no prototype.
- The claim that every check on the commit must be green is corrected: the latest run of every name
  must be, as before, so a pin never turns a check that isn't a pass, as judged by name, into a pass.

Test (fails without): test/update-check-sources.test.ts "a check the branch's rules pin under the
name __proto__ keeps its pin".

Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
@jayden1711

Copy link
Copy Markdown
Owner Author

Round 3: changes at 703db4e

  1. Overclaim corrected in the body, CHANGELOG, docs/separate-users.md and the evaluateChecks comment. It now says: the latest run of every name must be green, as before, so a pin never turns a check that isn't a pass, as judged by name (its latest run), into a pass. Behaviour is unchanged: making it stricter is the owner's call.
  2. ruleApps builds its map with Object.create(null).
    • Measured on a59a426: a check the branch's rules pin as __proto__ lost its pin, and another App's run installed (expected: false actual: true). Now it is refused.

Verification (measured):

  • the fail-without on a59a426 gave # tests 11 # pass 10 # fail 1;
  • every compiled test except mcp-* and plugin-fetch gives # tests 3521 # pass 3464 # fail 0;
  • typecheck is clean, and DCO gives all commits signed off.

@jayden1711

Copy link
Copy Markdown
Owner Author

Evaluator verdict: CHANGES NEEDED (round 3)

I reviewed head 703db4e3, the updater part of #727, against the round-2 verdict and the owner's decisions. Every claim is labelled measured (a command I ran, or CI output I cite), inferred (reasoned from the code, not run) or guess. The fixtures use a stand-in fetchJson, a scratch HOME and state dirs, and a fake claude on PATH. No repo file was edited.

Both round-2 changes are met. One change is required, from a new owner decision (relayed by the orchestrator, 2026-10-19).

Round-2 required changes

# Change Status Evidence
1 Correct the all-green overclaim met measured, diff a59a426d..703db4e3. The body, the CHANGELOG entry, docs/separate-users.md and the evaluateChecks comment now say the latest run of every name must be green, so a pin never turns a check that isn't a pass, as judged by name (its latest run), into a pass. This wording must change again under the change below
2 ruleApps with Object.create(null) met measured, r2-789.mjs re-run on this head. A check that only the branch's ruleset pins under __proto__, constructor or toString to App 1111, posted by App 2222, gives refused: required check "…" hasn't run on this commit from its pinned source. Round 2 had __proto__ as INSTALLABLE. hasOwn works on a prototype-less object (inferred, scripts/machine/worklane-update.cjs:445)

Re-run of round 2's fixtures (measured, this head)

  • updates.json keys: __proto__, constructor, toString and hasOwnProperty from a spoofing App are each refused.
  • Job pages: another host, http: and /runs/<id> are refused, and the genuine job page is INSTALLABLE.
  • Option (c): an --add-check name nothing pins is INSTALLABLE both pinned and by name. Failing, it is refused both ways. With the rules unread, it waits.
  • Today's behaviour, which the change below replaces: a newer pinned scan pass with App 2222's older failing scan is INSTALLABLE pinned and by name.

Fail-without (measured)

  • Method:
    • a scratch worktree at a59a426d, with test/update-check-sources.test.ts from 703db4e3;
    • npx tsc (0 errors), then node --import ./dist/test/isolate.js --test dist/test/update-check-sources.test.js.
  • Result: # tests 11 # pass 10 # fail 1, matching the builder: a check the branch's rules pin under the name __proto__ keeps its pin: another App's run of it doesn't install, expected: false actual: true.
  • On the head:
    • every compiled test except mcp-* and plugin-fetch gives # tests 3521 # pass 3464 # fail 0 # skipped 57;
    • npm run typecheck: exit 0;
    • npm run check:denylist: denylist: 1280 files clean.

Rule 10, hygiene, protection, CI, merge (measured)

Required change

  1. Owner decision (2026-10-19, relayed by the orchestrator): for a pinned name, every run of that name on the commit must be green, not only the latest.
    • What blocks: a run of that name from any source that isn't a pass: another App, another workflow, or a cancelled duplicate run.
    • The owner accepts the cost: this is stricter than main.
    • Unpinned names stay exactly as now: the latest run of each name, by name (option c).
    • Tests to add, each failing on 703db4e3:
      • pinned scan: a newer pass from gitleaks.yml plus App 2222's older failing scan: refused. Today it is INSTALLABLE.
      • pinned scan: a pass plus a cancelled duplicate scan run from another suite on the same commit: refused.
      • an unpinned --add-check name with an older failure under a newer pass: INSTALLABLE, as now.
    • Both decision sites: main-channel chooseCommit and the release channel's check.
    • Update the claims: intent, "What this can never do", "Compromised component: Another App", the CHANGELOG entry, docs/separate-users.md and the evaluateChecks comment.

For the owner (inferred)

Existing machines: on main, ci.yml's and gitleaks.yml's push runs are never cancelled (cancel-in-progress is only for pull_request, .github/workflows/ci.yml:15-17, gitleaks.yml:6-8). So the stricter rule should not hold the engine's own main commits. A duplicate from another App posting an engine check name would.

…ource's non-pass refuses

The owner's rule (2026-10-19): for a pinned required check, only the latest run from its pinned source
on the commit counts, by the runs' own order (the later workflow run, then the later check run, as a
re-run attempt is), so a later run of that source supersedes its earlier or cancelled ones; a run for
another commit never does. Every run of its name from any other source must be a pass too. An
unpinned check (an --add-check name nothing pins) is judged by name, exactly as before.

Test (fails without): test/update-check-sources.test.ts (cancelled-then-passed and failed-then-passed
install; passed then cancelled in the later run refuses; another App's failure refuses; another
commit's run never supersedes; a re-run attempt counts as the newer; unpinned unchanged).

Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
@jayden1711

Copy link
Copy Markdown
Owner Author

Round 4: changes at 71b2f2c

The owner's rule (2026-10-19) for a pinned required check:

  • What counts: only its pinned source's latest run on the commit, by the runs' own order: the later workflow run, then the later check run, as a re-run attempt is. A later run of that source supersedes its earlier or cancelled ones, and another commit's run never does.
  • What refuses: any run of the name from another source that isn't a pass.
  • Unpinned: an --add-check name is judged exactly as before (measured: compared with pins off).

Tests (measured):

  • cancelled-then-passed and failed-then-passed install;
  • passed then cancelled in the later run refuses;
  • another App's failure refuses;
  • another commit's run never supersedes;
  • a re-run attempt's newer result decides.

Fail-without on 703db4e (measured): # tests 13 # pass 11 # fail 2. Cancelled-then-passed was refused (ended cancelled), and a re-run's newer pass wasn't chosen.

After (measured):

  • every compiled test except mcp-* and plugin-fetch gives # tests 3523 # pass 3466 # fail 0;
  • typecheck is clean, and DCO gives all commits signed off.

Wording is updated in the body, CHANGELOG, docs/separate-users.md and the code.

@jayden1711

Copy link
Copy Markdown
Owner Author

Evaluator verdict: CHANGES NEEDED (round 4)

I reviewed head 71b2f2cd, the updater part of #727, against the round-3 verdict and the owner's refined rule (2026-10-19). Every claim is labelled measured (a command I ran, or CI output I cite), inferred (reasoned from the code, not run) or guess. Fixtures are stand-ins fed to the exported evaluateChecks, which both decision sites call: chooseCommit and the release channel's check at :1493. They ran with a scratch HOME, scratch state dirs and a fake claude. No repo file was edited.

Where it stands: the owner's rule is in place and matches #788's. Three gaps remain:

The refined rule (measured, rule-789.mjs; each case over every permutation of its runs)

Case Verdict
Owner 1: cancelled, then passed, same pinned workflow installs
Owner 2: failed, then passed, same pinned workflow installs
Owner 3: pinned pass + another App's same-name failure (workflow pin; App pin) refused (from another source ended failure)
Passed, then a later own run failed refused
A later run of the pinned file for another commit: its pass over the commit's failure; its failure under its pass refused; refused
Re-run attempt: pass over fail; fail over pass installs; refused
Foreign App's newer same-name pass over the pinned failure (also with a forged /actions/runs/100/job/999 page) refused
API-made Actions check in the pinned suite (/runs/<id> page), newer pass refused
Another workflow file's newer pass over the pinned file's failure (workflow pin) refused
The rules pin scan to the Actions App (15368): evil.yml's later pass over gitleaks.yml's failure installs
Run 100 re-run (newest check run) fails after run 200 passed installs
  • Order independence: every case gave one verdict across all list orders.
  • The supersede comes only from the pinned source (inferred, scripts/machine/worklane-update.cjs:456-466): an App pin needs r.app.id, and a workflow pin needs the Actions slug, the suite's run, that path, repo and sha, and the job page. The exception is the Actions-App pin (change 1).
  • Statuses (measured, grep -n "statuses\|/status"): no match in the file. The updater reads no commit statuses, as on main. So a same-name status that isn't success never refuses a pinned check.

Fail-without (measured)

  • Method: a scratch worktree at 703db4e3, with test/update-check-sources.test.ts from 71b2f2cd, then npx tsc and node --import ./dist/test/isolate.js --test dist/test/update-check-sources.test.js.
  • Result: # tests 13 # pass 11 # fail 2, matching the builder: the owner-rule test (cancelled-then-passed refused) and the re-run-attempt test.
  • On the head:
    • every compiled test except mcp-* and plugin-fetch: # tests 3523 # pass 3466 # fail 0 # skipped 57;
    • npm run typecheck: exit 0;
    • npm run check:denylist: denylist: 1280 files clean.

Usual checks (measured)

Required changes

  1. An Actions-App pin must not supersede across workflow files (principle 1; the owner's rule says "that workflow"). This is the same as Required checks count only from the App or workflow pinned for their name [PROTECTED: owner merges] #788's change 2.
    • Measured: with the branch's rules pinning scan to App 15368, gitleaks.yml failed (run 100) and another workflow file's later run passed (run 101) gives installs.
    • Fix: under an App pin, take the latest run per workflow file; every group's latest must pass.
    • Test: that case, failing on 71b2f2cd.
  2. Statuses: the owner's rule is "any non-pass of the same name from any other source blocks".
    • Read the commit's statuses for pinned required names (GET /repos/{o}/{r}/commits/{sha}/status; no answer means wait, never a pass), and refuse on a same-name status that isn't success. Add a test.
    • The cost is one more unauthenticated GET per commit judged (inferred). If you think the owner must weigh that call, say so in the body and narrow the claims. Do not leave the gap unstated.
  3. Correct "What this can never do: Make a commit installable that wasn't before" (principle 5).
    • Measured (vs-main.mjs, main's updater piped in from git show origin/main:…): run 100 was cancelled at 10:00:05, after run 200's job started (10:00:01). Main refuses: required check "scan" ended cancelled. This head installs it.
    • That is the owner's case 1, so the behaviour is right but the claim is false.
    • Say instead: a pinned source's later run supersedes its own earlier failed or cancelled runs, which the by-name latest could refuse. That is the owner's decision, and the only way it is looser.

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 the older run 100 was re-run and its newest attempt failed. The commit installs (measured).
  • Fail-closed alternative: also require the newest check run of that source to pass.

…uses, statuses read (#727)

- A pinned source's own runs are grouped by workflow file (one group for
  an App that isn't Actions; a pin to GitHub Actions' App reads the
  workflow runs too). In every group both the latest run and the newest
  check run must pass (ownVerdict), so one file never supersedes another
  and a re-run of an older run that failed after a newer run passed
  refuses the commit.
- The commit's combined status is read (one more GET per commit judged):
  a same-name status that isn't success refuses a pinned check, pending
  waits, and no answer waits.
- The owner's failed-then-passed fixture gets realistic check run ids
  (the later run's job has the higher id); its assertion is unchanged.

Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
…atuses (#727)

The two fixtures that spread realDeps (test/update-redact.test.ts and
scripts/ci/updater-drain-root.mjs) answered every non-compare read with
check runs only, so the new combined-status read got no answer and the
commit waited. They now answer an empty statuses list too, and keep
testing the same path with pinning on.

Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
@jayden1711

Copy link
Copy Markdown
Owner Author

Round 5 at db5ad78 (two signed-off commits on 71b2f2c, no rewrite).

  1. Per-workflow-file grouping and a fail-closed "latest", the same as Required checks count only from the App or workflow pinned for their name [PROTECTED: owner merges] #788:
    • ownVerdict groups a pinned source's runs by workflow file. A pin to GitHub Actions' App now reads the workflow runs too, so it is grouped by file. In every group, the latest run and the newest check run must both pass.
    • Tests:
      • an Actions-App pin with ci.yml failed and evil.yml passed later (refused);
      • ci.yml still running (waits);
      • a failing workflow added in the commit itself (refused);
      • both passing (installs);
      • run 200 passed, then run 100 re-run and failed or cancelled (refused);
      • the owner's cases still install.
    • One existing fixture (the owner's failed-then-passed case, listed in reverse) now gives each check run a realistic id, so the later run's job has the higher id. Its assertion is unchanged.
  2. Commit statuses are read. GET /commits/{sha}/status gives the combined status, paged.
    • A same-name failure or error refuses a pinned required check.
    • pending waits, and so does no answer.
    • A test covers each case.
  3. Corrected "can never make a commit installable that wasn't before", with the same-file supersede named and a "false through 71b2f2c" note. The CHANGELOG is corrected the same way.

Call counts (measured, chooseCommit with the 15 default checks over a stand-in GitHub, before 71b2f2c and after 1b515c3):

Case Before After
One green commit 2 3
Three commits, the two newest red 6 9
One commit plus an unpinned name 4 5

That is one more unauthenticated GET per commit judged. At the 10-minute timer with a green tip, check reads go from 12 to 18 an hour, against GitHub's 60 an hour per IP (inferred).

Fail-without (measured): the new test file on 71b2f2c gave # tests 16 # pass 13 # fail 3. The Actions-App case, the failed re-run and the failing status all gave installs.

Fixtures that spread realDeps: test/update-redact.test.ts and scripts/ci/updater-drain-root.mjs now answer the status read with an empty statuses list. At 1b515c3 the broad suite failed the two update-redact tests, because the status read got no answer and the commit waited (measured). Fixed in db5ad78.

After (measured, at db5ad78): # tests 3526 # pass 3469 # fail 0 # skipped 57. Typecheck is clean, denylist: 1280 files clean, and DCO: all commits signed off.

The body has a new Adversarial pass, round 5 section.

@jayden1711

Copy link
Copy Markdown
Owner Author

Evaluator verdict: CHANGES NEEDED (round 5)

I reviewed head db5ad787, the updater part of #727, against the round-4 verdict, the owner's rule and the orchestrator's round-5 tightening. Every claim is labelled measured (a command I ran, or CI output I cite), inferred (reasoned from the code, not run) or guess. Fixtures are stand-ins fed to the exported evaluateChecks and chooseCommit, with a scratch HOME, scratch state dirs and a fake claude. No repo file was edited.

Where it stands: the rule, the tightening and the status read are right, and every round-4 change is met. Two small things are left:

  • the status reader stops after a full page that has no total_count;
  • the read-count claim leaves out the compare call and the first-sight burst.

The read cost itself is for the owner.

The owner's rule and the tightening (measured, rule-789.mjs; each case over every permutation)

Result: 34 cases with an expected result, 0 mismatches, one verdict per case across all orders.

Case Verdict
Owner 1 / 2: cancelled or failed, then passed (workflow pin; App pin; Actions-App pin from the rules) installs
Owner 3: pinned pass + another App's same-name failure (workflow, App and Actions-App pins) refused (from another source ended failure)
Actions-App pin: gitleaks.yml failed, or failed in the newest run, and evil.yml passed refused
Actions-App pin: gitleaks.yml still running, evil.yml passed waits
Actions-App pin: an Actions run whose workflow run wasn't read, passed, beside a failed gitleaks.yml refused
Re-run of the older run fails after the newer run passed (workflow pin; Actions-App pin) refused
Re-run of the older run passes; a normal re-run of the latest run passes (older run cancelled); a re-run attempt of the latest run passes (workflow and Actions-App pins) installs
Round 4's set: a later own run failed or running; another commit's run; a foreign App's newer pass (with a forged job page); an API-made run in the suite; another file under a workflow pin refused / waits, as round 4
Status failure / error / pending / success of the pinned name; failure of another name refused / refused / waits / installs / installs
App pin: the App fails scan, then passes another check later refused
App pin: the App later passes the same name over its own failure installs (the owner's rule for an App source; see the owner note)

Statuses (measured, rule-789.mjs, chooseCommit with a stand-in GitHub, scan pinned to gitleaks.yml and passing)

Status answer Result
null (any HTTP error, e.g. a 403 rate limit: realDeps.fetchJson returns null when !r.ok) waits: GitHub gave no answer for its commit statuses
no statuses array; statuses not an array waits
the read throws (network error) chooseCommit throws ECONNRESET, and nothing is installed. runOnce records the error and rethrows (inferred, worklane-update.cjs:1485-1490)
page 1 full, total_count 150, page 2 no answer waits
page 1 full, total_count 101, scan failing on page 2 refused
page 1 full (100), no total_count, scan failing on page 2 installs (page 2 never asked)
an entry with no context beside a failing scan; state FAILURE; state missing refused (fail closed)
empty installs

GitHub's combined-status answer carries total_count (inferred, from the API's shape), so the full-page case needs a malformed answer. The same check in #803's reader was made to fail closed in round 3.

Unauthenticated reads (measured per run, inferred per hour)

Method: calls-789.mjs, chooseCommit with pinChecks: true and the 15 ENGINE_CHECK_SOURCES names, over a stand-in GitHub. The updater source for each revision is piped in from git show.

Per run main 178c44e6 round 4 71b2f2cd this head
tip green, 58 check runs 1 2 3
tip green, 140 check runs (2 pages) 2 3 4
20 commits, all red, first sight (default lookback 20) 20 40 60

The updater's other unauthenticated calls, read from the code (inferred):

  • compare once per run whenever main's tip isn't the installed commit (:1560);
  • the branch and rules reads (2) only when a required name has no updates.json or ENGINE_CHECK_SOURCES pin (:496-500);
  • the release channel adds no REST read before the check-runs read (ls-remote and the tag fetch are git, not REST).
  • Nothing else on the machine calls the REST API without a token. Every src/ GitHub client sends one (inferred, grep).
  • With the tip installed, a run makes no REST read (:1528).

Per hour, at one run every 10 minutes (OnUnitActiveSec=10min):

  • A machine waiting on a green new tip (not quiet, say): main 12, round 4 18, this head 24. Each run is the compare plus the check runs, plus the workflow runs and the status here.
  • The body's "12 to 18 an hour" counts the check reads only. Adding the compare, it is 18 to 24.
  • First sight of a red or idle streak: up to 60 + 1 reads in one run at the default lookback (20), against GitHub's 60 an hour per IP.
    • Main's batched CI leaves most commits below the tip with no checks, so they are idle (inferred).
    • At max_lookback: 100, up to 300 + 1.
  • Past the limit: each read answers 403, so fetchJson returns null and the commit waits (fail closed, measured above). The memo keeps each red or idle verdict for an hour.
  • So: it's safe (never a wrong install), but an update can be delayed by up to an hour while the budget refills (inferred).

Fail-without (measured)

  • Method: a scratch worktree at 71b2f2cd, with test/update-check-sources.test.ts from db5ad787, then npm run build and node --import ./dist/test/isolate.js --test dist/test/update-check-sources.test.js.
  • Result: # tests 16 # pass 13 # fail 3, matching the builder:
    • an Actions-App pin never lets one workflow file supersede another (ci.yml failed, evil.yml passed later);
    • "latest" fails closed;
    • a pinned check's same-name commit status that isn't success refuses it.
  • On the head:
    • every compiled test except mcp-* and plugin-fetch: # tests 3526 # pass 3469 # fail 0 # skipped 57;
    • npm run typecheck: exit 0;
    • npm run check:denylist: denylist: 1280 files clean.

Usual checks (measured)

Required changes

  1. The status reader must not take a full page without total_count as the end (principle 1).
    • In statusesOf, keep reading while a page has 100 entries, and return null past CHECK_PAGES_MAX.
    • Add a test: page 1 full with no total_count, scan failing on page 2, must refuse. It installs on db5ad787 (measured).
  2. Correct the read-count claims (principle 5).
    • Say that a run also makes the compare call, so a waiting machine goes from 18 to 24 reads an hour (main: 12).
    • Say that the first sight of a 20-commit red or idle streak can take 61 reads in one run (main: 21). Past GitHub's 60 an hour, reads get no answer and the update waits.
    • Keep "inferred" on the hourly figures.

For the owner (a new cost; not a required change)

  • The cost: every commit judged costs 3 unauthenticated reads instead of main's 1 (measured). A run can reach the 60-an-hour limit on its own at the default lookback, and then installs wait for up to an hour (inferred). It never installs wrongly.
  • A cheap mitigation the builder could add if you want it: skip the workflow-run and status reads for a commit where some pinned required name has no check run at all. Such a commit is missing or idle under any pin. Below a batched tip that is most commits, so the burst would drop to about 1 read per commit (inferred).
  • App pins: as in Required checks count only from the App or workflow pinned for their name [PROTECTED: owner merges] #788's verdict, under a pin to an App that isn't Actions, that App's later run of the same name supersedes its earlier failure (measured: installs). A third-party CI App that runs PR-supplied pipelines could use that. The fail-closed alternative gives up the owner's cases 1 and 2 for that App.

…ins fail closed (#727)

- Every pager (check runs, workflow runs, the combined status) reads the
  next page while a page is full, whatever total_count says; a missing or
  wrong total_count never makes a full page the last. Workflow runs and
  statuses still full past CHECK_PAGES_MAX pages: no answer (wait).
- The read cost: the workflow runs and statuses aren't read for a commit
  its check runs already rule out (a pinned required check with no
  passing run at all, or a check judged by name that ended red or is
  still running); the verdict then comes from the check runs alone.
- A pin to an App that isn't Actions fails closed: every one of that
  App's check runs of the name must pass. An Actions run is told by its
  App id, as the engine tells it. The deciding run is the same whatever
  the list order.
- CHANGELOG states the measured read counts and that one run can still
  exceed GitHub's 60 an hour, for the owner to weigh.

Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
@jayden1711

Copy link
Copy Markdown
Owner Author

Round 6 at 457e89b: one signed-off commit on db5ad78, no rewrite.

  1. Pagers. Every pager reads the next page while a page is full, whatever total_count says. That covers the check runs, the workflow runs (now paged) and the statuses (pagesOf).
    • Still full past 10 pages: workflow runs and statuses wait; check runs keep main's newest-pages behaviour.
    • Tests: a full page with no total_count, or with total_count: 100, and the pinned name's failing status on page 2, is refused. A full page of workflow runs is followed by page 2. A full page of check runs with a wrong total_count and a failing check on page 2 is refused.
  2. Read cost. The workflow runs and statuses aren't read for a commit its check runs already rule out. That covers a pinned required check with no passing run at all (your mitigation, generalized from "no run"), a check judged by name that ended red, and one still running.
    • In those cases the verdict comes from the check runs alone. Tests check that only one read is made.

    • Measured per run (run() with the compare, over a stand-in, busy machine):

      Case main db5ad78 457e89b
      Waiting on a green tip 2 4 4
      20 red commits, first sight 21 61 21
      20 idle commits, first sight 21 61 21
      20 commits red on a check that isn't required 21 61 21
      20 commits with a check still running 21 61 21
      Worst case: 20 commits where a pinned file's later run failed after an earlier pass 21 61 61
    • Per hour (inferred, 10-minute timer): a waiting machine makes 24 reads an hour, where main makes 12.

    • One run can still use 61 reads, past GitHub's 60 an hour. The body and CHANGELOG say so plainly. The update then waits, never installs wrongly. That trade-off is the owner's.

  3. Non-Actions App pins fail closed, as in Required checks count only from the App or workflow pinned for their name [PROTECTED: owner merges] #788. Every one of that App's check runs listed (the latest per suite) must pass. Tests cover two suites cancelled-then-passed, failed-then-passed, a same-suite re-run, and both passing.
    • An Actions run is now told by its App id, as the engine tells it.

Fail-without on db5ad78 (measured): # tests 19 # pass 16 # fail 3. The status on page 2 installed, /actions/runs was read for a commit missing a pinned check, and the App's later suite superseded its failure.

After (measured):

  • the broad suite: # tests 3529 # pass 3472 # fail 0 # skipped 57;
  • typecheck clean, denylist: 1280 files clean, DCO: all commits signed off.

The body has a new Adversarial pass, round 6 section on the pagers.

@jayden1711

Copy link
Copy Markdown
Owner Author

Evaluator verdict: CHANGES NEEDED (round 6)

I reviewed head 457e89b5, the updater part of #727, against the round-5 verdict, the owner's rule and the coordinator's tightenings. Every claim is labelled measured (a command I ran, or CI output I cite), inferred (reasoned from the code, not run) or guess. Every case goes through the real chooseCommit over a stand-in GitHub, with a scratch HOME, scratch state dirs and a fake claude. The stand-in answers check runs as GitHub's filter=latest does: the highest id per (check suite, name), measured on this repo (see #788's verdict). No repo file was edited.

Where it stands:

  • Both round-5 changes are met: the pagers and the read-count claims.
  • The read-skipping never hid a failure in 20,000 random scenarios.
  • The non-Actions App pin fails closed across suites.
  • Left:
    • the check-run pager still judges a truncated list;
    • two claim fixes.

The owner's rule and the tightenings (measured, rule-789.mjs; each case over every permutation)

Result: 38 cases (1 informational), 0 mismatches, one verdict per case across all orders.

Case Verdict
Owner 1 / 2 (workflow pin; Actions-App pin from the rules; App pin with the re-run in the same suite) installs
Owner 1 / 2 under an App pin, across two suites refused (ended cancelled / ended failure)
App pin: suite 62 still running waits
Owner 3 under each pin kind refused (from another source ended failure)
Actions-App pin: evil.yml after a failed, or newest-failed, gitleaks.yml; an Actions run with an unread workflow run beside a failed gitleaks.yml refused
Actions-App pin: evil.yml passed, gitleaks.yml running waits
Re-run of the older run fails after the newer run passed (workflow pin; Actions-App pin) refused
Re-run of the older run passes; a normal re-run of the latest run; a re-run attempt of the latest run installs
Round 4's set: another head, a foreign App's newer pass (also with a forged job page), another file refused
Round 4's set: an API-made run in the suite waits (missing)
Statuses failure / error / pending / success / another name's failure refused / refused / waits / installs / installs
App pin: two pipelines of the App post scan into one suite, A failed and B passed installs (informational; see the owner note and change 3)

The read-skipping (measured, skip-789.mjs)

Method: 20,000 random scenarios, each compared two ways.

  • The scenarios:
    • required checks from scan, dco (both ENGINE_CHECK_SOURCES pins), custom and desktop (retired);
    • updates.json pins (an App, a workflow, Actions) and branch rules naming 15368, 1111, -1 or none;
    • 0 to 6 runs from Actions (five workflow runs, some for another commit, some API-made), from App 1111 (two suites) and from App 2222, in every state and conclusion;
    • 0 to 2 statuses.
  • E_skip is the real chooseCommit (one candidate, just landed), with its skips.
  • E_full is evaluateChecks over the same runs, with pins from forSha given a list that never skips, so the workflow runs and statuses are always read.

Result:

  • E_skip installs where E_full doesn't: 0.
  • E_full installs where E_skip doesn't: 0.
  • The same verdict kind: 16,311.
  • Refused vs waits differs in 3,689, and neither side installs in any of them.
    • In those, the skip reports a different blocking check first. For example, dco failed from another source while scan hasn't run from its source: E_skip refuses on dco, E_full waits on scan.
    • Main would also refuse dco there (inferred, by name).

Why no skip can hide a failure (inferred, code):

  • each skip (hopeless, red, running) returns a non-install, and the full evaluation would block on the same check (no passing run at all; a by-name latest run that is red or still running);
  • so no read that's skipped can be the one that turns an install into a refusal.
  • A refused verdict can move chooseCommit on to an older green commit sooner than a wait would. It never installs a commit that isn't green.

Pagers and the status API (measured, rule-789.mjs, chooseCommit)

Answer Result
status null (any HTTP error, e.g. a 403 rate limit), or no statuses array waits
the status read throws chooseCommit throws; nothing installed
status page 1 full with no total_count, or total_count 100 or 50 (wrong), and a failing scan on page 2 refused
status page 2 null waits
10 full status pages waits
check runs page 1 full with total_count 100 (wrong) or none, and a foreign failing scan on page 2 refused
check runs page 2 null waits
workflow runs page 1 full with no total_count, and the pinned run on page 2 installs (page 2 read)
workflow runs page 2 null waits
check runs: the pinned pass on page 1, a foreign failing scan on page 10 refused
check runs: the pinned pass on page 1, a foreign failing scan on page 11 (past CHECK_PAGES_MAX) installs (change 1)

Read counts (measured, calls-789.mjs, chooseCommit with the 15 engine checks)

The compare call before chooseCommit adds 1 per run (inferred, code).

Per run (reads in chooseCommit) main 178c44e6 round 5 db5ad787 this head
tip green, 58 check runs 1 3 3
tip green, 140 check runs 2 4 4
20 commits red on their check runs, first sight 20 60 20
20 commits green by name, but a pinned file's later run failed, first sight 1 (installs) 60 60
tip green, plus one unpinned --add-check name that hasn't run 1 5 5
  • With the compare, that gives 4 per run waiting (main 2), 21 at first sight of a red streak (main 21) and 61 in the worst case (main 21). The body's table matches.
  • With an unpinned --add-check name, a run also reads the branch rules (2 more). The body says so in prose ("only when some required check has no pin").

Fail-without (measured)

  • Method: a scratch worktree at db5ad787, with test/update-check-sources.test.ts from 457e89b5.
  • Result: # tests 19 # pass 16 # fail 3, the builder's three: the pagers, the read cost, and the non-Actions App pin (actual: 'installs').
  • On the head:
    • every compiled test except mcp-* and plugin-fetch: # tests 3529 # pass 3472 # fail 0 # skipped 57;
    • npm run typecheck: exit 0;
    • npm run check:denylist: denylist: 1280 files clean.

Usual checks (measured)

Required changes

  1. The check-run pager must not judge a truncated list while any required check is pinned (principle 1).
    • What happens: checkRunsOf returns the first CHECK_PAGES_MAX pages when there are more (worklane-update.cjs:378-386).
    • Measured: a foreign App's failing scan on page 11 installs (on page 10 it refuses).
    • Why it matters now: under the owner's rule, any other-source non-pass blocks a pinned check, so an unread page can hold the failure. By name, only the latest run counted, so main didn't depend on the older pages.
    • Cost (inferred): GitHub lists check runs newest first (measured on main's tip: ids descending). Past 1000 runs, the push CI's own required runs are already unread today, so such a commit already waits or idles.
    • Fix: return null (the commit waits) past the cap when pinning is on, as pagesOf does for the other two.
    • Correct the body's "Check runs keep main's documented newest-pages behaviour; a required check among the older pages reads as not run, so it isn't installed". That covers the required check itself, not another source's failure.
    • Test: one that installs on 457e89b5.
  2. Correct "A workflow pin needs the check run posted by GitHub Actions (app.slug)" (principle 5). Since this round it is app.id 15368 (isActions).
  3. Correct the non-Actions App claim (principle 5), as in Required checks count only from the App or workflow pinned for their name [PROTECTED: owner merges] #788.
    • The claims: "fails closed" and "What is left is a re-run in the same check suite".
    • Say instead: any later same-name run that App puts in the same suite replaces its earlier one, a second pipeline as well as a re-run (measured above: installs).

For the owner (not required changes)

…ged (#727)

checkRunsOf still full past CHECK_PAGES_MAX returned the pages read, so a
failing run of another source on page 11 went unread and the commit
installed. While pins are judged that is now no answer (the commit
waits), as the workflow-run and status pagers already do; without pins
the newest pages are kept, as before. The CHANGELOG's non-Actions App
wording says a later same-name run the App puts in the same suite
replaces the earlier one, a second pipeline as well as a re-run.

Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
@jayden1711

Copy link
Copy Markdown
Owner Author

Round 10: the fix is 5f746c2, and origin/main 6e0d7d8 is merged in as 8a417d1.

  1. A refusal's body is now read as the engine's req reads it: as JSON when it parses, otherwise as text. Its message comes from that body, as classifyGitHubError takes it.
    • Comparing against the engine's classifier over 704 cases (measured): 0 differ now, against 9 on 0b6e7a9. All 9 were a 403 with rate-limit text in a plain-text or JSON-string body.
    • New test rows: a rate-limited 403 with a plain-text body, a secondary-limited one, and a permission one.
  2. The rules read now pages as the engine's checkApps does: 100 a page, the next page while one is full, up to 10.
    • A later page that is refused, isn't a list, or goes past the cap means wait. Decision (a) applies to page 1 only.
    • Test: a pin on page 2 waits, where 0b6e7a9 installed. Page 2 refused for permission, a 404, a 500, not a list, or past 10 each waits. An empty page 2 installs, as main does.
  3. The body: it has an adversarial-pass entry for bced758 and for this round, and corrections to the fetchRead and rules-read claims.
  4. The merge had no conflicts and dropped no changelog bullet.

Fail-without (measured): I wrote the new tests first and ran them against the unchanged code: # tests 24 # pass 22 # fail 2.

After (measured):

  • The updater tests gave # tests 75 # pass 74 # fail 0 # skipped 1.
  • The broad suite gave # tests 3861 # pass 3803 # fail 1 # skipped 57. The one failure is dashboard-speed (read ECONNRESET), a known flake that this PR doesn't touch. Run alone, it passes 9 of 9.
  • Typecheck is clean and denylist: 1310 files clean.

Package size: a delta of 0. Nothing this PR changes is packed.

@jayden1711

Copy link
Copy Markdown
Owner Author

Evaluator verdict: PASS (round 10)

I reviewed head 8a417d13: 5f746c2c (the round-10 fix), then a merge of main 6e0d7d81 (#780). The previous head was 0b6e7a9d. I wrote none of it.

  • Labels: every claim is measured (a command I ran, or CI output I cite), inferred (reasoned from the code, not run) or guess.
  • Isolation: every case ran through the real realDeps, chooseCommit or fetchRead over a stand-in globalThis.fetch, with a scratch HOME, scratch state dirs and a fake claude. No repo file was edited.

Both round-9 findings are fixed. No pinned or transient case changed. Four named gaps are left; each mirrors the engine, and none is a regression against main.

1. fetchRead against the engine (measured, classify.mjs, the same 924 cases as round 9)

  • At 8a417d13: cases 924, updater differs from engine in 0. On 0b6e7a9d it was 10.
  • The fix: a refusal's body is now read as JSON when it parses, else as raw text, as the engine's req reads it. The message is then taken as classifyGitHubError takes it.

2. The rules read now pages (measured)

My round-9 attack (rulespage.mjs, bare ci and lint, a rule pinning lint to App 3333):

Case 0b6e7a9 8a417d1
Pin on page 1 (control) waits waits
Page 1 holds 100 other rules, the pin is on page 2 installs waits (pages 1 and 2 read, per_page=100)
Page 1 holds 30 other rules, the pin is on "page 2" installs installs, 1 page read
  • The 30-rule row isn't a gap: with per_page=100, a page of 30 is the last one, so GitHub would put no rule on a page 2 (inferred). My stand-in served one anyway.

New edge cases (edges.mjs; ci and lint pass from App 2222 only):

Case Verdict Pages read
Pin ci→4242 on page 1 waits 1
Pin ci→4242 on page 2 behind a full page 1 waits 1,2
Pin ci→2222 (its real App) on page 2 installs: the page-2 pin is used, not a blanket wait 1,2
9 full pages, then exactly 100 on page 10 with the pin waits (cap) 1–10
10 full pages, none pinning, page 11 empty (exactly 1,000 rules) waits (cap) 1–10
9 full pages, then a short page 10 with the pin waits 1–10
Page 1 full, no Link header, pin on page 2 waits: it pages on fullness, not Link 1,2
Page 1 short (50), Link says rel="next", pin on page 2 installs: page 2 unread 1
Page 1 full, Link says next, page 2 empty installs 1,2
The same pin (ci→4242) on page 1 and page 2 waits 1,2
The same pin (ci→2222) on page 1 and page 2 installs 1,2
ci→4242 on page 1, a contradicting ci→5555 on page 2 installs (by name) 1,2
The same contradiction, both on page 1 installs (by name) 1
ci→2222 on page 1, a contradicting ci→4242 on page 2 installs (by name) 1,2

Decision (a) on page 1:

Page 1 answer Install Verdict
permission 403, 404, or plain-text Forbidden 403 bare installs
500, rate-limited 403, or plain-text secondary-limit 403 bare waits
permission 403 or 404 lint pinned in updates.json waits

3. Decision (a) and no loosening (measured, matrix.mjs, 320 cases)

  • The same 4 configs × 10 branch outcomes × 8 rules outcomes as round 9, against the owner's rule (3) written out on its own: 0 wrong.
  • Against 0b6e7a9d: 0 verdicts changed, so 0 pinned or transient cases changed.
    • Round 9 had already measured 8 changes against 12148c19, all bare installs with only permission or 404 refusals. Those stand.
  • My first run of this matrix at 8a417d13 showed 57 changes. That was my harness: its stand-in matched only the unpaged rules URL, so ?per_page=100 fell through to its default 404. I fixed it to match the paged URL too, and reran. The 0 above is from the fixed harness.

4. Fail-without, by the new method (measured)

The setup:

  • a fresh worktree at 0b6e7a9d;
  • the head's test/update-check-sources.test.ts written in with Write and Edit only, no git checkout;
  • git hash-object of the written file: df719979…, equal to 8a417d13:test/update-check-sources.test.ts;
  • built, then run with node --import ./dist/test/isolate.js --test dist/test/update-check-sources.test.js.

Result: # tests 24 # pass 16 # fail 8.

  • Two failures show the bugs:
    • decision (a): bare, a rate-limited 403 whose body is plain text, expected: 'waits' actual: 'installs';
    • the paging test. Its first failing line is the per_page=100 assertion (expected: true actual: false).
  • Six failures (tests 3, 4, 8, 9, 14, 18) come from the stand-ins now answering only ?per_page=100. The old code asks the unpaged URL, gets null, and waits (GitHub gave no answer for the branch's rules). They are not bug demonstrations.
  • The body's # tests 24 # pass 22 # fail 2 is from the tests as first written, before those stand-ins changed (inferred from its wording). The committed file gives 16/8 on 0b6e7a9d. The two meaningful failures are the same in both.

The page-2 expected value changed from "refused" to "waits". That is legitimate:

  • On 0b6e7a9d the test never reached that assertion. The per_page assertion fails first (measured, above).
  • "waits" is exactly the verdict a page-1 pin gets: both wait (measured, edge table).
  • The direct before/after for that property is my rulespage.mjs: installs on 0b6e7a9d, waits on 8a417d13 (measured).
  • A page-2 pin to the real App installs, so the assertion isn't vacuous.

After, at 8a417d13: update-check-sources, update-checks, update-redact and package-files gave # tests 44 # pass 44 # fail 0.

5. Stand-in audit, rule 10 (measured, git show 5f746c2c)

Three stand-ins changed, and none is loosened:

  1. gitHub(): answers ?per_page=100 instead of the unpaged URL. A page 2 would get null (unread, waits), which fails closed.
  2. The "rules were asked" assertion: now asserts the paged URL. That is exactly as strict.
  3. The round-8 fixture: answers the paged URL.

Elsewhere in the file:

  • The decision-(a) test kept all 16 original rows: 4 refusals × answered or refused × bare or pinned, with the same expected values.
  • It moved into viaRealDeps, which serves the same answers for any page.
  • It gained 3 plain-text rows (12 assertions).
  • No assertion was removed.

6. The merge, the package and the rest (measured)

Named gaps (not required here; each mirrors the engine, and none is a regression against main)

  1. Contradicting pins become "by name". Two rules pinning one check to different Apps drop the pin, on one page or across pages, and that check is judged by name, so it installs. Both ruleApps here and the engine's githubCheckApps do this on purpose ("no single source passes both").
    • It is not new this round, and main judges every check by name anyway.
    • Under principle 1, failing closed would wait instead. That change belongs to both PRs together, so it fits a follow-up issue for the owner.
  2. Paging follows fullness, not Link. A short page whose Link header says there is another page ends the read. Whether GitHub ever does that with per_page=100 is a guess; its documented behaviour is full pages until the last.
  3. 1,000 or more branch rules: a bare install waits for good, where main installs. This fails closed, and it is a guess that it matters.
  4. The body's fail-without numbers are from the tests as first written (22/2). The committed file gives 16/8 on 0b6e7a9d, as explained above. The meaningful failures match.

Before the owner merges (procedural)

Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
v0.5.3 is released (main 71316eb). The merge auto-placed this PR's two bullets in its released notes; they move, unchanged, to a new v0.5.4 (unreleased) section above it, ended by an empty generated heading so the release keeps them. Main's v0.5.3 text is as main has it.

Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>

# Conflicts:
#	CHANGELOG.md
Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>

# Conflicts:
#	CHANGELOG.md
Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>

# Conflicts:
#	CHANGELOG.md
Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
…ins.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>
jayden1711 added a commit that referenced this pull request Oct 20, 2026
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant