Skip to content

doctor prints a paste-ready required_check_sources block; a new project's config starts pinned (after #788) [PROTECTED: owner merges] - #803

Merged
jayden1711 merged 27 commits into
mainfrom
issue-727-pin-block
Oct 21, 2026
Merged

jayden1711 merged 27 commits into
mainfrom
issue-727-pin-block

Conversation

@jayden1711

@jayden1711 jayden1711 commented Oct 19, 2026 •

Copy link
Copy Markdown
Owner

Intent: doctor prints a paste-ready required_check_sources block for the required checks with no pin, and a new project's drafted config starts pinned. With #788 (option c), a required check nothing pins is judged by name; this makes pinning it one paste. Split out of #788.

Risk class: protected. src/** is engine, and src/doctor.ts, src/auto-config.ts and src/cli.ts are touched; docs/design.md is engine-guards. The owner merges.

Part of #727. Merge order: after #788. The base is #788's branch, which is merged in (no rewrite).

How it works (src/check-pin-block.ts)

  • workflowsAt(repo, rev) reads the workflow files committed at one commit, with git.
    • doctor uses main's tip, as ghTipRuns reads it, or origin/<default> when the tip can't be read. A new project uses its base commit.
    • It takes .github/workflows/*.y(a)ml regular blobs only (mode 100644 or 100755), each at most 256 KiB.
    • It never reads the working tree, an untracked file, another branch, or a symlink's target. A symlink, an oversize file or an unreadable blob is listed.
    • The commit read is named in the evidence.
  • workflowCheckNames turns the jobs into check names as GitHub names them: plain names or keys, plain matrices, and ${{ matrix.x }} substitutions.
    • Anything it can't name surely is listed for a manual choice ("couldn't map"), never guessed: a matrix with include/exclude, a non-string name:, a reusable workflow, an expression name or matrix, a file that doesn't parse.
    • The block shows each one, so the owner knows a pinned name might also come from it.
  • pinPlan takes the names, deduplicated:
    • confirmed: one file, and its run on main's tip is a job of a run of that file (the workflow-pin test);
    • from the workflow files only: one file, and no run of it on the tip;
    • an app_id pin: only when no file posts the name, the tip was read in full, and exactly one App posts it there that it can prove: its id isn't the Actions App's (15368), and its slug is given and isn't github-actions. The GitHub Actions App is never proposed: any workflow posts as it. An App it can't prove is a manual choice;
    • a manual choice for everything else: two files, a tip that disagrees, a partial tip, a name the schema or YAML can't take (__proto__, over 1000 characters), or a pin the engine's schema refuses.
  • pinBlock and validPins: each comment is one line, with no control or format characters (no terminal escape, no bidi override). doctor offers a block only when it parses as the engine parses config.yaml and passes ProjectConfig.
  • pinNewConfig appends the block to a config.yaml that auto-config (init, add-project) has just scaffolded. It writes nothing unless the whole config still validates, so the block can't make init or add-project fail. Auto-config leaves a repo that already has a config.yaml alone.
  • ghTipRuns resolves main's tip to its sha once, then reads every page of that commit's check runs and workflow runs (up to 10 pages each, 64 MiB buffer), with read-only gh api.
    • A full page with no total_count means it keeps reading.
    • A read past the cap is marked partial.
    • It is used only by the CLI's doctor; tests use fixtures.

How it was verified

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

This PR doesn't change PACKAGE_BUDGET. It fits main's { size: 1_950_000, unpackedSize: 6_060_000 } (#823), so test/package-files.test.ts is main's file exactly.

Adversarial pass (decision guide)

Attack Principle Result (measured) Fixed
An untracked workflow file, or one only on another branch 1 was mapped (reviewer: "new-deps": { workflow: ".github/workflows/untracked-local.yml" }); now never read yes: git at main's tip
A committed symlink to a file outside the repo 1 its job landed in a new project's config (reviewer: SECRET-FROM-OUTSIDE-THE-REPO); now listed a symlink: never followed yes
A workflow file over 256 KiB 1 now listed over 256 KiB, not read yes
An Actions run that fails the workflow-pin test (API-made, another repo) 1 was pinned as { app_id: 15368 }; now a manual choice yes
include/exclude matrices, name: 123, a reusable job, an expression name or matrix, an unparseable file 1 were guessed or dropped (reviewer: test (22), deploy pinned by its key); now each is listed and nothing is pinned for it yes
A duplicate required name; __proto__; a name over 1000 characters; a workflow file name the schema refuses 1 made the block or the new config invalid (Map keys must be unique, must be at most 1024 chars); now deduplicated or listed, and the block validates (test) yes
init/add-project on a repo with a job keyed __proto__, a 1100-character name and a broken file 1 on ce41a61 the proposed config failed to load; now it loads, with the safe pins only yes
The tip's 150 check runs over 2 pages; more than 10 pages 1 only the first 100 were read, and a 1 MB buffer overflowed; now all pages up to the cap, partial marked, no App pin from a partial read yes
A line break, ESC or bidi override in a name or file name 1 line breaks injected a pin (round 1); now every comment is one line with no control or format character yes
Round 2: App 15368 under another slug, a run with no slug, or the Actions slug under another id 1 on 1f5b8df App 15368 with slug something-else became { app_id: 15368 }; now each is a manual choice ("an App it can't prove") yes
Round 2: full pages of runs with no total_count 1 the read stopped as if complete; now it reads on, and past the cap it is partial (no App pin) yes
Round 2: each page read commits/<branch>/check-runs, so the branch could move between pages 1 on 1f5b8df the pages asked commits/main/check-runs; now the tip is resolved to its sha once, and every page reads that commit yes
doctor's comment said an unpinned check is refused 5 contradicted option (c); corrected yes
Does doctor write anything, or block an OFF switch? 2, 6 it prints only. pinNewConfig writes only a config auto-config just scaffolded not needed

Adversarial pass, round 5: could a proposed pin let another source pass?

This covers every way a source other than the pinned workflow file's own latest run could pass a check this block pins. Judging is #788's, merged in at bdfcc29; this PR only proposes pins.

What this can never do

  • Change how any check is judged. It prints a block, and appends one to a config it just scaffolded.
  • Rewrite an existing config.yaml, or write a config the engine refuses.
  • Read the working tree, an untracked file or a symlink's target.
  • Propose the GitHub Actions App as a pin, or guess a pin for a name it can't map surely: those are listed for a manual choice.
  • Write to GitHub. doctor's gh calls are GETs.

Compromised component

  • Someone who can change a workflow file on main: decides what the block proposes for the checks that file posts. A pin trusts main's committed workflow file in any case, and the owner reviews the pasted block in a PR.
  • Another App posting on main's tip: can make a name a manual choice. For a name no workflow file posts, it can be proposed as that name's app_id, and only when the tip was read in full and it is the only App. The block says "from main's tip only", and it applies only once the owner pastes it.
  • Someone with local access to the checkout: an untracked or working-tree file no longer reaches the block.
  • Agents, coordinator, dashboard, other instances, hub viewer: no new reach.

New dependencies

None.

…onfig starts pinned

With #788, a required check nothing pins is judged by name (the owner's option c). To protect an
install, its owner pins the checks. doctor now prints the block to paste (src/check-pin-block.ts):
each unpinned required check mapped to the workflow file whose jobs post it, read from the committed
workflow files (PR-only checks included, matrix names expanded as GitHub names them), confirmed
against main's tip check runs where it has one (the CLI reads them with gh, read-only), and a name
more than one file or source posts, or none, listed for a manual choice, never guessed. auto-config
(init, add-project) appends the same block to a newly scaffolded config.yaml; an existing one is left
alone.

Test (fails without): test/check-pin-block.test.ts, test/auto-config.test.ts "#727: a new project's
drafted config.yaml ... starts with each check its workflow files post pinned", and
test/required-check-source.test.ts "doctor names each required check ... with the block that pins it".

Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
…ke a workflow pin

From an adversarial pass against the owner's decision guide:
- A workflow file name or check name with a line break ended its comment in the pin block and could
  inject a pin into what the owner pastes. Comments are now one line.
- Main's tip runs counted as from a workflow file by their page's path alone. They now pass the same
  test a workflow pin does: this repo, this commit, one of that run's own jobs on its host
  (isJobOfRun).

Test (fails without): test/check-pin-block.test.ts "adversarial pass: a workflow file or check name with
a line break never injects a pin into the block".

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

Copy link
Copy Markdown
Owner Author

Evaluator verdict: CHANGES NEEDED (round 1)

Reviewed at head a70144d (base: #788's branch at d614853). I didn't write it. The newline fix holds, and no hostile name injects a pin. What has to change: the mapping doesn't come from main's committed tree, one path still proposes a pin from a run that failed the workflow-pin test, and several cases are guessed instead of listed for a manual choice. Every finding below comes from fixture inputs fed to the PR's own functions (dist/src/check-pin-block.js), in a scratch worktree with a scratch HOME, state dirs and a fake claude, and a fixture gh for ghTipRuns.

Acceptance (owner's option c)

Criterion Result
Bare names keep working until pinned met (inferred): pinnedChecks is unchanged from #788
doctor prints a paste-ready block mapped from the committed workflow files not met. doctor reads the checkout's working tree (workflowFiles(root), src/doctor.ts:78), untracked files included. Measured: an untracked untracked-local.yml gave "new-deps": { workflow: ".github/workflows/untracked-local.yml" }
PR-only checks (new-deps) included met (measured: from the workflow files only)
Confirmed against main's tip: this repo, this commit, the job-page test partly. ghTipRuns applies the same test as a workflow pin (measured: a run from another repo, another commit, a job page on another host, or an API-made page each gives workflow: null). But such a run then comes back as { app_id: 15368 } (required change 3). The tip is also read only partly (required change 7)
Ambiguous names listed for a manual choice, never guessed partly. Two files and a disagreeing tip are listed (measured). Matrices with include/exclude, a non-string name:, and jobs it can't name are guessed or dropped silently (required changes 4 and 5)
A new install's config gets the block; an existing config.yaml is never rewritten met for never rewriting (measured: the test's merged, main unchanged, and proposeHeldDraft overwrites with the previewed files). Not met for "never makes the draft invalid" (required change 6)
A new-project pin only tightens, and a wrong pin fails closed met (inferred: pinnedChecks turns a wrong pin into missing, and a non-pass from any source still blocks)
Turning auto-merge off and the stop can't be blocked (principle 2) met (inferred): the pins are config data. stop-all, the stop file and the policy kill switch never read them

Hostile names (measured; parsed like the engine: parseDocument(…, { uniqueKeys: true }) + ProjectConfig)

No injection in any of these. Each gave exactly its own key:

  • a newline in a job key;
  • #;
  • quotes and backslashes;
  • the literal text &a *b !!str;
  • a real anchor and alias (name: &n deploy, name: *n);
  • YAML flow syntax ({ app_id: 1 }, [x, y]);
  • x: { app_id: 1 };
  • a leading - ;
  • \r;
  • U+2028, U+2029 and U+0085 (in a manual-choice comment);
  • C0 control characters;
  • a bidi override;
  • a Cyrillic lookalike (dеploy: its own key, pinned to its own file).

Hostile workflow file names were also tried: a newline, NEL, U+2028, and " #. Each stayed inside the quoted value or a one-line comment. These failed instead:

  • A name over about 1020 characters: the block doesn't parse (The : indicator must be at most 1024 chars).
  • A job keyed __proto__: the schema refuses the whole required_check_sources.
  • A required name listed twice: gives Map keys must be unique (required change 6).

Fail-without (measured)

Head checks (measured)

Protection and scope (measured)

Required changes

  1. Map from main's committed tree, not the checkout (owner's option c; principle 1). Read .github/workflows/* with git at the commit doctor confirms against: main's tip sha, or origin/<default> when the tip can't be read. Read regular blobs only (mode 100644/100755), with a size cap like the main watch's 256 KiB. Name that commit in the evidence. Add a test: an untracked file, or one only on a branch, never enters the block.

  2. Never follow a symlink out of the repo. A committed symlink .github/workflows/leak.yml pointing outside the repo is read. Its job names land in the new project's config.yaml, and auto-config commits and pushes that as the setup PR. Measured: "SECRET-FROM-OUTSIDE-THE-REPO": { workflow: ".github/workflows/leak.yml" }. A FIFO or device there would hang or exhaust readFileSync (inferred). Reading blobs from git (change 1) fixes this, for pinNewConfig too. Add a test.

  3. Never propose the GitHub Actions App as an app_id pin (principle 1). An Actions check run that fails the workflow-pin test falls back to App 15368. For a name no file posts, the plan then pins { app_id: 15368 } "from main's tip only". Measured: an API-made run and a run from another repo both gave ["apimade",{"app_id":15368}] and ["otherrepo",{"app_id":15368}]. That turns exactly the run the test rejects into a pin that any workflow, including one a PR adds, satisfies. An unmatched Actions run must be a manual choice. Add a test.

  4. Don't guess matrix names or non-string names (owner: "a matrix job … listed for a manual choice"). Measured:

    • an include that adds a key gives test (22), but GitHub names it test (22, true) (inferred);
    • an include-only matrix gives test;
    • an exclude still lists test (24);
    • name: 123 falls back to the key, so deploy was pinned to a file whose job isn't posted as deploy.

    A matrix with include/exclude (or one whose names aren't certain), and a non-string name:, must go to a manual choice. Add a test for each.

  5. List the jobs it can't name. A reusable-workflow job, an expression name:, an expression matrix and an unparseable file are dropped silently. When another file posts the same name, the block pins it there with no warning. Measured: lint pinned to ci.yml alongside a reusable lint job in call.yml, an expression-named job in pr.yml, and a broken broken.yml. Add a # needs a manual choice comment for each such job or file, so the owner sees the mapping isn't complete.

  6. Never write or print a block the engine refuses (it's what auto-config proposes, and what the owner pastes).

    • Dedupe the names (duplicate required_checks is allowed by the schema).
    • Leave out, as a comment, any name the schema or YAML can't take: __proto__, or over about 1020 characters.
    • Validate the block with the engine's own parse and ProjectConfig before pinNewConfig appends it, and append nothing if it fails.

    Today a repo with a job keyed __proto__, or a name that long, makes propose()'s loadConfig fail, so init and add-project fail for it (measured for pinNewConfig's output, inferred for propose() from src/auto-config.ts). Add a test.

  7. Read the whole tip, or say it's partial. There's no pagination: measured, total_count: 150 with one page read is taken as complete. 100 workflow runs at this repo's measured ~12 KB each overflow execFileSync's default 1 MB maxBuffer, so the tip "couldn't be read" (measured; the real tip at 0b294b5 was 841 KB for 68 runs). Paginate, or drop App pins when total_count exceeds what was read. Raise maxBuffer, or trim with --jq. "Exactly one App on the tip" is a guess on a partial read.

  8. Correct the false claims (principle 5):

    • the body says it "reads the committed .github/workflows/*.yml", but doctor reads the working tree;
    • "What this can never do: Guess a pin for an ambiguous name" is false (changes 3 to 5);
    • "Compromised component: a pin trusts main's workflow file in any case" is false while doctor reads the checkout;
    • src/doctor.ts:76 says an unpinned check "is accepted only from the App GitHub names for it, else refused", which contradicts option c (judged by name).

    Add each of these attacks and its result to the Adversarial pass.

Not required: the comments fold line breaks but pass ESC and bidi characters raw to the owner's terminal. Escaping \p{Cc}\p{Cf} in comments would close that. It's display-only (measured: no YAML effect).

…-727-pin-block

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

# Conflicts:
#	CHANGELOG.md
#	docs/design.md
#	src/check-source.ts
…offer a block the engine refuses

Review round 1 of #803:
- The mapping read the checkout's working tree (untracked files, and a committed symlink's target
  outside the repo). It now reads the workflow files committed at main's tip (or origin/<default>;
  a new project's base) with git: regular blobs only, at most 256 KiB, never a symlink's target.
- An Actions check run that failed the workflow-pin test fell back to the Actions App (15368) as an
  app_id pin, which any workflow satisfies. It is now a manual choice; the Actions App is never
  proposed.
- A matrix with include/exclude, a non-string name, a reusable job, an expression name or matrix and
  an unparseable file were guessed or dropped silently. Each is now listed ("couldn't map"), and a
  pinned name that could also come from one of them is flagged by that listing.
- A duplicate name, __proto__, a name too long for a YAML key, or a pin the schema refuses could make
  the block, and a new project's config.yaml, invalid, failing init and add-project. Names are
  deduplicated, unusable ones become comments, each pin is checked against the engine's schema, and
  pinNewConfig writes nothing unless the whole config still validates.
- The tip's check runs and workflow runs are read every page (up to 10), with a 64 MiB buffer; a
  read past the cap is marked partial, and no App pin is proposed from a partial read.
- Comments carry no control or format characters (no terminal escape or bidi override).
- doctor's comment no longer contradicts option (c).

Test (fails without): test/check-pin-block.test.ts (committed tree only; unnameable jobs listed;
never the Actions App; always-valid block; paging) and test/auto-config.test.ts (init can't fail
because of the block).

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

Copy link
Copy Markdown
Owner Author

Round 2: changes at 598cfc1

Base is #788's 5c4ef10, merged in with no rewrite. Each required change is fixed, with a passing fixture test; the Adversarial pass table in the body has each attack. Labels: measured or inferred.

  1. Main's committed tree only. workflowsAt reads .github/workflows/* regular blobs at main's tip (or origin/<default>; a new project's base) with git, each at most 256 KiB. It never reads the working tree, untracked files, other branches or a symlink's target.
  2. The Actions App is never proposed. An Actions run that fails the workflow-pin test is a manual choice.
  3. Nothing is guessed. include/exclude matrices, non-string names, reusable jobs, expression names or matrices and unparseable files are each listed ("couldn't map"), so a pinned name that could also come from one of them is visible.
  4. Never a config the engine refuses. Names are deduplicated, __proto__ and names over 1000 characters are comments, each pin is checked with the engine's schema, and pinNewConfig writes only when the whole config validates.
    • Measured: init/add-project on a repo with a __proto__ job, an 1100-character name and a broken file now proposes a config that loads. On ce41a61 it proposed one the engine refused (must be at most 1024 chars).
  5. The tip is read in full. Every page is read (up to 10), with a 64 MiB buffer. A read past the cap is marked partial, and no App pin is proposed from it.
  6. Claims corrected, and doctor's comment matches option (c).
  7. Comments carry no control or format characters.

Verification (measured):

  • the fail-without on ce41a61 gave # tests 12 # pass 10 # fail 2;
  • every compiled test except mcp-* and plugin-fetch gives # tests 3549 # pass 3492 # fail 0;
  • typecheck is clean, denylist: 1285 files clean, and DCO: all commits signed off.

@jayden1711

Copy link
Copy Markdown
Owner Author

Evaluator verdict: CHANGES NEEDED (round 3)

I reviewed head 598cfc1a (base: #788's 5c4ef109, merged in) against the round-1 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.

  • Inputs: every finding comes from fixture inputs fed to this head's dist/src/check-pin-block.js, with a scratch HOME, state dirs and a fake claude.
  • Committed tree: a real git tree built in a scratch repo with plumbing (hash-object, mktree; no commit), reached through the injectable Git with a made-up commit id.
  • Tip reads: served by ghTipRuns' injectable api.

All eight round-1 changes are fixed, and every round-1 attack now fails. Three small gaps remain in the tip read and the Actions App test.

Round-1 required changes

# Change Status Evidence
1 Map from main's committed tree only met measured, a real tree. Read: ci.yml (100644), exec.yaml (100755), a 262,144-byte file and paths with a newline or a tab. Listed: two symlinks (/etc/passwd, ../../README.md) as a symlink: never followed, a submodule entry as not a regular file (mode 160000), and a 256 KiB + 1 file as over 256 KiB. Neither symlink target's text reached pinNewConfig's config. A ref git can't read gives null. doctor reads tip.sha, else origin/<default>, never the working tree (src/doctor.ts:79-82)
2 Never follow a symlink met as above. Also, .github/workflows itself a symlink gives no files (measured)
3 Never propose the Actions App partly met (change 1) measured: an API-made Actions run is a manual choice. But App 15368 is proposed when the answer lacks app.slug or has another slug
4 Don't guess matrix names or non-string names met measured: include with an extra key, include-only, exclude and name: 123 each give no names and a couldn't map line. A plain two-axis matrix gives test (ubuntu-latest, 22) and test (ubuntu-latest, 24)
5 List the jobs it can't name met measured: an expression name, a reusable job, a broken file and a file with duplicate job keys are each in unmapped, so pinBlock writes # couldn't map: … (a pinned name could also come from it: check)
6 Never write or print a block the engine refuses met measured: __proto__, 1001 and 100,000 characters, a duplicate required name, newline/#/flow/anchor names and a Cyrillic lookalike each give a block and a pinNewConfig config that the engine's parse (parseDocument(…, { uniqueKeys: true })) and required_check_sources schema accept. ProjectConfig has no cross-field rule on the key (inferred, src/config/schema.ts:127-131)
7 Read the whole tip, or say it's partial partly met (changes 2 and 3) measured: 150 runs over 2 pages gives read 150, partial false. 1050 runs gives read 1000, partial true. A thrown read gives null
8 Correct the false claims met the body and src/doctor.ts match the code. doctor says an unpinned check is judged by name (option c)

Comments (measured)

  • Comments: every comment line is one line, with no \p{Cc}, \p{Cf}, \p{Zl} or \p{Zp}. That's 0 of 8 in a block built from bidi, ESC, U+2028, NEL, ZWSP and BOM names and a hostile skipped path.
  • Not required (display only): a pin key is JSON.stringify(name), which leaves U+202E as is. doctor's line "bidi‮evil": { workflow: … } reaches the terminal with the override. YAML's \uXXXX escapes in a double-quoted key would print it safely and parse to the same name.

This repo at main's current tip (measured)

Fail-without (measured)

  • Method:
    • a scratch worktree at ce41a61a (this branch before the fixes), with test/auto-config.test.ts, test/check-pin-block.test.ts and test/required-check-source.test.ts from 598cfc1a;
    • npx tsc, then the three test files.
  • Result: # tests 33 # pass 30 # fail 3. That is the builder's 12 / 10 / 2 for the first two files, plus doctor's test in the third:
    • auto-config: .worklane/ is invalid: config.yaml: The : indicator must be at most 1024 chars … at line 95;
    • check-pin-block.test.js: does not provide an export named 'NAME_MAX' (a new module's exports: unavoidable);
    • doctor: the old detail has no paste-ready block.
  • On the head:
    • every compiled test except mcp-* and plugin-fetch gives # tests 3549 # pass 3492 # fail 0 # skipped 57, matching the builder;
    • npm run typecheck: exit 0;
    • npm run check:denylist: denylist: 1285 files clean.

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

Required changes

  1. Never propose App 15368, whatever the answer says (principle 1; "What this can never do: Propose the GitHub Actions App").
    • Measured: a tip run app: { id: 15368 } with no slug gives pins: [["noslug",{"app_id":15368}]]. The same with slug: "actions" gives {"app_id":15368}.
    • Why: actions is only app.slug === 'github-actions' (src/check-pin-block.ts:278).
    • Fix: treat id 15368, or a missing slug, as not provable, so the name is a manual choice.
    • Test: both cases.
    • Inferred: GitHub's answer always has the slug, so this is defence in depth, but the claim is absolute.
  2. A full page with no numeric total_count is not the whole tip.
    • Measured: 150 runs, served without total_count, give read 100, partial false. pages() keeps total = 0 and stops after page 1 (src/check-pin-block.ts:258-262).
    • Why it matters: with a second App's run on page 2, "exactly one App on the tip" is decided on a partial read.
    • Fix: keep reading while pages are full, or mark the read partial when total_count is missing.
    • Test: add one.
  3. Read one commit's runs.
    • The problem: ghTipRuns asks commits/<branch>/check-runs for every page, so a page after main moves comes from another commit.
    • Measured: page 1 from aaaa… and page 2 from bbbb… (App 4242's late) give pins: [["late",{"app_id":4242}]], with evidence "from main's tip aaaaaaaa only".
    • Fix: take the sha from the first answer and read commits/<sha>/check-runs for every page. Or drop runs whose head_sha isn't the sha and mark the read partial.
    • Test: add one.

After #788's next round, merge its head again (no rewrite) and re-run the suite.

Not required

  • "Whole config" wording: the body says pinNewConfig "writes nothing unless the whole config still validates". validPins parses the whole text as the engine does but runs only required_check_sources through the schema (src/check-pin-block.ts:211-216). Appending a top-level key can't change another key (inferred), so the guarantee holds. The wording says more than the code checks.
  • Upper-case .YML: a file named so is neither read nor listed (measured). Whether GitHub runs it is a guess.

…-727-pin-block

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

# Conflicts:
#	CHANGELOG.md
#	docs/design.md
#	src/check-source.ts
…e commit, read to the end

Review round 2 of #803:
- An App pin needed only "not flagged Actions": App 15368 under another slug, a run with no slug,
  or the Actions slug under another id could be proposed. Now an App is a pin only when its id isn't
  the Actions App's and its slug is given and isn't Actions'; anything else is a manual choice.
- A full page of runs with no total_count ended the read as if complete. Now it keeps reading, and
  past the cap the read is partial.
- Every page read commits/<branch>/check-runs, so the branch could move between pages. The tip is now
  resolved to its sha once, and every page reads that commit.

Test (fails without): test/check-pin-block.test.ts (unprovable Apps are manual; paging without
total_count; the check-run pages read the resolved sha).

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

Copy link
Copy Markdown
Owner Author

Round 3: changes at bb9ca92

#788's round-4 head is merged in (1f5b8df, no rewrite). Each claim is labelled measured.

  1. An App it can't prove is never a pin. An App pin needs an id that isn't 15368 and a slug that is given and isn't github-actions. Anything else is a manual choice: App 15368 under another slug, a run with no slug, or the Actions slug under another id.
  2. Full pages with no total_count: it keeps reading, and past the cap the read is partial (no App pin).
  3. One tip commit: the tip is resolved to its sha once, and every page reads commits/<sha>/check-runs.

Fail-without on 1f5b8df (measured): # tests 7 # pass 5 # fail 2. App 15368/something-else was pinned, and the pages asked commits/main/check-runs.

After (measured):

  • every compiled test except mcp-* and plugin-fetch gives # tests 3553 # pass 3496 # fail 0;
  • typecheck is clean, denylist: 1285 files clean, and DCO: all commits signed off.

@jayden1711

Copy link
Copy Markdown
Owner Author

Evaluator verdict: PASS

I reviewed head bb9ca924 (base: #788's branch, with #788's e8107eb4 merged in at 1f5b8dff) against the round-3 verdict. Every claim is labelled measured (a command I ran, or CI output I cite), inferred (reasoned from the code, not run) or guess. All attacks feed fixture answers to ghTipRuns' injectable api and to pinPlan, with a scratch HOME, scratch state dirs and a fake claude. No repo file was edited.

All three round-3 changes are met, and every round-3 attack now fails.

Round-3 required changes

# Change Status Evidence (measured, adv-803.mjs)
1 Never propose App 15368; an unprovable App is never a pin met Each a manual choice, with a valid block: App 15368 with no slug (B2), App 15368 with slug actions (B3), and an API-made Actions run (B1). Also manual: App 4242 with slug null or "", the same App with one run missing its slug, and an id given as the string "1111" ("an App it can't prove"). Refused by the schema (manual): ids 0, -1, 1.5 and 1e20. A provable App on a full read is still pinned: B4 gives {"app_id":4242}
2 No total_count: keep reading, or mark partial met No total_count: 150 runs read 150, partial false. 1000 runs read 1000, partial true. 200 runs (page 3 empty) read 200, partial false. With total_count: 1050 runs read 1000, partial true. Round 3's C4: an App run on page 2 with no total_count is now read, giving {"app_id":4242} from the full read (round 3: missed)
3 One tip sha for every page met The paths asked: repos/o/r/commits/main, then commits/<SHA>/check-runs pages 1 and 2, then actions/runs?head_sha=<SHA>. A branch-named page would have served commit bbbb's App run (C5); it is never asked, so late has "no run". A tip answer that isn't 40 lower-case hex (upper case, 39 or 64 chars, a number, null) gives null

Round-3 attacks re-run (measured)

  • B5: two Apps give a manual choice.
  • B6: an App plus an Actions run gives a manual choice.
  • C6: gh failing gives null.
  • Earlier rounds: this round's diff touches only src/check-pin-block.ts (slug, provableApp, pages, ghTipRuns) and its test (measured, git diff --stat 1f5b8dff bb9ca924). So round 1's committed-tree, symlink and comment results stand (inferred; not re-run).

This repo at main's current tip (measured; also #794's evidence)

Fail-without (measured)

  • Method: a scratch worktree at 1f5b8dff, with test/check-pin-block.test.ts from bb9ca924. npx tsc reports 3 type errors (the new slug field) but still emits. Then node --import ./dist/test/isolate.js --test dist/test/check-pin-block.test.js.
  • Result: # tests 7 # pass 5 # fail 2, matching the builder:
    • the plan: … never the GitHub Actions App;
    • the tip is read page by page ….
  • On the head:
    • every compiled test except mcp-* and plugin-fetch: # tests 3553 # pass 3496 # fail 0 # skipped 57;
    • npm run typecheck: exit 0;
    • npm run check:denylist: denylist: 1285 files clean.

Usual checks (measured)

Before the owner merges (procedural)

#788 needs another round (see its verdict). Once #788's final head exists, merge it into this branch (no rewrite) and re-run the suite. This PR's own files need a fresh review only if they change.

Not required (noted)

  • A malformed page: a full page 1 with no total_count, followed by a page 2 that has no check_runs array, reads as complete: 100 read, partial false (measured, E2).
    • ghApi is gh api, which exits non-zero on an HTTP error, so the read becomes null. GitHub's 200 answer always has the array (inferred).
    • A one-line fix would mark a page without the array as partial.
  • "Whole config" wording: round 3's note stands. The body says pinNewConfig writes nothing "unless the whole config still validates"; the code validates required_check_sources after a whole-text parse.

…7-pin-block

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

# Conflicts:
#	CHANGELOG.md
#	docs/design.md
#	src/check-source.ts
#	test/required-check-source.test.ts
@jayden1711

Copy link
Copy Markdown
Owner Author

Round 5 at 49f7ba7: a signed-off merge of #788's round-5 head bdfcc29, with no rewrite and no code changes.

@jayden1711

Copy link
Copy Markdown
Owner Author

Evaluator verdict: PASS

I reviewed head 49f7ba7f: #788's round-5 head bdfcc294 merged into this branch, whose own head was bb9ca924, passed in round 4. Every claim is labelled measured (a command I ran, or CI output I cite), inferred (reasoned from the code, not run) or guess. Attacks feed stand-in answers to ghTipRuns' injectable reader and pinPlan, with a scratch HOME, scratch state dirs and a fake claude. No repo file was edited.

This round is a merge only. It keeps both sides, and this PR's own code is unchanged.

The merge (measured)

  • Parents: bb9ca924 (this branch) and bdfcc294 (Required checks count only from the App or workflow pinned for their name [PROTECTED: owner merges] #788). Their merge base is e8107eb4. git show --remerge-diff 49f7ba7f shows four conflicts:

    • CHANGELOG.md;
    • docs/design.md;
    • the header of src/check-source.ts;
    • one import line in test/required-check-source.test.ts.
  • Both texts kept (keepboth.mjs: every line, or for the two one-line paragraphs every sentence, that each side added against the base must be in the merge, and every one it removed must be gone):

    File This PR's side Required checks count only from the App or workflow pinned for their name [PROTECTED: owner merges] #788's side
    CHANGELOG.md (by sentence) 4 added, 0 lost; 2 removed, 0 left 17 added, 0 lost; 9 removed, 0 left
    docs/design.md (by sentence) 1 added, 0 lost; 1 removed, 0 left 8 added, 0 lost; 4 removed, 0 left
    src/check-source.ts (by line) 2 added, 0 lost 57 added, 0 lost; 13 removed, 0 left
    test/required-check-source.test.ts (by line) the import is merged: checkSourceDoctor from check-pin-block.js, plus Required checks count only from the App or workflow pinned for their name [PROTECTED: owner merges] #788's withTestRuns and JudgedChecks 55 added, 0 lost
  • The two code files: git diff bdfcc294 49f7ba7f -- src/check-source.ts test/required-check-source.test.ts is exactly this PR's own change from round 4 (git diff e8107eb4 bb9ca924 on the same files):

    • the two doctor lines in the header;
    • checkSourceDoctor moved out;
    • the doctor test.
  • The merge message's comment lines (# Conflicts: and the four paths, with tabs) are harmless:

    • git interpret-trailers --parse treats them as comments and still finds Signed-off-by: Jayden Vasquez;
    • scripts/check-dco.mjs reads --no-merges anyway;
    • they hold no attribution and no instruction.

Pins (measured)

  • The pin tests (required-check-source, check-pin-block, pin-engine, required-check-source-coordinator, main-watch): # tests 87 # pass 87 # fail 0, matching the body.
  • Round 4's attacks re-run (adv-803.mjs), all as in round 4:
    • App 15368 (no slug, slug actions, or an API-made run), two Apps, an App plus an Actions run, and every unprovable or schema-invalid id: each a manual choice, with a valid block;
    • a provable App on a full read: {"app_id":4242};
    • paging: 150 runs read in full, 1050 partial, no-total_count pages read on, the tip resolved once to its sha;
    • gh failing gives null.
  • This repo at main's current tip (178c44e6, read-only gh api, now 98 check runs, partial false): Pin this repo's required checks to the workflow files that post them (after #788 and the engine upgrade) [PROTECTED: owner merges] #794's exact 9 pins, 0 manual choices. The config loads with this stack's engine: loads: 9 pins for 9 required checks.
  • Suite: every compiled test except mcp-* and plugin-fetch: # tests 3558 # pass 3501 # fail 0 # skipped 57.
  • npm run typecheck: exit 0. npm run check:denylist: denylist: 1285 files clean.

Usual checks (measured)

Before the owner merges (procedural)

#788 needs another round (see its verdict): its main watch under an App pin must change.

Conflicts in CHANGELOG.md, docs/design.md, the header of
src/check-source.ts and one import in test/required-check-source.test.ts,
each resolved to #788's text plus this branch's doctor text
(checkSourceDoctor stays in src/check-pin-block.ts).

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

Copy link
Copy Markdown
Owner Author

Round 6 at dede6c6: a signed-off merge of #788's round-6 head 961ecba, with no rewrite and no code changes here.

@jayden1711

Copy link
Copy Markdown
Owner Author

Evaluator verdict: PASS

I reviewed head dede6c68, which merges #788's round-6 head 961ecbad into this branch (previous head 49f7ba7f, which passed in round 5). Every claim is labelled measured (a command I ran, or CI output I cite), inferred (reasoned from the code, not run) or guess. Everything ran with a scratch HOME, scratch state dirs and a fake claude. No repo file was edited.

This round is a merge only. It keeps both texts, and this PR's own code is unchanged.

The merge (measured)

  • Parents: 49f7ba7f (this branch) and 961ecbad (Required checks count only from the App or workflow pinned for their name [PROTECTED: owner merges] #788). Their merge base is bdfcc294.

  • Conflicts: git show --remerge-diff dede6c68 lists four, all resolved:

    • CHANGELOG.md;
    • docs/design.md;
    • the header of src/check-source.ts;
    • one import in test/required-check-source.test.ts.
  • Both texts kept (keepboth.mjs: every line, or every sentence for the one-line paragraphs, that each side added against the base must be in the merge, and every one it removed must be gone):

    File This PR's side Required checks count only from the App or workflow pinned for their name [PROTECTED: owner merges] #788's side
    CHANGELOG.md (by sentence) 4 added, 0 lost; 2 removed, 0 left 14 added, 0 lost; 7 removed, 0 left
    docs/design.md (by sentence) 1 added, 0 lost; 1 removed, 0 left 8 added, 0 lost; 4 removed, 0 left
    src/check-source.ts (by line) 2 added, 0 lost (the one "left" removed line is a lone }) 91 added, 0 lost; 47 removed, 0 left
    test/required-check-source.test.ts (by line) the import is merged: checkSourceDoctor from check-pin-block.js, plus Required checks count only from the App or workflow pinned for their name [PROTECTED: owner merges] #788's names without withTestRuns 39 added, 0 lost
  • This PR's own change is the same as before the merge:

    • git diff 961ecbad dede6c68 against git diff bdfcc294 49f7ba7f is identical, hunk for hunk, for src/auto-config.ts, src/check-pin-block.ts, src/cli.ts, src/doctor.ts, templates/project/config.yaml, test/auto-config.test.ts and test/check-pin-block.test.ts;
    • in src/check-source.ts and test/required-check-source.test.ts it is still: the two doctor lines in the header, checkSourceDoctor moved out, and the doctor test.
  • Sign-off: the merge message carries Signed-off-by: Jayden Vasquez (git interpret-trailers --parse).

Pins (measured)

Usual checks (measured)

Before the owner merges (procedural)

#788 needs another round (its verdict lists the changes: the page caps, the status reads and two claim fixes).

Conflicts in CHANGELOG.md and docs/design.md only, each resolved to

Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
#788's text plus this branch's doctor text.
@jayden1711

Copy link
Copy Markdown
Owner Author

Round 7 at 34ea609: a signed-off merge of #788's round-7 head ed9eb36, with no rewrite and no code changes here.

…7-pin-block

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

Copy link
Copy Markdown
Owner Author

Merged #788's 9c1c8bd (a test-only fix for main's no-clock-helpers lint) as 142caff: a signed-off merge, clean. Pin tests (measured): # tests 101 # pass 101 # fail 0; DCO: all commits signed off.

@jayden1711

Copy link
Copy Markdown
Owner Author

Evaluator verdict: PASS

I reviewed head 142caffa. It merges #788's round-7 head in two steps: ed9eb363 as 34ea6091, then 9c1c8bd5 as 142caffa (previous head dede6c68, which passed in round 6). Every claim is labelled measured (a command I ran, or CI output I cite), inferred (reasoned from the code, not run) or guess. Everything ran with a scratch HOME, scratch state dirs and a fake claude. No repo file was edited.

This round is merges only. They keep both texts, and this PR's own code is unchanged.

The merges (measured)

  • Conflicts (git show --remerge-diff):

    • 34ea6091: CHANGELOG.md and docs/design.md, both resolved;
    • 142caffa: none.
  • Both texts kept (keepboth.mjs, by sentence, against the merge base 961ecbad):

    File This PR's side Required checks count only from the App or workflow pinned for their name [PROTECTED: owner merges] #788's side
    CHANGELOG.md 4 added, 0 lost; 2 removed, 0 left 10 added, 0 lost; 5 removed, 0 left
    docs/design.md 1 added, 0 lost; 1 removed, 0 left 5 added, 0 lost; 2 removed, 0 left
  • This PR's own change is the same as before: git diff 9c1c8bd5 142caffa against git diff 961ecbad dede6c68 (hunk bodies) is identical for:

    • src/auto-config.ts, src/check-pin-block.ts, src/check-source.ts, src/cli.ts and src/doctor.ts;
    • templates/project/config.yaml;
    • test/auto-config.test.ts, test/check-pin-block.test.ts and test/required-check-source.test.ts.

    In CHANGELOG.md and docs/design.md, the one paragraph each that both sides edit carries Required checks count only from the App or workflow pinned for their name [PROTECTED: owner merges] #788's new sentences beside this PR's (the table above).

  • Sign-off: both merge messages carry Signed-off-by: Jayden Vasquez (git interpret-trailers --parse). node scripts/check-dco.mjs 9c1c8bd5 142caffa: DCO: all commits signed off.

Pins (measured)

Usual checks (measured)

Before the owner merges (procedural)

A conflict in CHANGELOG.md only, resolved to #788's text plus this
branch's doctor text.

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

Copy link
Copy Markdown
Owner Author

#788's 20b912b merged in as 5412d59, and this PR's budget re-rounded to the next 10,000.

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

# Conflicts:
#	test/package-files.test.ts
@jayden1711

Copy link
Copy Markdown
Owner Author

#788's 9b9f846 merged in as ad0022d, which carries main d5674d0, and the budget raise dropped (the owner's revised package-size practice).

  • The merge: the one conflict was PACKAGE_BUDGET. test/package-files.test.ts is now main's file exactly.
  • Package size (measured, pack dry-run on clean builds): 1,918,697 / 5,990,181. Against main d5674d0 that is +16,539 / +55,770, leaving 31,303 / 69,819 under main's budget.
  • Tests: the pin tests with package-files gave # tests 98 # pass 98 # fail 0.

@jayden1711

Copy link
Copy Markdown
Owner Author

Evaluator verdict: PASS

I reviewed head ad0022d5, a merge of #788's 9b9f846c into the PASSed head 5412d591. I wrote none of it.

  • Labels: every claim is measured (a command I ran, or CI output I cite), inferred (reasoned, not run) or guess.
  • Isolation: scratch HOME and state dirs, and a fake claude. No repo file was edited.

1. The merge brings in only #788 (measured)

2. Package size (measured)

The method: npm pack --dry-run --json --ignore-scripts, run inside each worktree after npm ci, npm run build and npm run build:web. The UI was in each pack.

packed unpacked files
main d5674d04 1,902,158 5,934,411 512
#788 9b9f846c 1,912,424 5,972,971 513
this head ad0022d5 1,918,697 (+16,539) 5,990,181 (+55,770) 514

The per-file diff matches the body:

The budget:

  • Headroom is 31,303 / 69,819, as the body says.
  • The budget is untouched. The "Package size" section claims no raise.

3. Tests and checks (measured)

4. CI on ad0022d5 (measured, gh pr checks 803)

Every required job passes, and so does wizard end to end (real systemd). The other slow jobs were skipped by scope.

5. Hygiene (measured)

Merge order (inferred)

  1. After Required checks count only from the App or workflow pinned for their name [PROTECTED: owner merges] #788.
  2. GitHub retargets this PR to main once Required checks count only from the App or workflow pinned for their name [PROTECTED: owner merges] #788 merges.
  3. Then Engine updater counts a required check only from its pinned workflow or App [PROTECTED: owner merges] #789 (after its fix round), then Pin this repo's required checks to the workflow files that post them (after #788 and the engine upgrade) [PROTECTED: owner merges] #794 (a draft until the engine upgrade).

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>
Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>

# Conflicts:
#	CHANGELOG.md
…owner)

Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
This PR's only note was its edit of #788's Upgrading bullet (the paste-ready block). With notes in fragments (#870), that edit is made in changelog.d/788-pin-required-checks.md, byte for byte as before; CHANGELOG.md is #788's exactly, and this PR adds no fragment of its own (it adds no note line of its own).

Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
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>
Base automatically changed from issue-727-pin-checks to main October 21, 2026 00:59
@jayden1711
jayden1711 merged commit 4027466 into main Oct 21, 2026
27 of 28 checks passed
@jayden1711
jayden1711 deleted the issue-727-pin-block branch October 21, 2026 01:14
jayden1711 added a commit that referenced this pull request Oct 21, 2026
…gh child-process.ts (#883)

#834 (src/auto-merge-resume.ts, spawn of git ls-remote) and #803 (src/check-pin-block.ts, execFileSync of git
and gh) landed on main with direct node:child_process imports, which #883 requires to go through
src/os/child-process.ts like every other module. The guard's import tree is now 60 modules with main's
src/ack-reason.ts (#834) and this PR's src/os/child-process.ts, so #883's note says 60, not 59.

Test (fails without): test/child-process.test.ts "#846 backstop" fails on the merge commit with exactly
src/auto-merge-resume.ts:14 and src/check-pin-block.ts:13.

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