Repository navigation
DO NOT MERGE: combined CI for batch 17 (#927) - #1025
Draft
jayden1711 wants to merge 30 commits into
Draft
jayden1711 wants to merge 30 commits into
jayden1711 wants to merge 30 commits into
Conversation
…ts off refs/replace/* and $GIT_DIR/info/grafts are repo state every worktree shares, so an agent can write either, and every git the engine ran there read the agent's stand-in: a `git replace` of the pushed commit hid a refused file and a big one from pushProblems (2 problems became 0), and a graft that made the base reach the head emptied the range (2 became 0). `--no-replace-objects` alone still honours grafts (measured on git 2.43). src/os/git.ts is now the one way src/ runs git: every call gets `--no-replace-objects` and GIT_NO_REPLACE_OBJECTS=1, and GIT_GRAFT_FILE names a path that can't exist, set last over the caller's environment. A runner that takes any command (the setup probes' and init's run) gives git the settings by its name (commandSpawnSync). scripts/lib/git.cjs holds the same settings for scripts; the files that run on their own (the push gate and its hook, new-deps, the test-run VM script) write them out, and the hook hands them to the gate's environment. gitleaks, which runs git itself, gets them in its environment, in the engine's scan and in the push gate. Display and prompt readers follow the issue: the owner's decision diff, the fix-forward culprit diff and the patch hash read every file's lines whatever the change's attributes say (--text, no textconv, no external diff). The evaluator's sources line (src/roles.ts) needs the owner's role eval and the no-newline size count needs the owner's formula: both stay for the owner. test/git-runner.test.ts fails on a git call in src/ or scripts/ that doesn't come through a runner (a scan, a backstop next to the runner, with its exceptions registered as a never-grow table), and is locked with the runner itself. Test (fails without): test/git-runner.test.ts: the replace route and the graft route on pushProblems (each 2 problems became 0 on main), the runner scan (81 files bypassed it on main), the gitleaks and push-gate replace routes. Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
#883 moved every child_process import in src/ to src/os/child-process.ts. Each conflict was an import line: the git calls keep the git runner, the other calls take #883's module, and src/os/git.ts now starts its processes through src/os/child-process.ts too (test/child-process.test.ts's backstop). The guard's tree lists both modules, the locked tests list both PRs' entries, and the root updater's new ls-remote call is counted in the git runner scan. Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
…the test header says what it measures (#807) With a type-only import first, tsc dropped auto-merge-resume.ts's leading comment from its build (measured: 1,428 bytes gone from dist/src/auto-merge-resume.js). The value import goes first again. test/git-runner.test.ts's header now gives the counts its tests measure (2 problems became 0, for each route). Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
…927) The release note for the git runner: what an upgrade changes (a new patch_hash for the same change, the optional setup:hooks rerun) and the guardrail it adds. Test (fails without): none, release notes only; test/changelog*.test.ts pass with it. Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
CI was red at a8d9c98 on six files that expected the old argv or copied fixed file lists: - intake-coordinator expects `git --no-replace-objects merge-base ...`; - testrun-measure's pinned import list for measure-cli swaps ../os/child-process.js for ../os/git.js (a swap, not a loosening: os/git.ts starts its processes only through os/child-process.ts); - compare-screenshots, eval-spend-gate, engine-release and installed-engine-config copy scripts/lib/git.cjs beside the scripts they copy. Test (fails without): the six files, red in run 39540570972; 59 of 59 pass with this. Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
A `shallow` file lists commits whose parents git treats as missing, and any agent can write one into the shared repo. With the head's parent listed, pushProblems read 0 problems for a range that added a refused file; deleted again before the push, the push carried that file to the remote (measured by #927's final check). shallowCut refuses a range such a commit cuts, before and after the walks and once more right before the push in checkedPush; a shallow file it can't read is a refusal too. Only a cut inside the range is refused: the harness-eval replay's clone is shallow at the task's base (measured), and a depth-limited clone pushes changes above its cut as before. Not --shallow-file: git's internal. The git-runner scan's comment lists its measured misses, and the release note names the shallow refusal. Test (fails without): test/push-shallow.test.ts, 3 of 4 fail at a8d9c98 (a hand-written cut: 0 problems; the delete-between race; an unreadable shallow file); the depth-limited clone control passes either way. Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
A forged parent link in the shared objects/info/commit-graph made a walk skip the commit that added a refused file, and the push then landed it (measured by #927's round-2 check). -c core.commitGraph=false joins the runner's fixed arguments, and gitEnv appends the same pair to GIT_CONFIG_COUNT after the caller's own, for the gits a program runs itself (gitleaks). scripts/lib/git.cjs, the four standalone scripts and the push gate's gitleaks env do the same. Measured cost: about 30 ms more for three walks over this repo's 2,903 commits. Test (fails without): test/push-snapshot.test.ts's commit-graph cases (push limits, checkedPush, gitleaks) fail at a1ff009; test/git-runner.test.ts holds the settings and their copies. Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
git reads a shallow file more loosely than round 2's exact match (an uppercase id, text after the id, a line over 1 KB each cut a range), and a shallow file toggled every ~3 ms beat round 2's before-and-after checks (23 of 150 pushProblems runs; 5 of 80 checkedPush calls landed the refused file; both measured by the round-2 check). gitSnapshot (src/os/git.ts) is a private GIT_DIR over the repo's objects (GIT_OBJECT_DIRECTORY) with one copy of its shallow file, read once and parsed strictly: any line that isn't exactly a full lowercase object id is refused. pushProblems resolves base and head in the repo, takes one snapshot, checks the cut and runs every range walk through it; checkedPush takes a fresh one right before the push; scanRange runs gitleaks through one, and a cut inside the range is never "clean". Documented environment only, not --shallow-file. Test (fails without): test/push-snapshot.test.ts, 9 of 9 fail at a1ff009 (four shallow-line forms, the mid-walk toggle for pushProblems and checkedPush, the forged graph for the limits and gitleaks, gitleaks under a cut). Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
…only (#927) CI at 3955c23 failed test/child-env-sites.test.ts (#846): with GIT_CONFIG_COUNT on Object.prototype, gitEnv read it through the prototype, numbered its pair 1 and left pair 0 unset, so git exited 128. The count is now read only when it is the environment's own key; the same in scripts/lib/git.cjs and the push gate. Test (fails without): test/child-env-sites.test.ts (2 cases, red in run 39549011913) and a polluted-prototype case in test/git-runner.test.ts. Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
…, nothing else (#927) shallowIds took any 40- or 64-hex line as an id, whatever the repo's format. In a sha1 repo git reads a 64-hex line as a cut at its first 40 hex digits, so the line hid the commit that added a refused file from pushProblems, and the snapshot copied the same cut. A process toggling the line got the file onto the remote in 34 of 80 checkedPush calls (measured by the round-3 check). In a sha256 repo git refuses a 40-hex line, and pushProblems threw instead of refusing. shallowIds now takes the format gitSnapshot already reads and accepts exactly 40 lowercase hex digits for sha1 and 64 for sha256; any other line, or a format it doesn't know, is a refusal. The new test file also holds round 2's 1 KB form (an id outside the range, 983 characters, then an id inside it), which git reads as a cut of its own. Test (fails without): test/push-shallow-format.test.ts. At 6aa0853, 4 of 6 fail: the two 64-hex lines in a sha1 repo (expected 1, actual 0), the line removed right before the push's spawn ({"ok":true}, the push landed), and the sha256 repo's 40-hex line (a throw). At a1ff009, the 1 KB test fails for its own reason (expected 1, actual 0). Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
src/push-check.ts: #928's names from plumbing (rangeNames: rev-list into diff-tree -z), its crafted-name refusals and its 1 MB SKILL.md read (blobsByName) are kept. Their git calls go through the runner (the local git helper is the runner's gitExecFileSync, not #928's execFileSync with --no-replace-objects) and through the push limits' snapshot: rangeNames and blobsByName take an optional env, and judge passes snap.env. src/coordinator/change-files.ts: run, runBytes and #928's plumbingBytes take that env; plumbingBytes runs through gitSpawnSync instead of a raw spawnSync('git', ...). Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
…says it did (#927) push-snapshot's two toggle tests wrote the cut only while `git log --name-only` ran, through execFileSync. After the main merge the push limits read names with #928's rev-list and diff-tree, through spawnSync, and no `git log` is left in push-check.ts or change-files.ts (measured by grep), so the hook never fired and the tests passed without toggling anything (inferred). The hook now wraps execFileSync and spawnSync, toggles during every rev-list and diff-tree (and the old log walk), and each test asserts it fired. Test (fails without): no src change. The hook's own `fired > 0` assertion fails if it reaches no walk. A scratch probe (names-probe.mjs) measured that the same toggle hides keys/secret.pem from rangeNames without the snapshot's env, and that with the env it is listed. Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
src/merge-policy-weakening.ts: main's version, whole. #886 replaced the central CHECK_TABLES and LINT_TESTS lists with in-file marks, so this branch's three registrations move onto marks in test/git-runner.test.ts: `// check-table: never-grow` before OUTSIDE_THE_RUNNER and before RUNNERS, and `// lint-test` at the top. None is dropped: a registry comparison (central lists plus marks) gives main 78, this branch before the merge 78, the merge 81, and lost 0 against either. src/coordinator/change-files.ts: #886's filesWithText ran a raw spawnSync('git', ...); it runs through gitSpawnSync, as the runner scan requires. Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
No conflict. #947 brought scripts/bench/pull.ts's git helper, a raw execFileSync('git', [...GIT_HARDENED, ...args], { env: GIT_ENV }) that the runner scan refuses (measured on this merge before the fix: "every git call in src/ and scripts/ goes through the runner" fails with "scripts/bench/pull.ts: starts git 1 time(s) (not on the list)"). It now runs through gitExecFileSync with both lists kept: GIT_HARDENED as arguments after the runner's, and GIT_ENV as the env, over which the runner sets its own settings last (gitExecFileSync applies gitEnv). Not added to OUTSIDE_THE_RUNNER, which is never-grow. Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
One conflict, test/review.test.ts's GUARD_TREE list: the union of both sides. This branch adds src/os/git.ts; Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com> #976 adds src/guardrails/reach.ts and src/os/process-links.ts. No entry from either side is dropped.
src/review.ts: the engine-guards lock list takes the union, test/git-runner.test.ts and #920's test/plan-decisions.test.ts, each with its comment. src/plan-decisions.ts (#920, locked as the planners' decisions index): its two raw spawnSync('git', ...) calls, the object read (cat-file --batch) and committedRecords' rev-parse helper, run through gitSpawnSync, which the runner scan requires. Their own --no-replace-objects, the object-id checks and the hash re-check stay as they were; the runner adds grafts and the commit-graph off. No OUTSIDE_THE_RUNNER entry, and no plan-decisions test changed. Measured on this merge before the routing: git-runner's "every git call ... goes through the runner" fails with "src/plan-decisions.ts: starts git 2 time(s) (not on the list)". Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
No textual conflict, but a semantic one in src/decision-detail.ts: #921 (#889) imports only redactCut there and redacts every shown diff before cutting it, while this branch's maxBuffer branch of decisionDiff still called redactString(stdout.slice(0, MAX)), which the merged tree can't compile and which cut a token across the 512 KiB cut before redacting it. That branch now uses redactCut(stdout, MAX) too; redactString is not re-imported. Test (fails without): test/decision-diff-overflow.test.ts, a real diff over the 8 MiB buffer with a token the cut lands four characters past `ghp_` in. At 5d8c98a (slice, then redact) the shown diff ends in "+ghp_0054"; here no part of any token is shown. Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
gitEnv read the caller's GIT_CONFIG_COUNT with Number(), which accepts what git's strtoul doesn't: `0x1`, `1e0` and `1 ` became 1 and `abc` became 0, so the runner turned git's "bogus count" refusal into a run, and for the first three into a run that applied the caller's first pair (measured by #927's second review, A8; #1001 item 1). A count is now read as git reads it (optional leading whitespace and `+`, then digits; empty is 0). Any other count is left as it is, with no pair added, so git still refuses it; the fixed arguments still turn the commit-graph off for the runner's own gits. The same in scripts/lib/git.cjs and in the push gate's graphOff (scripts/push-gate.mjs). Test (fails without): test/git-config-count.test.ts. At 5d8c98a, 7 of 12 fail: the seven refused counts (`0x1`, `1e0`, `1 `, `abc`, `-1`, `1.5`, a lone space) got a pair and a new count; the five accepted forms pass either way. Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
…IT_CONFIG_PARAMETERS (#927) Round 8 read GIT_CONFIG_COUNT the way it took git's strtoul to: optional whitespace and `+`, then digits. It missed the minus sign. git reads `-0`, ` -0` and `-00` as 0 and runs, while round 8 left them alone with no pair, so a git that reads only its environment (gitleaks') ran with the commit-graph on (measured by both round-8 reviews). And git reads GIT_CONFIG_PARAMETERS (what `git -c` hands the gits it runs) after the COUNT pairs, so a caller's `git -c core.commitGraph=true`, which the agent guardrail allows, turned it back on for the push gate's gitleaks (measured). In all three copies (src/os/git.ts gitEnv, scripts/lib/git.cjs gitEnv, the push gate's graphOff): - only a canonical count, a plain decimal 0 to 9999, gets the runner's pair after the caller's. Any other value becomes `refused`, a count git refuses on every platform, so the git fails closed instead of guessing git's parse; - `'core.commitGraph=false'` is appended to GIT_CONFIG_PARAMETERS, which git reads last; - no early return, so a later tail (#944's guardEnv) still runs. test/git-runner.test.ts's two expected gitEnv objects gain the GIT_CONFIG_PARAMETERS entry; nothing else in it changed. Test (fails without): test/git-config-count.test.ts, rewritten to the reviewers' tables and run through real git, with the push gate's graphOff taken from its source. As a new file in a fresh worktree at 7696ed8, 44 of 46 fail: every non-canonical count (36), every canonical one (5, no GIT_CONFIG_PARAMETERS), and the three GIT_CONFIG_PARAMETERS cases. graphOff alone, from 7696ed8's source, is wrong on 37 of 40 cases and on 0 here. Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
… view (#927) The decision diff is read with `--text` (#807 F2), so a binary file's bytes are shown. UTF-16 text puts a NUL between ASCII characters, and no redaction rule matched it: a token in a UTF-16 file read whole once the NULs were dropped, where main shows "Binary files differ" (measured by round 8's second review). That reopened something main closed. Both branches of decisionDiff now drop every NUL before redacting and cutting. `--text` stays: a NUL, or a `-diff` attribute, is also how a change hides its lines from review as "binary", which F2 closes. Test (fails without): test/decision-diff-utf16.test.ts, a token in a UTF-16LE and in a UTF-16BE file. As a new file in a fresh worktree at 7696ed8, both fail: the shown diff, NULs dropped, holds `ghp_`. Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
…, its text lines do (#927) Round 9 showed the `--text` diff with its NULs dropped. That showed a binary file's printable bytes (a DER private key's), and it glued byte runs together, so a fixed-length key shape a NUL had ended lost the boundary its redaction rule needs (AWS, Google, 0x hex; and `\b` before `sk-`, Stripe). main shows "Binary files differ" (both round-9 reviews, measured and inferred). diffTextView reads git's bytes (latin1 from the runner, one character per byte). A file whose diff lines hold a NUL, invalid UTF-8 or a control byte text doesn't hold is binary, and each of its lines shows only as text it decodes to: UTF-32 or UTF-16 when its NULs fall as mostly-Latin text's do, else UTF-8 of at least 16 characters, each NUL run as U+2400, a boundary for every rule. Any other line becomes `[binary: N bytes not shown]`. Every other file shows as it is. `--text` stays (#807 F2: a NUL or a `-diff` attribute must not hide a text change from review). Test (fails without): test/decision-diff-binary.test.ts. As a new file in a fresh worktree at 4e4287e, 5 of 7 fail: the DER keys (ed25519 PKCS#8, RSA-2048 PKCS#1; no 6-byte run of their printable bytes may show), an AWS, a Google and a 0x hex key shape between NULs with a letter after (shown in the clear there), and a text file with one NUL (its NUL must show as a mark). The two UTF-32 cases pass there too; they pin that this view still reads UTF-32. Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
…e header (#927) CI at 633b03d failed test/change-files-renames.test.ts's "one helper is the only way src/ lists changed names or counts" in shard 2/4 on Node 22 and 24: diffTextView split the diff into files at their `diff --git` header lines, which that lint's "patch headers" words find, and src/decision-detail.ts isn't on its never-grow list. The view now decides per hunk, from its `@@ ` line to the first line that doesn't start with `+`, `-`, a space or `\`, and reads no file header. A hunk is one file's, so what it shows is the same. The lint is unchanged. Test (fails without): test/change-files-renames.test.ts's lint, as CI ran it at 633b03d. test/decision-diff-binary and test/decision-diff-utf16 still pass. Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
No conflict. The runner scan finds no git start outside the runner in the merged tree (git-runner passes), and Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com> #918's src/signed-receipts.ts comment no longer matches it.
…nd the binary view go (#927) The owner, 2026-10-21: "Cut --text, land the runner." Every required change in both round-10 reviews came from `--text` and the view that tried to show a binary file's text lines safely: a UTF-32 line that crashed it, a hunk header's function-context bytes, diff.suppressBlankEmpty ending a hunk early, and the fix-forward culprit diff carrying a binary file's bytes into a tracker issue. - src/decision-detail.ts: decisionDiff runs main's `git diff --no-color --no-ext-diff base..head` through gitExecFile; diffTextView and its decoders are removed. A binary file shows as "Binary files ... differ", as on main, and output past the buffer is no diff, as on main. - src/coordinator/merge-flow.ts: the culprit diff is main's `git diff <sha>^1 <sha>` again, through the runner (gitOrNull), not patchText. patchText (`--text`) stays for the patch hash only, which is hashed, never shown. - The PR's own tests for the removed behaviour go: test/decision-diff-binary, decision-diff-utf16 and decision-diff-overflow (none is on main). - changelog.d/927-git-runner.md: scoped to the JS and TS code the scan reads, the .sh helpers and the old hook named, the strict GIT_CONFIG_COUNT named, and the two diffs said to be as before. Test (fails without): test/binary-diffs-as-main.test.ts. Each diff shows "Binary files /dev/null and b/blob.bin differ" for a binary file and a text file's lines. As a new file in a fresh worktree at 0b1c7da, 2 of 2 fail. Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
…s comment to the hash (#927) Wording only, no logic change. The round-11 final check: the replace-refs claims hold on git 2.42 and later (measured on 2.43); before 2.42 a config `core.useReplaceRefs=true` may turn replace refs back on (inferred). The changelog fragment now says so. patchText's doc comment said its patch was for a person, a prompt or a hash; since the cut it is for the patch hash only, never shown (both reviewers). Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
Conflicts, each resolved to keep both sides: - .worklane/review.yaml: the engine-guards list keeps test/git-runner.test.ts and #903's vacuity tests. - src/coordinator/change-files.ts: the runner's import; main's local SizeFile replaces the pr-rules import. - src/coordinator/merge-flow.ts: the runner's imports with main's randomBytes; isAncestor keeps main's arguments through gitSpawnSync. - src/scan/secrets.ts: main's new content-based scanCommit (#979) whole; the runner's imports for scanRange. - src/worktrees.ts: main's removed() split, with the runner's calls. Every raw git start the merge brought now goes through the runner, with the same arguments and options: merge-flow.ts (the tip fetch, sync and async; merge-base; rev-list --merges; vacuityFor's git; branch -D; the changed-lines spawn), scan/secrets.ts (gitOut, headId, the core.fsmonitor read), worktrees.ts (removeWorktreeAsync), hook.ts (gitQuery) and vacuity.ts (baseTextReader). No entry is added to the scan's exception list. Its src/scan/secrets.ts entry shrinks from 2 starts to 1, since #979 took the commit scan's gitleaks `git` call out; the pin on that call is replaced by one on scanRange's `--log-opts` snapshot range. Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
…stop's process (#927) CI at 802150f failed #746's desktop backstop in every shard: "src/os/git.ts runs in this backstop's process, so it must be a guard file" (test/desktop-removed.test.ts). After the merge with main, src/merge-policy-weakening.ts reads changed files through src/coordinator/change-files.ts, which now runs git through the runner, so a changed line of src/os/git.ts could change what a backstop reads. GUARD_FILES gains 'src/os/git.ts' (an addition only), next to src/os/child-process.ts, so such a line waits for the owner. Test (fails without): test/desktop-removed.test.ts's "every module this backstop runs ... is held", as CI ran it at 802150f; it passes here. Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
Keep the policy snapshot from #1006 and the worktree ownership, locking, and read-denial protections from #893. Route the added git calls through the existing runner without changing arguments or options. No exception-table entry or test assertion changes. Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Never merged. This draft runs combined CI for the next merge batch. The orchestrator closes it and deletes the temporary branch after the batch completes.
Measured: built on main
0f3370f91412b1668a02bdbfde8323345155eaba, merging PR #927 at exact head021f6f18cca0458d9a4508b6df748bd266d7fcf4with a signed-offgit merge --no-ffand no conflicts. Combined commitab7b405c3c15f9272acabc8af361b4a8719be264; tested treee541143afa9dd40282e305148cce2f0568505247.021f6f18cca0458d9a4508b6df748bd266d7fcf4e541143afa9dd40282e305148cce2f0568505247#927 is critical. Its exact-head independent final and second verification reviews, exact-head required CI, and this combined CI must all pass before any merge. Reviews and CI are still being checked; opening this draft asserts no completed gate.
The implementation integrates the four PRs already landed on main while preserving their policy snapshot, worktree ownership/locking and read-denial protections. The added git starts use #927's existing runner. Builder measurements: isolated build/typecheck and 40 named integration tests pass, no skips. Independent reviewers verify their own evidence separately.