Skip to content

git: one runner for every engine git call, with replace refs and grafts off (#807) [PROTECTED: owner merges] - #927

Open
jayden1711 wants to merge 29 commits into
mainfrom
issue-807-git-runner
Open

jayden1711 wants to merge 29 commits into
mainfrom
issue-807-git-runner

Conversation

@jayden1711

@jayden1711 jayden1711 commented Oct 21, 2026 •

Copy link
Copy Markdown
Owner

Intent: no git that the engine's JS and TS code runs can be steered by refs/replace/* or $GIT_DIR/info/grafts. That is repo state every worktree shares and any agent can write.

  • One runner, src/os/git.ts (and scripts/lib/git.cjs for scripts), starts every such git with --no-replace-objects, GIT_NO_REPLACE_OBJECTS=1 and a GIT_GRAFT_FILE that can't exist, set last over the caller's environment.
  • Scope: measured on git 2.43 only. That it holds from git 2.42 on is inferred, not run. Before 2.42, a config core.useReplaceRefs=true may turn replace refs back on (inferred, not run; follow-up 10).
  • The root updater (scripts/machine/worklane-update.cjs) runs alone and sets only --no-replace-objects on its verification clone: no graft file, and the commit-graph stays on. That clone is root's own (inferred: no agent can write a graft or commit-graph there). See Exceptions and follow-up 11.
  • A scan in test/git-runner.test.ts fails on a git call that bypasses the runner in the JS and TS files under src/ and scripts/. It is a backstop next to the runner, not the protection.
  • Outside the runner: the .sh helpers aren't scanned and run git outside it. So does an install's old push-gate hook, until setup:hooks reruns (What remains).
  • The owner's decision diff and the fix-forward culprit diff are as on main, through the runner. Only the never-shown patch hash reads every file's lines (F2).

Risk class: protected. It touches:

It also touches src/merge-policy-weakening.ts, the merge-policy registry. At 2172591 that is one addition to GUARD_FILES, src/os/git.ts (see below). Its check registrations moved to #886's marks at the 7d90d65 merge. The owner merges.

Part of #807: F1 (the runner) and, of F2, the patch hash. Left for the owner, as the issue says: the role-prompt line in src/roles.ts (it needs the owner's role eval with a real model) and F4 (the no-newline size count; the owner picks the formula).

Current integration head: 021f6f1

Measured during recovery: this signed-off merge incorporates main 0f3370f9 after #1006, #997, #893 and #910 landed. It is now pushed to the PR branch. CI and the two independent PASSes on the earlier 2172591b head do not certify this merge.

  • Five conflict resolutions preserve main's regular-file config reads, worktree unlock/ownership/placeholder behavior, and Merge policy: read the money-path list and weakening marks at one default-branch commit (#1005) [PROTECTED: owner merges] #1006's single policy snapshot (policy.moneyPaths).
  • The added git calls in worktree locking/removal and alternate-store enumeration use the existing runner with their arguments and options unchanged. No scan exception or test assertion changed.
  • Measured: isolated build and typecheck pass; denylist reports 1639 files clean. Eight named integration files run 40 tests, all passing, with no skips: git-runner, worktree-lock, worktree-lock-stale, read-denials-git-alternates, read-denials-r20-kept, merge-policy-snapshot, pr-hygiene-coordinator and checks-from-feedback-coordinator. Logs are in the recovery scratch directory work/codex-927-merge/.
  • Two independent read-only delta reviews PASS on exact 021f6f18: main's protections are preserved and new git calls retain their arguments/options through the runner. Neither reviewer independently reran the 40 integration tests on this merge; those remain builder-measured.
  • The push gate measured one commit and one tip clean during the push. Still required: current-head CI and a fresh combined check on current main. No release is authorized.

Previous integration head: 2172591

2172591: the git runner is a guard file

  • The failure: CI at 802150f failed Remove the desktop app, its source, Tauri lockfile and docs; keep the desktop check one more release as a no-op (#744) [PROTECTED: owner merges] #746's desktop backstop in every shard, with "src/os/git.ts runs in this backstop's process, so it must be a guard file" (test/desktop-removed.test.ts; measured, gh run view 39730685942).
  • Why: after the merge, src/merge-policy-weakening.ts reads changed files through src/coordinator/change-files.ts, which runs git through the runner. So the runner runs in the backstop's process.
  • The fix: GUARD_FILES gains 'src/os/git.ts', next to src/os/child-process.ts. This is an addition only, and it tightens: a changed line there now waits for the owner.
  • Measured locally: desktop-removed, merge-policy, review, git-runner and coordinator pass (252, 4 skipped). Typecheck is clean.
  • That backstop step ran before the shard tests, so CI at 802150f ran no shard tests. CI at 2172591 is below.

The landing merge (802150f)

802150f merges main 03c0cb8 (#746, #979, #903), signed off. It answers the final check. It is a landing step, not new design.

  • Conflicts, each resolved to keep both sides:

  • Raw git starts the merge brought, now through the runner with the same arguments and options:

    File Calls
    src/coordinator/merge-flow.ts the tip fetch, sync and async (gitExecFileSync, gitSpawn); merge-base; rev-list --merges; vacuityFor's git; branch -D; the changed-lines spawn
    src/scan/secrets.ts gitOut, headId, the core.fsmonitor read
    src/worktrees.ts removeWorktreeAsync
    src/hook.ts gitQuery
    src/vacuity.ts baseTextReader
  • The scan's exception table: no entry was added. The src/scan/secrets.ts entry shrinks from 2 starts to 1, because guard: the commit scan covers every commit form and checkout the guard allows [PROTECTED: owner merges] #979 took the commit scan's own gitleaks git call out. Its pin on that call is replaced by a pin on scanRange's --log-opts snapshot range; the pin on gitEnv(env) stays.

  • Measured at 802150f:

    • The scan copy reports files 527 unaccounted 0.

    • The registry has 131 registrations against main 03c0cb8's 128, and against 35f873e's 110. Lost 0 both ways; the 3 extra over main are this PR's marks.

    • Tests:

      Batch Result
      the 9 adversarial/commit-scan* tests and commit-plan 63 of 63
      every merge-vacuity-*, merge-policy*, vacuity and package-files test 352 pass, 16 todo (named limits), 0 fail
      git-runner, review, merge-policy 89 of 89
      the push tests, push-gate, push-gate-hook, change-files-renames, git-config-count, binary-diffs-as-main, child-process, boundaries, plan-decisions, intake-coordinator 258 of 258
      hook, adversarial/tool-guard-writes, guard-reach 14 of 14
    • Typecheck is clean, and check:denylist reports 1577 files clean.

  • CI at 802150f (measured): red in the desktop backstop only, fixed in 2172591 (above). I didn't rerun it.

  • CI at 2172591 (measured from GitHub's check-runs API during session recovery): 28 success, 3 skipped (publish, bootstrap, bootstrap (windows-wsl)), 0 pending or failed. All eight required checks succeeded on this exact head. Nothing was rerun. The saved local test counts above are builder-reported measurements from the interrupted session; they were not rerun during recovery.

Before it: the cut and the wording (35f873e)

The owner, 2026-10-21: "Cut --text, land the runner."

  • 99b27ec, the cut. Both round-10 reviews asked for changes, and every required item came from --text and the view meant to show a binary file's text lines safely (final check, second review).
    • src/decision-detail.ts: decisionDiff runs main's git diff --no-color --no-ext-diff base..head, now through gitExecFile. Against main, the file differs in the import, the call, and the function's doc comment, which grew from one line to four to quote the owner (measured, git diff origin/main).
    • diffTextView and its decoders are gone. A binary file shows as "Binary files ... differ", and output past the buffer gives no diff, both as on main.
    • src/coordinator/merge-flow.ts: the fix-forward culprit diff is main's git diff <sha>^1 <sha> again, through the runner (gitOrNull).
    • Removed tests, none of them on main: decision-diff-binary, decision-diff-utf16 and decision-diff-overflow.
    • New test, test/binary-diffs-as-main.test.ts: both diffs show Binary files /dev/null and b/blob.bin differ and a text file's lines, and neither shows the binary file's printable run. Fail-without (measured): written as a new file into a fresh worktree at 0b1c7da, 2 of 2 fail.
    • CI at 99b27ec: 28 pass, 3 skipped, 0 fail (measured).
  • 03b281e: merges main 8df2304 (Receipts: signed, chained merge receipts anyone can verify offline (#717 slice 1) [PROTECTED: owner merges] #918), with no conflict.
  • 35f873e, wording and comments only (the round-11 final check; the second review passed 99b27ec).
    • The replace-refs claims are scoped to git 2.42 and later, here and in changelog.d/927-git-runner.md.
    • patchText's doc comment says its patch is for the hash only, never shown. No logic changed.
  • Re-measured at 35f873e:
    • 191 of 191 pass over change-files-renames, the three changelog tests, git-runner, review and merge-policy. Typecheck is clean, and check:denylist reports 1534 files clean.
    • At 99b27ec: 345 tests pass (1 skipped) over the runner, push and lock tests, and 70 pass over the decision, fix-forward and changelog tests.
    • At 35f873e: the scan copy reports files 524 unaccounted 0, and the registry probe loses 0 against main 8df2304.
    • fix-forward-coordinator fails locally the same way on main 9f9533a, so this machine can't run it. It passes in CI.
  • CI at 35f873e (measured, gh pr checks 927): green. 28 pass, 3 are skipped (publish, bootstrap, bootstrap (windows-wsl)), 0 fail. Nothing was rerun.

What changed

How it was verified

Every fail-without was measured by writing the tests as new files into a fresh worktree at the earlier head, then building and running them by name. Each run used a scratch HOME, state, TMPDIR and git config.

Test Fails at Result there
git-runner (replace ref, graft, gitleaks, push gate, scan) main 41f6dd4 10 of 11 fail
push-shallow (a cut in the range, the delete-between race) a8d9c98 3 of 4 fail
push-snapshot (shallow forms, mid-walk toggle, forged commit-graph, gitleaks) a1ff009 9 of 9 fail
push-shallow-format (64-hex in sha1, 40-hex in sha256, the 1 KB form) 6aa0853 / a1ff009 4 of 6 / 5 of 6 fail
git-config-count (strict count, GIT_CONFIG_PARAMETERS, all three copies) 7696ed8 44 of 46 fail
binary-diffs-as-main (both diffs as main) 0b1c7da 2 of 2 fail

The registry probe was run on every main merge. At 35f873e it counts 110 registrations against main 8df2304's 107, and against 0b1c7da's 110: lost 0 both ways, and the 3 extra are this PR's marks (measured). The scan copy reports files 524 unaccounted 0 at 35f873e (measured). CI runs the full suite.

Package size

Measured with npm pack --dry-run --json, no web build.

  • Worktrees at main 8df2304 and at 99b27ec: the whole PR is +6,245 bytes in the tarball (1,567,258 to 1,573,503) and +17,391 unpacked, with one new file. 35f873e changes comments only.
  • That is 0.4% over main, inside the 5% headroom. PACKAGE_BUDGET (2,170,000 / 6,750,000) needs no change (inferred: CI adds the web build).

What this can never do

  • Turn replace refs or grafts back on for any caller of the runner. Measured on git 2.43; that this holds from git 2.42 on is inferred, since only 2.43 was run. The settings go last over the caller's environment, and --no-replace-objects is in the argument list, which no environment takes back.
    • Round 10's second review measured six forms on 2.43 and found none could: core.useReplaceRefs=true in repo config, in a caller's GIT_CONFIG_PARAMETERS, or by -c; GIT_REPLACE_REF_BASE; through the runner and through an env-only git.
    • Before 2.42, a config core.useReplaceRefs=true may turn replace refs back on (inferred, not run). The by-design fix is follow-up 10.
  • Change what git reads in a repo with neither replace refs nor grafts, beyond the commit-graph.
    • Corrections: rounds 8 and 9 found this false for a caller's GIT_CONFIG_COUNT git refuses or reads as -0, and for a caller's GIT_CONFIG_PARAMETERS (from a guardrail-allowed git -c, in the pushing agent's own environment).
    • Since 550323c, the strict count and the parameters entry close both (measured, git-config-count).
  • Loosen a check: no existing test, guard or table loses a line. The scan's exception table is new and never-grow.
  • Break an existing install's push: the push gate imports nothing new.
    • The fail-closed cost: an environment holding a count git accepts that isn't canonical (empty, +1, 1, 01, -0, 10000 and up) now fails every push through the gate and every git the runner starts.
    • Nothing in the engine writes such a count (inferred, grep).

Compromised component

  • Agent user: can still write refs/replace/*, info/grafts, .git/shallow and the commit-graph in the shared repo.
    • Within the scope above, replace refs, grafts and the commit-graph no longer change what a runner git reads.
    • A .git/shallow still does, as on main. Measured by the round-11 final check: gitSpawnSync(['rev-list', '--count', 'HEAD']) gives 5, and 3 with a shallow file at HEAD~2.
    • The push limits and the secret scan read one checked copy of the shallow file, and refuse a cut in the pushed range.
    • Correction (the trim): the body briefly said none of the four changes what a runner git reads. That was false for .git/shallow.
    • Correction (round 3): "a line git might read differently" was false for a 64-hex line in a sha1 repo. Fixed in round 4: 0 of 80 pushes under a toggler.
    • It can make pushProblems throw rather than refuse, by toggling a shallow line git rejects. Nothing is pushed (follow-up 2).
  • Coordinator: runs every git through the runner; nothing new it can do.
  • Dashboard: the decision diff is main's (owner-only, redacted, capped at 512 KB), now through the runner.
    • Correction: rounds 1 to 10 read it with --text, and a binary file's bytes reached the owner's page in several forms. The cut ends that (measured, binary-diffs-as-main).
    • Still open, as on main: a hunk with a key's body lines and no BEGIN line (follow-up 1).
  • Fix-forward tracker issue: the culprit diff is main's again. A binary file is "Binary files differ" (measured).
  • Other instances, hub viewer: no new surface.
  • Someone with push access: could add a git call in a file the scan can't read (follow-up 5). A change to the scan's exception table waits for the owner.

Adversarial pass

Every attack ran in a scratch repo or with fixtures, never on real state. Details are in each round's review comment (History).

  1. Replace ref or graft on the pushed range: on main, the problems went from 2 to 0; now they stay 2. gitleaks and the push gate script also read the real commits (measured, tests).

  2. A caller's env turning it back on (GIT_GRAFT_FILE, an empty env, GIT_CONFIG_*, GIT_CONFIG_PARAMETERS): the runner's settings win (measured).

  3. .git/shallow in all its forms:

    • a cut in the range, and one written and removed mid-walk (a toggler every ~3 ms);
    • uppercase ids, text after the id, a 1 KB line, a 64-hex line in sha1, a 40-hex line in sha256.

    Each is refused, and 0 toggled pushes landed (measured).

  4. A forged commit-graph parent link: it is no longer read, by the push limits or by gitleaks (measured).

  5. 36 GIT_CONFIG_COUNT values and malformed GIT_CONFIG_PARAMETERS, through real git, in all three copies: none runs with the commit-graph on, and none applies a caller pair git would refuse (measured).

  6. Scan bypasses: a name built at run time, a ternary, a shell string, a returned name, and the .sh files are not caught (named in the test's comment; follow-up 5).

  7. Binary files on the owner's diff and in the tracker: found again and again while --text was in place, and closed by cutting it (measured, binary-diffs-as-main).

Exceptions this adds (principle 9)

The scan is new, so it removes no check. Its exception table is where a review should look. Each entry says why the file starts git outside a runner:

  • runners that take any command (src/init.ts, src/setup-steps.ts, src/setup-mode.ts, src/setup-actions.ts, src/starter-ci.ts, src/testrun/host.ts, and three CI and dev scripts);
  • four scripts that run on their own with the runner's settings written out (scripts/push-gate-hook.mjs, scripts/push-gate.mjs, scripts/ci/new-deps.mjs, scripts/ci/testrun-real-systemd.mjs);
  • the root updater (scripts/machine/worklane-update.cjs), installed alone. As the scan's entry says (test/git-runner.test.ts:231), its reads of the verification clone carry VERIFY_GIT_ARGS, which is --no-replace-objects only: replace refs off, but no graft file, and the commit-graph isn't turned off. That clone is root's own (inferred: no agent can write a graft or commit-graph there). Follow-up 11;
  • text that names git without starting it (src/scan/secrets.ts, src/review.ts, src/plugins/approval.ts, src/updater-setup.ts, src/os/index.ts, scripts/verify-release-tag.mjs).

No main merge added an entry: every raw call that came in was moved onto the runner instead.

Files other lanes own

Import-line and call-site changes only: Lane B (src/setup-*.ts, src/init.ts, src/doctor.ts), Lane E (scripts/bench/*), Lane A (most of the rest).

Overlap

All measured by git merge-tree --write-tree against 802150f, with the scan copy run on each merged tree. On a tree that has conflicts, the scan reads the conflict markers too, so its count there is approximate.

PR (head) Conflicts Scan on the merged tree
#944 (fb913e4, stacked) src/os/git.ts, src/scan/secrets.ts src/scan/secrets.ts 2
#910 (359a59b) none 0
#893 (3c6c9e1) src/config/load.ts, src/coordinator/checks-from-feedback.ts, src/coordinator/merge-flow.ts, src/coordinator/pr-hygiene.ts, src/worktrees.ts raw git starts in checks-from-feedback.ts, merge-flow.ts, pr-hygiene.ts, read-denials.ts and worktrees.ts

What remains

  • Owner: rerun npm run setup:hooks after this merges. Until then, an install's old copy of the push-gate hook runs its own git calls without the runner's settings, including the one that loads the gate. So a replace ref could make an old hook load a substituted gate (inferred).
  • The impact is low: the hook is a client-side layer the agent can skip (--no-verify), and the engine's pushes go through checkedPush.

History

Each round's reviews are linked. All claims above are current, and each correction sits next to the claim it fixed.

Round Head What it did Reviews
1 a8d9c98 the runner, the scan, F2
2 a1ff009 a shallow cut in the range is refused check
3 6aa0853 one snapshot per check; the commit-graph off check
4 541c642 shallow ids of the repo's own format; main merged (#928, #919) second review
5 0c158ca registrations as #886's marks; #947's git onto the runner final, second, body
6 and 7 5d8c98a main merged (#976, #920); #920's git onto the runner second, final
8 7696ed8 #921 merged; a count git refuses stays refused final, second
9 4e4287e strict count; GIT_CONFIG_PARAMETERS final, second
10 0b1c7da a binary view for the decision diff, superseded by the cut final, second
the cut 99b27ec --text goes from both shown diffs final, second
wording 35f873e git 2.42+ scope; patchText's comment final, second

Follow-ups (not required)

None of these breaks something that works on main, reopens a route main closed, or rests on a claim this body still makes. Each title is a separate issue:

  1. Redactor: a diff hunk with a private key's body lines but no BEGIN line shows them. It is the same on main.
  2. Push limits: a toggled shallow line git rejects makes pushProblems throw instead of refusing. It fails closed; a fix moves commitIds and sizeFiles onto the snapshot.
  3. Show binary files' text lines safely (the case --text was meant to cover). Round 10's reviews list what such a view must handle: function-context headers, diff.suppressBlankEmpty, long UTF-32 lines, and the tracker body.
  4. git: an instance's git runs nothing its checkout's config names, and .git/ is the coordinator's (#926) [PROTECTED: owner merges] #944: guardEnv has the GIT_CONFIG_PARAMETERS class of its own. A caller's parameters are read after GIT_GUARD_PAIRS (inferred).
  5. Runner scan: git in .sh files, in shell strings, and in a returned name (return 'git').
  6. Shared object store: forged pack bitmaps and multi-pack-index.
  7. Push gate and its hook take no snapshot of the history.
  8. docs/design.md: name the git runner.
  9. Decision diff: pin -c diff.suppressBlankEmpty=false, as src/comment-policy.ts does. It is the same on main.
  10. Replace refs on git before 2.42.
    • By design: add -c core.useReplaceRefs=false to the runner's fixed arguments (and GIT_CONFIG_PARAMETERS).
    • Or measure whether --no-replace-objects already wins there, or state a minimum git version.
    • Not done in this round: the owner's cap allowed wording only.
  11. The root updater's verification clone (scripts/machine/worklane-update.cjs) sets no GIT_GRAFT_FILE and leaves the commit-graph on. Only --no-replace-objects is set (VERIFY_GIT_ARGS). The clone is root's own bare repo (inferred), so no agent can write either there.
    • The fix: give VERIFY_GIT_ARGS the runner's full settings.
    • The changelog's "every git the engine's JS and TS code runs ignores replace refs and grafts" should name this exception. This round's push was wording only on other lines, so it doesn't yet.
    • Round 11's second review, follow-up 3.

New dependencies

None.

…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>
jayden1711 added a commit that referenced this pull request Oct 21, 2026
The same lock, at another place in each list: next to the push tests in src/review.ts and in engine-guards, so it
doesn't sit on the lines #927 adds its own entry to (git merge-tree measured a conflict there).

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

Copy link
Copy Markdown
Owner Author

Evaluator verdict: CHANGES NEEDED (round 1/2)

Final check of PR #927 at head a8d9c98 (a8d9c98ccab2aa8aede80e0cd0265ad23987f6ff), base main 41f6dd4. Probes ran in scratch repos with a temp HOME and TMPDIR, never on real state.

The runner itself holds up. Every way I tried to turn its settings back off failed. The blocker is CI: it is red at this head because of this PR. Six test files fail where fixtures or expectations weren't updated for the runner.

1. Replace and graft routes

Reader Replace ref Graft Evidence
pushProblems closed closed measured: test 1 and 2 pass at the head. At 41f6dd4 both fail with expected: 2, actual: 0
push gate (scripts/push-gate.mjs --range) closed closed (inferred: every git it runs, and gitleaks, gets GIT_ENV) measured: test 4. At 41f6dd4 it fails (expected: 0, actual: 0: the gate passed the decoy)
gitleaks (scanRange) closed closed measured: test 3 (expected: 'leaks', actual: 'clean' at 41f6dd4). Graft probe at the head returns leaks (plain rev-list is empty)
change-files already closed on main closed measured: under a graft, changedNames(c3, {tree}) listed keys, other.txt, weights.bin at 41f6dd4 and only other.txt at the head. changedNames and sizeFiles over base..head are right under a replace ref at both

Fail-without (measured): I wrote test/git-runner.test.ts byte-identical to the head's copy (sha256 05a691b3…) into a fresh worktree at 41f6dd4, then built it and ran node --import ./dist/test/isolate.js --test dist/test/git-runner.test.js. Result: 10 of 11 fail. The only one that passes is the scan's fixture test, as the builder said. Tests 5-7 fail on Cannot find module …/dist/src/os/git.js, which is expected because they import the new runner. The scan test fails on main with the bypass list. At the head the same file passes 11 of 11 (25 of 25 with child-process.test.js).

2. Can a caller turn the settings off? (each one probed through gitSpawnSync at the head, git 2.43.0)

All of these were measured. In the scratch repo a replace ref and .git/info/grafts were both active, and plain git returned an empty range.

  • Caller env GIT_GRAFT_FILE=<repo grafts> and GIT_NO_REPLACE_OBJECTS='': closed. The runner's values go on last.
  • -c core.useReplaceRefs=true later on the command line: closed.
  • --replace-objects: error exit 129: unknown option. There is no such option.
  • Env GIT_CONFIG_PARAMETERS and GIT_CONFIG_COUNT/KEY/VALUE setting core.useReplaceRefs=true: closed.
  • core.useReplaceRefs=true in the repo's own (agent-writable) .git/config: closed on 2.43. Guess: git before about 2.42 let that config turn replace refs back on after --no-replace-objects. CI uses git 2.55 (measured from the job log), so this matters only for old local git.
  • GIT_REPLACE_REF_BASE, GIT_DIR, --git-dir, GIT_COMMON_DIR: closed.
  • .git/info/grafts read from a linked worktree (common dir): closed. pushProblems there returns 2.
  • An alias that runs git again (alias.zz=!git log …): closed. The nested git takes the env.

3. The scan

Named gaps, fed to the scan's own patterns as fixtures (measured): none of these is caught: a name built at run time ('gi'+'t', ${'git'}), a ternary on either side, a command from config or env, and git in a shell string. Gaps the test and body don't name are not caught either: cmd ?? 'git', cmd || 'git', bash -lc 'git …', 'cd x && git log', execSync('cd x && git log'), spawnSync('git log', {shell:true}), an object property (tools.vcs), and String.raw\git`. Sixteen .shfiles underscripts/run git and are not scanned (measured). The biggest arescripts/machine/worklane-testrun.sh(18 lines),scripts/ci/testrun-network-real-systemd.sh(13) andscripts/ci/egress-real-systemd.sh` (12). Inferred: the machine helpers read stores that a root or test user owns, filled from engine-made bundles, not the shared repo. The protection is the runner, by design, so these gaps are acceptable for a backstop. They should be listed, though (advisory A1).

Exceptions (21 entries). I printed every line the scan matches in each listed file (measured):

  • The runners that take any command (init.ts, setup-steps.ts, setup-actions.ts, starter-ci.ts) each spawn through commandSpawnSync (init.ts:658, setup-steps.ts:100, setup-actions.ts:152, starter-ci.ts:170). setup-mode.ts runs through systemIo(). testrun/host.ts:556 passes gitArgs and gitEnv by hand, and the clone script's env is gitEnv(...) (host.ts:548). Justified.
  • Text-only matches (review.ts attribute "way", plugins/approval.ts literal, updater-setup.ts comment, os/index.ts Git for Windows bash lookup, verify-release-tag.mjs -n git): none of them starts git. Justified.
  • The four standalone scripts each write the settings out. The pins in must match the source.
  • Root updater (scripts/machine/worklane-update.cjs, n 7): this PR doesn't touch the file (measured: no diff). Its seven matches are four VERIFY_GIT_ARGS calls on the verification clone and three ls-remote calls, which read no local object. It lacks GIT_GRAFT_FILE. Inferred justified: VERIFY_GIT is a bare clone that root owns, created by root (init --bare, line 1516). A graft there needs root write, and fetch never carries info/grafts. Its BUILD_SCRIPT shell string (git clone "$1", line 963) isn't counted (the shell-string gap). It clones from that root clone, and a clone carries neither refs/replace nor info/grafts (inferred). Going from 6 to 7 in the merge follows main's added ls-remote. The table is new in this PR, so nothing grows relative to main.

4. The open item: a hand-written .git/shallow

Measured against a local bare remote, at the head:

  • pushProblems returns 2 problems, then 0 with .git/shallow naming the head (the builder's finding reproduces).
  • Pushing with the file still in place: ! [remote rejected] … (shallow update not allowed). That part of the claim is true.
  • But after .git/shallow is removed post-check, the same push exits 0 and the remote holds keys/secret.pem.

Inferred: checkedPush (src/push-check.ts) runs pushProblems and then git push as separate processes. The coordinator's change check (src/coordinator.ts:1493) runs even earlier. Any agent that shares the common dir can delete the file in that window. So "the push it would allow is then refused by the remote" holds only while the file stays in place through the push. GitHub: inferred that it refuses the static case the same way (shallow update not allowed is its receive-pack refusal). The race doesn't depend on the remote at all.

Measured: git --shallow-file <missing path> rev-list base..H reads past the hand-written file (2 commits). It's an internal, undocumented option, so whether it stays stable is a guess.

Fail-closed options: refuse in pushProblems when rev-parse --is-shallow-repository says true, checked after the walks. Or read with a missing shallow file. Either one costs a legitimately shallow checkout its push, so it is a new cost: owner decision (flagged, not required here). The body's claim must be corrected (R2).

5. The merge (af25516) and its 54 conflicts (measured)

git merge-tree --write-tree 33ff97b4 41f6dd42 gives tree e4551c8a with exactly 54 conflicted files. Compared with that tree, the merge's tree differs in those 54 files plus two others. For each conflicted file I compared the PR's own added and removed lines (merge-base → 33ff97b) with the resolution (41f6dd4 → af25516):

The commits after the merge touch only auto-merge-resume.ts (import order), the test's header comment and the changelog fragment.

6. Lane B/E files (measured)

src/setup-actions.ts, src/setup-steps.ts, src/init.ts, src/doctor.ts and scripts/bench/grade.ts change only import lines and git call lines, plus three one-line comments naming the runner. src/setup-mode.ts is unchanged.

7. Other checks

Required changes

  1. Make CI green. Each failure below comes from this PR (measured, run 39540570972):
    • test/intake-coordinator.test.ts:174: the expected spawn list lacks --no-replace-objects (execFileSync git --no-replace-objects merge-base --is-ancestor …).
    • test/testrun-measure.test.ts:239: the pinned import list of measure-cli has '../os/child-process.js' where it now has '../os/git.js'. This is a guard test, so say in the body that the change is a swap, not a loosening (os/git.ts imports only child-process and node: modules).
    • test/compare-screenshots.test.ts:35, test/eval-spend-gate.test.ts (stale-dist), test/engine-release.test.ts:100 and test/installed-engine-config.test.ts:187 copy a fixed file list that now lacks scripts/lib/git.cjs, so each fails with ERR_MODULE_NOT_FOUND …/scripts/lib/git.cjs or Cannot find module '../lib/git.cjs'. Add it to each fixture. In the body, name the production paths and why they already have the file. Inferred: engine.sh runs engine-release.cjs from the cloned repo (scripts/setup/engine.sh:33-37), and the workflow archives the whole scripts dir (.github/workflows/installed-engine-config.yml:62-66).
    • Then run every test file that imports or copies a script now requiring scripts/lib/git.cjs, or that pins spawn args or imports.
  2. Correct the shallow claim (principle 5) in "Compromised component" and adversarial item 7. The remote refuses the push only while .git/shallow stays in place. Measured: with the file removed after the check, the push succeeds and the remote holds the refused file. Inferred: an agent can do that between pushProblems and git push. File the follow-up issue with this race and the fail-closed options from section 4, for the owner to decide.

Advisory (not blocking)

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>
@jayden1711

Copy link
Copy Markdown
Owner Author

Round 2 (head a1ff009), addressing issuecomment-6307895829.

1. CI fixtures (measured). The six files now pass by name, 59 of 59:

  • intake-coordinator now expects --no-replace-objects in the git argv.
  • testrun-measure's pinned import list swaps ../os/child-process.js for ../os/git.js. This is 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. Fixture changes only.

2. Shallow fails closed.

  • The fix: shallowCut in src/push-check.ts refuses a range that a shallow boundary cuts. It runs before the walks, again after them, and once more in checkedPush right before the push. A shallow file it can't read is refused too.
  • Fail-without (measured): the new file test/push-shallow.test.ts was written into a fresh worktree at a8d9c98: 3 of 4 fail (the hand-written cut gives 0 problems, the delete-between race isn't refused, an unreadable shallow file isn't refused). The --depth 1 control passes either way. All 4 pass here.
  • One deviation from the brief, with why: the brief said to refuse whenever --is-shallow-repository is true; I refuse only a cut inside the range. The harness-eval replay's clone is shallow (measured: fetch --depth 1, then clone, gives true), so refusing every shallow repo would refuse every replayed change. Every cut that can hide a commit of base..head is itself a commit of that range (inferred).
  • Engine checkouts: the instance checkout is a full clone (measured: checkout.sh passes no --depth), and the agents' worktrees share its history. Genuinely shallow checkouts can push only changes that lie wholly above their cut.
  • Residual (inferred, not measured): a cut written and removed again inside the walk window isn't seen. Closing it needs --shallow-file, which I didn't use, so it's left for the owner.
  • Claim corrected: the body's earlier "the push is refused anyway" line is corrected in adversarial 7.

3. Advisory. The scan's measured misses are listed in the test comment and the body. The body has a "What remains" line about the hook copy and setup:hooks.

4. #928 (0b1b060). git merge-tree conflicts in src/push-check.ts only (measured). The resolution is noted in the body; not resolved here.

Other checks. Main hasn't moved (41f6dd4), and merge-tree against it is clean (measured). 378 tests over 12 files: 374 pass, 4 skipped. Typecheck clean; denylist reports 1400 files clean. Size: tarball +3,342 bytes and unpacked +9,024 bytes against main.

@jayden1711

Copy link
Copy Markdown
Owner Author

Evaluator verdict: CHANGES NEEDED (round 2/2)

This is the delta final check of PR #927 at head a1ff009 (a1ff009105217da61ad7cf65a18cbe9d11946cf9). The previous head was a8d9c98 and the base is main 41f6dd4. Every probe ran in scratch repos with a temp HOME, TMPDIR and state dir, never on real state. The probes are in the session scratchpad under work/eval-927g/: shallow-probe.mjs, graph-probe.mjs, snapshot-probe.mjs and merge-prs.mjs.

In short: the round-1 findings on the fixtures and the claim are fixed. The "cut inside the range" rule is sound as a rule. The code that applies it has two holes, though, and both were measured: git reads the shallow file more loosely than shallowCut does, and a toggled cut gets past the before/after checks often enough to put a refused file on the remote. There is also one new route of the same class, a forged commit-graph, which the runner doesn't turn off.

1. Round-1 findings

Finding (issuecomment-6307895829) State at a1ff009 Evidence
R1. CI red from 6 test files fixed locally; CI still pending measured: push-shallow, git-runner, push-check, intake-coordinator, testrun-measure and compare-screenshots ran by name, 58 of 58 pass. I left engine-release, installed-engine-config and eval-spend-gate to CI because they set their own PATH with /usr/bin or spawn the eval script. CI's installed-engine-config job passes (measured).
R2. Shallow claim corrected, follow-up named claim corrected; the residual is mislabelled measured: the body now says the push is refused only while the file stays. Its residual, "inferred, not measured", is now measured and exploitable (section 2).
A1. Scan gaps named done measured: the diff of test/git-runner.test.ts is the docstring only. No pattern, table or assertion changed.
A2. Old hook copy done "What remains" names it.

I re-ran the round-1 reviewer's probe (work/eval-927f/probe.mjs) at the head (measured). Every replace and graft route is still closed. pushProblems with a hand-written .git/shallow now returns the cut refusal (1) instead of 0.

2. The deviation: refuse only a cut inside base..head

The rule (inferred, with measured cases). Shallow only removes parent edges, so the base side can only exclude less, never more. Take any commit of base..head that a cut hides. The first shallow commit on the path from head down to it is reachable from head in the cut view, and the base can't reach it. So rev-list base..head lists that cut, and the rule holds as git defines shallow. Measured with p0 → base → K (adds keys/secret.pem) → H (removes it):

Case Result
No shallow file key found
Cut at the base allowed, key found (nothing hidden)
Cut below the base allowed, key found
Cut at K (in range) refused: cut
Cut at the head refused: cut
Graft head → base (grafts off in the runner) key found (plain git counts 1)
Shallow file mode 000 refused: cut (fails closed)
No trailing newline refused: cut

Why the deviation is needed: the harness-eval replay's clone is shallow. The builder measured that; I did not re-measure it. I accept the deviation as a rule. Its implementation fails in two ways, both measured:

(a) git reads the shallow file more loosely than shallowCut. shallowCut matches each trimmed line exactly against rev-list's lowercase ids. Git takes the first 40 hex characters of each line, in any case, ignores the rest of the line, and reads the file in 1 KB chunks. For each of the four lines below, plain git counts 1 commit in base..head instead of 2, and pushProblems returns 0 problems, with the key missed:

  • <HEAD IN UPPERCASE>
  • <head>zz
  • <head> comment
  • a 1,063-character line: <p0> + 983 × x + <head>, where git's 1 KB read puts the head on a line of its own.

git rev-parse --is-shallow-repository says true for the uppercase-plus-word line. Git sees the cut and shallowCut doesn't. This is a deterministic route, not a race.

(b) The toggle window is real and lands the file on the remote. A child process wrote and removed .git/shallow (naming the head) every ~3 ms.

  • Over 150 pushProblems runs, 23 returned 0 problems, 122 refused the cut and 5 found the key.
  • Over 80 checkedPush calls (the remote is a local bare repo), 71 refused the cut, 4 found the key and 5 pushed. Afterwards the remote holds keys/secret.pem.

The body labels this residual "inferred, not measured". It is measured, and an agent can repeat its push until it wins. The brief's version (refuse whenever --is-shallow-repository is true) has the same window (inferred). The window comes from the checks and the walks reading the file in different processes, not from the deviation.

Closing it by design (measured, in snapshot-probe.mjs). The walks have to read the same snapshot that was checked. Two ways:

  • Documented env only: read the walks through a private GIT_DIR (an empty bare repo) with GIT_OBJECT_DIRECTORY set to the shared objects. The shared repo's cut is then ignored (2 commits, and log --name-only sees the key). With the checked snapshot copied in as that private dir's shallow, a real depth-1 clone still walks its range.
  • --shallow-file <snapshot>, git's undocumented option, also gives 2. Whether to rely on it is the owner's call; the private-GIT_DIR route needs no such decision.

Base and head must be resolved to ids first, because the private dir has no refs (inferred).

3. A new same-class route: a forged commit-graph (measured)

The repo's commit-graph file sits in the shared objects dir (an agent can write it, inferred) and git trusts its parent links. Setup: base → K (adds the key) → D (drops it) → H. I wrote a real graph, then pointed D's first parent at the base in the graph's CDAT chunk and recomputed the checksum. Results:

  • rev-list base..head through the runner counts 2 instead of 3, so K is hidden. With -c core.commitGraph=false it counts 3.
  • pushProblems returns 0 problems.
  • checkedPush fails only because the local remote's connectivity check finds K missing.
  • Removing the graph after a 0-problem check, the runner's push exits 0 and the remote holds keys/secret.pem. This is the same race shape as round 1's shallow finding.
  • git commit-graph verify flags the lie, but nothing in the engine runs it.

The issue's outcome is "no git reader in the engine can be steered by repo-controlled git state", and the commit-graph is that kind of state. It wasn't named in the issue, so it isn't a regression: main has the same hole (inferred). It is the class this PR exists to close (principle 4).

4. Re-measured fail-without (round 2)

I wrote test/push-shallow.test.ts (sha256 47ab92da…, identical to the head's) into a fresh worktree at a8d9c98, built it and ran node --import ./dist/test/isolate.js --test dist/test/push-shallow.test.js. Result: 3 of 4 fail, matching the builder (measured):

  • Test 1: expected: 1, actual: 0. This is the real bug.
  • Test 2: expected: true, actual: false. The race is not refused.
  • Test 3, a directory in place of shallow, fails only on the message. At a8d9c98 the key was already found (actual: 'touches paths that are never pushed (keys/secret.pem)…'), so the directory was never a hole. The body's "didn't fail closed" overstates it (principle 5).
  • Test 4, the depth-1 control, passes.

At the head, 4 of 4 pass (measured).

5. Changed existing test lines (weakening gate)

All measured from git diff a8d9c98c a1ff0091:

  • test/intake-coordinator.test.ts: the exact spawn list now expects git --no-replace-objects merge-base …. It is still a deepEqual of the whole list, so it is not loosened.
  • test/testrun-measure.test.ts: the pinned import list swaps ../os/child-process.js for ../os/git.js. This is a swap: measure-cli.ts imports only ../os/git.js and has no direct spawn left, and src/os/git.ts imports only ./child-process.js and node: modules (measured by grep). The reach deny list is unchanged.
  • compare-screenshots, eval-spend-gate, engine-release and installed-engine-config: each adds scripts/lib/git.cjs to the files a fixture copies. These are fixture-only changes, and no assertion changed.
  • test/git-runner.test.ts: docstring only.

None is loosened.

6. Body claims vs code

  • "Agent user … the one residual is a cut written and removed … (inferred, not measured)": false as labelled. It is measured and exploitable (2b). The parse gap (2a) is not mentioned.
  • "Every cut that can hide a commit of base..head is itself a commit of that range": true for git's semantics (inferred, section 2), but shallowCut doesn't read the cut set the way git does (2a).
  • "What this can never do: turn replace refs or grafts back on": still holds (measured, round-1 probe re-run).
  • "What remains" (the hook copy): accurate (inferred).

7. Other checks

  • npm run typecheck: clean. npm run check:denylist: 1400 files clean. Both measured at the head.
  • Size: dist/src/push-check.js grows from 6,150 to 8,391 bytes, +2,241 (measured). The builder's tarball figure is +3,342. CI's package-budget test is in shard 1, which passed (measured).
  • Commits fee5bb1 and a1ff009 are signed off and carry no attribution trailers (measured). No dependency changes.

git merge-tree --write-tree against a1ff009 (all measured; the raw git calls come from merge-prs.mjs over each PR's added lines in src/ and scripts/):

PR Merge-tree result Raw git calls the PR adds that must move onto the runner
origin/main 41f6dd4 clean n/a
#928 0b1b060 (the head named in the brief) conflict in src/push-check.ts none
#928 now 0286164 conflict in src/push-check.ts, imports and the SKILL.md loop. To resolve: keep all three shallowCut calls, take rangeNames/blobsByName, and drop the ./os/child-process.js import (no execFileSync/spawnSync call is left in the merged file, measured by grep) none (its one hit is a message string)
#944 fc552f5 clean. It is stacked on 927's round-1 head a8d9c98, not on a1ff009 none outside the runner. Its new spawnSync/execFileSync('git'…) lines are inside src/os/git.ts itself
#893 48d425f clean src/read-denials.ts:134 spawnSync('git', ['-c','core.quotePath=true','--git-dir', common, …]) through ./os/child-process.js
#903 9a27323 conflicts in .worklane/review.yaml and src/coordinator/merge-flow.ts src/coordinator/merge-flow.ts:1604 execFileSync('git', args, …) and :1668 spawnSync('git', ['branch','-D', branch], …)
#921 8633356 conflict in src/merge-policy-weakening.ts none

Inferred: whichever of #893 or #903 lands after this PR fails test/git-runner.test.ts's scan until those calls use gitSpawnSync/gitExecFileSync.

CI at a1ff009 (runs 39545170872 and 39545170912, not rerun; measured with gh pr checks):

  • Pass: test shards 1-3 on Node 22 and 24, shard 4 on Node 22, scan, dco, push-gate, new-deps, denylist, agentshield, installed-engine-config, desktop, egress firewall, wizard e2e and the sign-in probe. Shard 1 carries the package-budget test.
  • Still pending: shard 4/4 on Node 24, test (ubuntu-latest, 22), build, add-project and the test-run sandbox (real systemd).

Required changes

  1. Parse the shallow file at least as loosely as git, failing closed (principle 1). Lines in uppercase, with trailing text, or over 1 KB currently give 0 problems (measured, 2a). One fix: refuse unless every line is exactly a full lowercase object id. A cross-check with rev-parse --is-shallow-repository is a backstop only. Add a test for each of those four lines.
  2. Close the toggle window by design (principles 1 and 4). Measured: 5 of 80 checkedPush calls put the refused file on the remote (2b). Make every walk in pushProblems, and the gitleaks range scan if it shares the window, read the one snapshot that shallowCut checked. The private GIT_DIR + GIT_OBJECT_DIRECTORY route uses documented env only (measured to work, section 2). If the builder finds only --shallow-file works, that goes to the owner as its own question. Add a test that writes the cut between the check and the walks, for example through an injected hook in the test and not in src/, and shows it can't change the result.
  3. Turn the commit-graph off in the runner (-c core.commitGraph=false in src/os/git.ts, scripts/lib/git.cjs, the four scripts that write the settings out, and gitleaks' env), with a test like the replace/graft ones: a graph that lies about a parent can't hide a commit from pushProblems (measured route, section 3). Guess: the speed cost of walking without the graph is small at this repo's size. If the builder measures a real cost, the owner decides between runner-wide and push-reader-only.
  4. Correct the body (principle 5):
  5. CI green at the new head.

Advisory

  • A1. An agent that can write the shared objects dir could also plant objects or pack indexes that don't match their ids. Guess: git doesn't re-hash on every read. This is beyond the issue's list, so it's a follow-up issue, not this PR.

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>
jayden1711 added a commit that referenced this pull request Oct 21, 2026
 cases are normal; an exact-entry suffix host

node:test reports a todo whose body passes as `ok # TODO`, never a failure: once #887 and #894 merged, their cases
passed on main as todos, so a regression of either would have stayed green, and nothing made anyone flip them.

- The #887 and #894 cases are normal cases now (both pass on main after the merge).
- The remaining holes are known-hole cases (knownHole): strict expected failures that pass while every attack still
  gets through and fail the suite, saying "this hole is closed: turn this into a normal case", once any is refused.
  The fix PR (#927, #928, #921, #893) goes red in its own CI until it flips its case; main never does. A self-test
  covers the helper: open, closed, partly closed, bad bookkeeping, and a body that can't run.
- tool.domain-allowlist gains two hosts that end with an exact allowlist entry (no dot before it, and after another
  label): the evaluator's E04 mutant (`h === a` read as `h.endsWith(a)`) survived the whole file before.

Test (fails without): E04 now fails tool.domain-allowlist (28 pass, 1 fail); without the two lines it left the file
green (evaluator, measured). At PR #921's head the #889 known-hole case fails with the flip message.

Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
jayden1711 added a commit that referenced this pull request Oct 21, 2026
…it past replace refs (#724)

rangeNames read names with `git log -z`, which is porcelain: log.showSignature=true with an ssh-signed commit glued
the signature check's text to the first name ("No signature\nsecrets/k.key"), so the refused-path, workflow and skill
checks gave 0 where main gave 1 (a regression, measured by #928's adversarial review), and log.showRoot=false hid an
orphan root's files under an "ours" merge (main too). It now runs rev-list into `diff-tree --stdin -r --root -m
--first-parent --no-renames --name-only --no-commit-id -z`, which reads no log config.

A touched path with a ".", ".." or ".git" part (any case) is refused: only a crafted tree has one, and
`./secrets/x.pem` matched no `secrets/**` glob (measured). push-check's own git helper turns replace refs off: a
replace of a 3 MB blob with a small one got past the size limit (measured); #927 moves the helper onto its runner.
test/push-names-plumbing.test.ts is locked with the other push tests.

Test (fails without): test/push-names-plumbing.test.ts, 7 of 8 fail at 0286164 (showSignature for all three
checks, showRoot=false, the four crafted parts, the replace ref); the empty-name tree, which git refuses, passes
either way. The lock case fails there too.

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>
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>
@jayden1711

Copy link
Copy Markdown
Owner Author

Evaluator verdict (delta final check, round 8): CHANGES NEEDED

Head checked: 7696ed8. It is the merge of main 1cee9bd (#921) at 8bf832c plus the GIT_CONFIG_COUNT fix. It answers issuecomment-6327718337 and #1001 item 1.

Summary:

  • Passes: the merge, the decision-detail fix and its test, the scan, the registry, the tests, typecheck, denylist, size and CI.
  • Fails: the new count parse is not git's.
    • git 2.43 accepts a minus sign: -0 reads as 0, and -18446744073709551615 wraps to 1. All three copies leave those counts untouched, so they add no core.commitGraph=false pair.
    • The gits started underneath therefore run with the commit-graph on. That includes gitleaks' gits in the push gate.
    • At 5d8c98a they got the pair. So this round reopens, for those counts, a route the previous round had closed.

1. The merge re-derives: PASS

  • measured: git merge-tree --write-tree 5d8c98ab 1cee9bd4 gives fcfa4a5a with no conflict.
  • measured: git diff fcfa4a5a 8bf832cd^{tree} changes only two files:
  • measured: the merged tree compiles. merge-tree 7696ed8b 1cee9bd4 gives 303aca21, which equals the head's tree, and npm run build and typecheck at the head are clean.
  • measured: both commits have a Signed-off-by trailer and no attribution trailer.

2. test/decision-diff-overflow.test.ts: PASS

measured: I wrote the file into a fresh worktree at 5d8c98a (same blob, 3aa9c1fc) and ran it with a scratch HOME, state, TMPDIR and git config.

  • At 5d8c98a: it fails at the last assertion, no part of any token is shown; the end: "…\n+gh*_[REDACTED]\n+ghp_0054". The fixture's own asserts pass before that: the diff is past 8 MiB, and MAX - start === 8, so the cut is inside a token.
  • At the head: it passes (0.5 s).

3. Count parsing: FAIL

measured: work/delta-927-r8/count-probe.mjs runs plain git config --get (git 2.43.0, no fixed args, which is what a sub-git sees) for 34 counts, with a caller pair x.y=z. It does this on each count as given, and on each copy's output: src/os/git.ts gitEnv (built), scripts/lib/git.cjs gitEnv, and scripts/push-gate.mjs graphOff, the last taken verbatim from the file.

  • Matches git, all three copies (the same result each time):

    • Accepted: ``, 0, `+0`, `1`, `01`, ` 1`, `+1`, and `\t1`, `\n1`, `\v1`, `\f1`, `\r1`. Each keeps `x.y` and gets `core.commitGraph=false`.
    • Refused: 1 , 0x1, 1e0, abc, -1, 1.5, , -, +, +-1, -+1, 1\n, 2147483648, 99999999999999999999, NBSP+1 and ١. Each is still refused.
  • Does not match git, all three copies (15 mismatches in all):

    Count git 2.43 Each copy at 7696ed8 Each copy at 5d8c98a
    -0, -0, -00, \t-0 accepts, as 0 left as is: git runs, core.commitGraph unset count 1, core.commitGraph=false
    -18446744073709551615 accepts, as 1 (strtoul negates mod 2^64): x.y=z left as is: git runs, x.y=z, core.commitGraph unset count 1, graph off (the caller's pair overwritten)
    • The cause: CONFIG_COUNT = /^[ \t\n\v\f\r]*\+?[0-9]+$/ has no -. strtoul takes an optional + or -; the "too many entries" check comes after, on the unsigned value.
  • Who can reach it:

    • Push gate (inferred): GIT_ENV = graphOff({ ...process.env, … }), and the hook gets the pusher's environment. So GIT_CONFIG_COUNT=-0 git push would hand gitleaks' gits the commit-graph. That is the route graphOff exists to close, and it was closed at 5d8c98a (measured above, on the function).
    • Runner (inferred): the runner's own gits keep -c core.commitGraph=false from the fixed arguments, so for them only sub-gits are affected.
  • The three copies agree (measured): they mismatch on the same counts.

    • inferred: test/git-config-count.test.ts checks only src/os/git.ts and scripts/lib/git.cjs. Nothing tests the parse in graphOff; the scan's pins don't cover it (below).
  • The push gate's pinned scan patterns still match (measured): git-runner.test.ts passes at the head. The pins are { encoding: 'utf8', env: GIT_ENV }, const GIT_ENV = graphOff({ ...process.env, and the [`GIT_CONFIG_KEY_${n}`]: 'core.commitGraph' line.

    • inferred: they pin the shape, not the parse.
  • The builder's fail-without (measured): at 5d8c98a, 7 of 12 fail (the refused counts), and 5 pass (the accepted ones). At the head, 12/12 pass.

4. Runner scan (the round-4 scan.mjs copy): PASS

  • measured: fetched first. origin/main is still 1cee9bd.
    • At the head: files 520 unaccounted 0.
    • On merge-tree 7696ed8b 1cee9bd4 (tree 303aca21, the same as the head's): files 520 unaccounted 0.

5. Registry probe: PASS

measured: work/delta-927-r8/reg.mjs imports each worktree's own built merge-policy-weakening.js.

6. Tests at the head: PASS

measured: run in the foreground with a scratch HOME, state, held-out dir, TMPDIR and git config. Recording stand-ins (exit 1) came first on PATH for claude, gh, systemctl, launchctl, loginctl, sudo, docker, tailscale, useradd and id. The stand-in log holds only claude --version twice, and the stand-in answered it.

Batch Result
git-runner, review, plan-decisions, decision-detail, decision-detail-chrome, child-env-sites, decision-diff-overflow, git-config-count 73 pass, 1 skipped (the real-Chrome case: no Chrome here)
All 16 redact* / redaction-one-module 84/84
push-check, push-gate, push-gate-hook, push-guard, push-hook-guard, push-names-plumbing, push-shallow, push-shallow-format, push-skill-links, push-skill-names, push-snapshot, direct-push-alert, merge-policy, change-files-renames 241/241

7. Typecheck, denylist, size: PASS

  • measured: npm run typecheck prints no errors.
  • measured: check:denylist reports 1503 files clean.
  • measured: size (npm pack --dry-run --json, local, no web UI built) is 1,533,807 / 5,129,894 at the head against 1,527,486 / 5,112,322 at 1cee9bd. That is +6,321 tarball and +17,572 unpacked, against a 2,050,000 / 6,400,000 budget that is unchanged (test/package-files.test.ts has no diff against main).

8. Merge-tree with origin/main and the open PRs

measured: git merge-tree --write-tree 7696ed8b <ref>, then the scan on each tree.

Ref Merge Scan
origin/main 1cee9bd clean 0
#997 26811fd, #957, #932, #918, #794, #746 clean 0
#944 fb913e4 CONFLICT src/os/git.ts (it was clean at 5d8c98a) 0
#910 9b4775d CONFLICT src/doctor.ts; it conflicts with main 1cee9bd too (measured), not from this PR 0
#979 dd6b301 CONFLICT change-files.ts, scan/secrets.ts 2 (hook.ts 1, scan/secrets.ts 5)
#903 d2a806d CONFLICT review.yaml, merge-flow.ts, worktrees.ts 3
#893 33af999 CONFLICT config/load.ts, checks-from-feedback.ts, merge-flow.ts, pr-hygiene.ts, worktrees.ts 5

#944:

#979, #903 and #893 are as in round 7 (inferred): their own unrouted calls are theirs to route.

9. CI at 7696ed8: green

  • measured: gh pr checks 927 shows 28 pass and 3 skipped (bootstrap, bootstrap (windows-wsl), publish). None is pending or failed. Nothing was rerun.

Other checks

  • Weakening gate (measured): this round changes no existing test line. Both of its test files are new, and the rest of the diff is src/ and scripts/. Nothing is loosened.
  • Body claims (measured): the Round 8 claims for the merge, the overflow test, the fail-withouts, the scan, the registry, typecheck, denylist and CI all match.
    • Not true: "gitEnv now reads a count as git does", and the correction under "What this can never do" ("a count git refuses is left as it is"). They miss the minus forms in §3.

Required changes

  1. Read the sign the way git does, in all three copies (principle 1; a regression from 5d8c98a). Pick one:

    • (a) Emulate strtoul exactly:
      • optional whitespace, then an optional + or -, then digits;
      • a magnitude over 2^64-1 becomes 2^64-1;
      • a - gives (2^64 - magnitude) mod 2^64;
      • a result over INT_MAX means git refuses, so leave the count alone.
    • (b) Simpler and the same on any unsigned long width (32-bit on Git for Windows; guess): when the count has a -, the runner makes git refuse. For example, set the count to -1 ("too many entries" on 32- and 64-bit). -0 then fails closed instead of running.

    In test/git-config-count.test.ts:

    • add -0, -0, -00 and -18446744073709551615, each run through real git;
    • cover scripts/push-gate.mjs graphOff too (for example its source fed to the test, as the probe does), so all three copies are pinned.

    It must fail at 7696ed8.

  2. Don't return early from gitEnv on a refused count (src/os/git.ts, and the same shape in scripts/lib/git.cjs). Skip only the pair append, and fall through to the end. Then git: an instance's git runs nothing its checkout's config names, and .git/ is the coordinator's (#926) [PROTECTED: owner merges] #944's return marked(out) ? guardEnv(out) : out; still runs after the merge, whatever the count.

  3. Correct the Round 8 text and the "What this can never do" correction to match (principle 5). Re-measure on the new head:

    • the count probe (or your own over the same 34 counts), 0 mismatches;
    • typecheck and denylist;
    • the scan copy at the head and on the merge with the then-current main;
    • the registry probe;
    • git-runner, git-config-count, push-gate and push-gate-hook.

Scripts: work/delta-927-r8/ (count-probe.mjs, reg.mjs, mt.sh, run.sh).

@jayden1711

Copy link
Copy Markdown
Owner Author

Second review, round 8: CHANGES NEEDED

Head checked: 7696ed8. It merges cleanly with main 1cee9bd (merge-tree gives tree 303aca21, the same tree the PR body names).

Scope: round 8 only:

Setup: every probe ran with a temp HOME and state, in scratch repos. The token and key shapes are fake.

Attacks

# Attack Result Label
A1 36 counts through plain git, the runner, and env only (what gitleaks' gits get) via gitEnv, git.cjs and graphOff. graphOff was pulled from scripts/push-gate.mjs in memory. The counts: empty, unset, 0, +0, 1, +1, 1, \v1, \n1, 01, many leading zeros, -0, -0, -00, \t-0, -, +, +-1, -+0, 1\n, 0x1, 1e0, 1 , abc, -1, 1.5, a lone space, NBSP+1, 2^31-2, 2^31-1, 2^31, 2^32, 2^64-1, 2^64, 2^53-1, 2^53+1 The three parsers agree on all 36 (0 divergent). Every count git refuses is still refused, and no caller pair applies. Every count git accepts gets the runner's pair last, except negative zero (A2). measured
A2 Negative zero: -0, -0, -00, \t-0 git's strtoul reads these as 0 and git runs (plain git config --get exits 1, not 128). The new regex refuses them, so gitEnv, git.cjs and graphOff return the env unchanged, with no pair. A git that reads only the environment (gitleaks) then has the commit-graph on (core.commitGraph unset). At 5d8c98a, Number('-0') gave pair 0, so this round loses the pair for these inputs. The runner's own gits are still covered by -c core.commitGraph=false in their args. The agent guardrail denies GIT_CONFIG_COUNT=-0 git push (verdict deny). measured
A3 A caller's GIT_CONFIG_PARAMETERS against the env pair. git reads the COUNT pairs first and GIT_CONFIG_PARAMETERS after them. The route: git -c core.commitGraph=true push, which the agent guardrail allows (verdict none), in a scratch repo whose pre-push hook applies the push gate's own graphOff to its env. The hook env holds GIT_CONFIG_PARAMETERS. The env-only git reads core.commitGraph = true. A plain push reads false. Set directly in the env, the same holds for gitEnv and graphOff (env-only true, while the runner, with its args, reads false). Whether a forged graph then hides a commit from gitleaks' --no-walk log, by giving it other parents: inferred, not built. measured (config), inferred (end to end)
A4 Key numbering: a gap (count 3 with no KEY_2), a missing VALUE_0, an empty KEY_0, a caller pair at the runner's index beyond the count, and count 2 with the caller's pair 1 = true The gap, the missing value and the empty key are refused both ways. A pair beyond the count is replaced and the runner's applies. With count 2, the result is true,false: the runner's comes last and wins. measured
A5 decisionDiff and --text (new in this PR) on a binary file holding a token as UTF-16LE or UTF-16BE Head: the file's lines are shown with a NUL between characters. The redactor doesn't match, and with the NULs dropped the token reads whole. main (git diff --no-color --no-ext-diff then the same redactCut) shows Binary files … differ. An ASCII token in a NUL-holding file, and in a -diff file, is redacted on head. measured
A6 Cut sweep: redactCut(·, 512 KiB) as both branches call it, 780 placements. Each moves an OpenSSH key (diff-prefixed, 400 lines) or a token, bare or glued, across the shown cut and across the window's end (MAX + 16 KiB). Half the runs have an early 26 KB key that shrinks the redacted text, so the window's raw tail is shown. 0 leaks. Detector control: it flags a raw key and a raw token, and not the filler. measured
A7 A key with one body line changed (50-line key, so the hunk holds neither BEGIN nor END) 10 key body lines shown, on head and on main. This existed before the PR: no BEGIN, so no rule fires. measured
A8 Error paths: git failing, timeout, maxBuffer on stderr with empty stdout Only why comes back, git's first stderr line. Base and head are SHA-checked, so that line carries no file content. Partial stdout is never shown outside the maxBuffer branch. inferred (code read)
A9 A raw git start the scan misses: my own scan of the merged tree 303aca21, plus the #921 merge diff (5d8c98a..8bf832c) for new spawn sites files 520 unaccounted 0. The merge adds no git start (its only exec hits are regex .exec). measured
A10 A later PR that undoes round 8: run through weakeningOf in scratch repos Deleting or weakening git-config-count.test.ts, or deleting decision-diff-overflow.test.ts: held. Reverting gitEnv to Number(), or decisionDiff to slice-then-redact: no hold, so only those tests catch it. measured (hold), inferred (CI catches the revert)
A11 Does any test pin graphOff's count parse? Two in-memory mutants of scripts/push-gate.mjs: round 7's Number() parse, and always pair 0 Every pattern git-runner.test.ts pins for the push gate still matches both mutants. git-config-count.test.ts covers only the two gitEnvs, and no test runs graphOff with a count. measured
A12 The PR's own new tests decision-diff-overflow and git-config-count: 13 of 13 pass at 7696ed8. measured

Required changes

  1. A5, a route main closed is reopened (principle 3), and the "Compromised component → Dashboard: … redacted" claim is false.
    • Problem: --text shows a binary file's bytes, and UTF-16 text goes past every rule.
    • Fix by design: redact a NUL-free view, e.g. redactCut(stdout.replace(/\u0000/g, ''), MAX) on both branches. --text stays for the review goal, since a NUL is also how a change hides as "binary".
    • Test that fails without it: a UTF-16LE file holding a token.
  2. A2, the "a count is read as git reads it" claim is false for negative zero.
    • Problem: strtoul accepts an optional -. Any nonzero negative becomes "too many entries", but -0…0 is 0 and git runs.
    • Fix: the same in all three copies, e.g. /^[ \t\n\v\f\r]*(?:\+?[0-9]+|-0+)$/, with a minus-zero read as 0.
    • Tests: add -0, -0 and -00 to the accepted cases, and -01 to the refused ones (strtoul gives ULONG_MAX, so git says "too many entries"; inferred, not run).
  3. A3, the claim "a config pair in the environment after any the caller set" (push-gate.mjs and the PR body) is false for GIT_CONFIG_PARAMETERS, which git reads after the COUNT pairs.
    • Route: a guardrail-allowed git -c core.commitGraph=true push turns the commit-graph back on for the push gate's gitleaks.
    • Not a main regression: main had no commit-graph setting for gitleaks.
    • Why it's required: it is this PR's own stated protection.
    • Fix by design: in gitEnv, git.cjs and graphOff, also append 'core.commitGraph'='false' to GIT_CONFIG_PARAMETERS, or set it when absent. git reads that last, and a malformed caller value still makes git refuse. Keep the COUNT pair too.
    • Test: an env-only git under a caller's GIT_CONFIG_PARAMETERS reads false.
  4. A11, graphOff's round-8 fix has no test that fails without it. Add graphOff, taken from the gate's source or by running the gate's GIT_ENV, to git-config-count.test.ts's refused and accepted tables, so it can't drift from gitEnv.

Follow-up, not blocking

  • A7 (exists before this PR): a partial-key hunk shows key body lines on the owner's diff. A rule for a run of base64 key-body lines in a diff would close it. Better as its own issue against the redactor.

"What this can never do" and "Compromised component"

  • Replace refs and grafts: no new route found this round.
  • The corrected "Change what git reads" bullet: holds for refused counts (A1). The negative-zero case changes nothing git reads, but drops the protection (A2).
  • Dashboard line: false as worded (A5).
  • "Only the engine's own environment reaches this; no agent sets it":
    • The push gate runs in the pushing agent's environment.
    • The guardrail blocks the env forms (A2, measured) but not git -c (A3, measured).

Refusals: none.

…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>
@jayden1711

Copy link
Copy Markdown
Owner Author

Second review (verification-scoped, after a classifier stop)

Verdict: CHANGES NEEDED on 4e4287e (4e4287e).

Scope. This is the one rerun allowed after a classifier stopped the full second review. It does three things only:

  • re-runs the documented attacks (round 7 and 8 probes, paths changed only) at 4e4287e and at 7696ed8;
  • re-runs the round 9 fail-without tests;
  • checks the body's claims by reading the code.

No new payloads were built. The one new finding below is inferred from reading the redaction rules. It was not run.

1. Documented attacks: head vs 7696ed8 (all measured)

All runs used a scratch HOME, TMPDIR and state under env -i.

Probe 7696ed8 4e4287e Status
r8 count-probe (36 counts, 3 copies + runner) -0, -0, -00, \t-0: git runs with no pair (commit-graph unset). GIT_CONFIG_PARAMETERS true: env-only git reads true Every non-canonical count gives refused, and env-only git exits 128. Canonical counts and unset read false. GIT_CONFIG_PARAMETERS reads false in all three copies. divergent: 0 fixed (A2, A3)
delta-r8 count-probe (oracle: match git) 15 mismatches: the copy runs with graph=unset 45 mismatches, every one "git accepts, the copy refuses" (fail closed, as designed). For the 17 counts git refuses, every copy refuses too. Not one row runs without false fixed (now fails closed)
r8 gap-probe gap, missing value and empty key refused both ways. Count 2 with KEY_1=true gives true,false same, with true,false,false (the COUNT pair, then the parameters entry) held
r8 hook-probe (git -c core.commitGraph=true push, gate's graphOff in the hook) gitleaks' git reads "true" reads "false" fixed (A3)
r8 guard-probe env forms deny, git -c none identical held (the guardrail is unchanged; the gate now covers -c)
r8 diff-probe UTF-16LE/BE token: visible once the NULs are dropped UTF-16LE/BE token: gh*_[REDACTED], not visible. The NUL-ASCII, -diff and plain-token cases are not visible. Partial key: 10 body lines on head and main fixed (A5). A7 is still open, as on main
r8 cut-sweep (780 placements) 0 leaks 0 leaks; the detector's control flags the raw key and the raw token held
r8/r7 scan-tree (the r7 copy is byte-identical except its path) files 520 unaccounted 0, 25 spawners, 19 non-JS identical held
r8 hold-probe H1–H3 held. H4/H5 (src reverts) not held H1 held. H2 stops with "mutation not applied": the line it removes is gone in the rewritten test, so H3–H5 don't run unchanged. src/merge-policy-weakening.ts is not in this round's diff, so I infer H3–H5 behave as at 7696ed8. The src reverts are now caught by the fail-without tests below held (H1). H2–H5 not runnable unchanged
r8 graphoff-mutant both mutants pass every pinned pattern, COUNT=2 / 1 both still pass the pins, but COUNT=refused, because the strict check runs before the mutated line. git-config-count.test.ts now runs the gate's graphOff from source: canonical counts at their KEY_n, plus the agreement test. So "always pair 0" fails it (inferred from the test's asserts) fixed (A11)
r7 count-probe runner = plain git for 7 counts same held

2. Fail-without (measured)

I wrote test/git-config-count-r9.test.ts (byte-identical to the head's git-config-count.test.ts, checked with cmp) and test/decision-diff-utf16.test.ts as new files into a fresh worktree at 7696ed8, then built.

  • At 7696ed8: 46 of 48 fail.
    • All 36 non-canonical counts, all 5 canonical ones, the 3 parameter cases, and both UTF-16 cases fail.
    • Each is a real assertion failure, e.g. '2' !== 'refused' on src/os/git.ts gitEnv, and expected false / actual true for ghp_.
    • Two pass at 7696ed8: "malformed parameters" and "copies agree". This matches the builder's 44 of 46.
  • At 4e4287e: git-config-count, decision-diff-utf16, git-runner and decision-diff-overflow pass, 60 of 60.
  • Typecheck at 4e4287e: clean.

3. Required change (inferred from code, not run)

Dropping NULs before redaction glues each byte run in a binary file to the next one. That defeats the redaction rules that need a boundary.

  • Where: src/decision-detail.ts (const shown = stdout.replace(/\0/g, '')).

  • Rules that need a boundary: the rules in src/events/redact.ts PATTERNS that end with a fixed-length body and a negative lookahead:

    • AWS key, (?:AKIA|ASIA)[A-Z0-9]{16}(?![A-Za-z0-9]);
    • Google key, AIza…{35}(?!…);
    • 32-byte hex key, \b0x…{64}(?!…).

    Also the rules that need a left boundary: other sk- keys (\bsk-), Stripe ((^|[^A-Za-z0-9_])) and the Discord token.

  • How the gluing defeats them: binary files keep strings NUL-separated. Take a key-shaped string with a NUL on either side and a letter or digit beyond the NUL:

    • at 7696ed8: the NUL is the boundary each rule needs, so the key is redacted;
    • at 4e4287e: the NUL is gone, the lookahead or left boundary fails, and the key is shown in the clear;
    • on main: the whole file is Binary files … differ.
  • Effect: this reopens, for those shapes, the route main closed (principle 3). It also makes the Dashboard line "It is redacted as a NUL-free view" false outside UTF-16.

  • Not affected: the shapes with greedy bodies and no left boundary (gh*_, github_pat_, sk-ant-, JWT).

  • Fix direction (the builder's call): keep the NUL as a boundary for the redactor and still rejoin UTF-16. One way: also redact a same-length view in which each NUL is a non-word character, so lines map 1:1. Fail closed per line when either view's redaction fires.

  • Test that fails without it: a fake fixed-length key shape between NULs, with a letter after the trailing NUL, in a file decisionDiff reads.

4. Claims (Round 9, "What this can never do", "Compromised component")

Round 9

Claim Label
Only /^(0|[1-9][0-9]{0,3})$/ is read, and anything else becomes refused, which git refuses true. src/os/git.ts 86–95, scripts/lib/git.cjs 29–38, scripts/push-gate.mjs 40. Measured on 36 counts at git 2.43. "On every platform / any width" is unscoped for git < 2.31, which doesn't read GIT_CONFIG_COUNT at all. That is harmless: there the caller's pairs are ignored too, and the parameters entry still sets false (inferred)
It covers -0, +1, 1, 01, 0x1, empty, ≥10000 true (measured). Note that empty, +1, 1 and 01 are counts git accepts, so a caller environment holding one now makes every runner git fail. That is fail-closed by design. Engine writers (service.ts, runner.ts, network.ts) only write canonical counts (inferred, grep)
core.commitGraph=false is appended to GIT_CONFIG_PARAMETERS, which git reads after the COUNT pairs true. The order was measured: --get-all gives true,false,false, and the hook route reads false
Appending is safe for every caller value git accepts true (inferred from git's parse_config_env_list / sq_dequote_step). Details below the tables
"No early return: each copy runs to its end" false as worded for the push gate. graphOff returns early at line 40, though both returns carry the parameters entry, so nothing is unsafe. True for both gitEnvs, which #944 touches. Correct the sentence
git-runner.test.ts: only the two expected objects gain the entry true (measured diff: 2 lines)
Fail-without: 44 of 46 fail, plus both UTF-16 cases true (measured above)
UTF-16 diff test: no part of the token is shown true (measured)

Why appending is safe for every value git accepts (inferred, from git's parser):

  • Accepted values: git accepts a GIT_CONFIG_PARAMETERS only if every entry closes its quote. A value that ends inside a quote fails sq_dequote_step. So any accepted value ends outside a quote.

  • The leading space: the appended text starts with a space, and that space ends the caller's last entry in every form:

    • old-style 'k=v': !cur || isspace;
    • new-style 'k'='v': cur && !isspace is false, so the value stands;
    • implicit bool 'k'=: isspace(*cur).

    Whitespace is then skipped, and the runner's entry is parsed on its own, last. So it wins for a single-valued bool.

  • A value git refuses: it stays refused. If it ends inside a quote, the appended ' closes that quote, and the c that follows is "bogus format". If it starts with whitespace, sq_dequote_step rejects it.

  • The empty string: handled by params ? … : fixed.

  • A caller's absolute include.path: it is expanded where it sits, so before the runner's entry.

What this can never do

Claim Label
Turn replace refs or grafts back on true (code: the fixed env goes last, and --no-replace-objects is in the args; unchanged this round)
Change what git reads beyond the commit-graph, with the round 8/9 corrections true as corrected. The round 9 correction's facts were measured at 7696ed8: -0 drops the pair, and the -c route reads true
Loosen a check true. Every touched test is new in this PR (none exist on main); the rewrite tightens
Break an existing install's push: imports nothing new true for the stated reason (push-gate.mjs imports are unchanged). Unscoped: a pusher's environment with a non-canonical count is now refused (fail-closed, inferred)

Compromised component

Claim Label
Agent user not re-tested this round. No probe in the documented set covers .git/shallow
Coordinator true (scan: unaccounted 0)
Dashboard: "redacted as a NUL-free view" true for UTF-16 (measured). False for NUL-separated fixed-length or left-boundary key shapes (section 3, inferred)
Dashboard: "still open, as on main: partial key" true (measured: 10 body lines on both)
Other instances, hub viewer true for this diff
Push access true (19 non-JS files name git: the scan's stated gaps)

Refusals

None.

@jayden1711

Copy link
Copy Markdown
Owner Author

Evaluator verdict (delta final check, round 9): CHANGES NEEDED

Head checked: 4e4287e. It is two signed-off commits on 7696ed8 (550323c and 4e4287e) and holds main 1cee9bd, which is still origin/main (fetched first).

Summary:

  • Passes:
    • the count fix, in all three copies;
    • the GIT_CONFIG_PARAMETERS append, including the quoting;
    • no early return;
    • the UTF-16 fix and every other text encoding I tried;
    • the two fail-withouts;
    • the test-line changes, the scan, the registry, typecheck, denylist, size and CI.
  • Fails:
    • --text still puts a binary file's bytes on the owner's dashboard, where main showed "Binary files differ". A DER private key comes through with 58 to 67% of its bytes verbatim. This is the same class as A5. The NUL drop does not cause it and does not fix it.
    • The body's Dashboard line doesn't say any of this.

1. Count parsing: PASS

I ran work/delta-927-r9/count-probe.mjs, the round-8 probe extended to the round-9 rule. It covers 51 counts, through each copy: src/os/git.ts gitEnv (built), scripts/lib/git.cjs gitEnv, and scripts/push-gate.mjs graphOff taken from the source. Each count runs with and without a caller GIT_CONFIG_PARAMETERS='core.commitGraph=true', through an env-only git 2.43 (no fixed args, which is what a sub-git gets).

The 51 counts include:

  • the reviewers' 36;
  • \f1, \r1, -+1, 00, 0 , -18446744073709551614;
  • Arabic-Indic and fullwidth 1;
  • 9999, 10000 and the literal refused.
Check Result
measured: every non-canonical count, in every copy GIT_CONFIG_COUNT=refused. git exits 128 on both core.commitGraph and the caller's x.y, and no caller pair applies.
measured: canonical 0, 1, 12 Up to 40 caller pairs, with core.commitGraph=true among them, are kept. The runner's pair is at index n, and GIT_CONFIG_COUNT is n+1. --get-all core.commitGraph ends in false, and the caller's x.y=z survives.
measured: canonical 9999 The same shape.
measured: unset count with a caller 'core.commitGraph'='true' All three copies read false.
Total, at the head 306 checks, 0 bad
Control, the same probe at 7696ed8 285 bad (measured)
  • The round-8 probe at the head (measured): work/delta-927-r8/count-probe.mjs reports 45 mismatches (15 counts × 3 copies). Every one is a count git accepts that isn't canonical, and the head now refuses it:

    • ``;
    • 01, 1, +1, \t1, \n1, \v1, \f1, \r1;
    • +0, -0, -0, -00, \t-0;
    • -18446744073709551615.

    That is the decided fail-closed behaviour, not a defect.

  • The push hook (measured): work/delta-927-r9/hook-probe.mjs is the second review's hook probe, with the worktree as an argument and more push forms. Its pre-push hook applies the gate's own graphOff and asks an env-only git for core.commitGraph:

    Push form Head 7696ed8
    plain false false
    git -c core.commitGraph=true push false true
    git -c core.commitGraph=true -c a.b=c push false true
    GIT_CONFIG_PARAMETERS set to true false true
    count-1 pair set to true false false
    count-1 pair true, plus -c true false true
    count -0 exit 128 exit 1 (graph unset)
    count +1, pair true exit 128 false
    count -18446744073709551615, pair true exit 128 true
  • inferred (code read): in the real gate, runGit uses the same GIT_ENV. A refused count therefore throws in targets/git(...) before gitleaks runs, and the outer catch fails the push ("the gate itself failed, so nothing is pushed").

2. GIT_CONFIG_PARAMETERS quoting: PASS

work/delta-927-r9/params-probe.mjs feeds caller values to each copy and checks two things:

  • Security: under the copy's env, an env-only git config --get core.commitGraph gives false or exit 128. Never true, never unset.
  • Parse: whenever plain git accepts the caller value alone, the copy's env is accepted too. Its git config --list must also equal exactly core.commitgraph=false + the caller's own list + core.commitgraph=false, so the append changes none of the caller's entries.

The values: 54 hand-picked, plus 4,000 random strings over ', \, !, space, tab, newline, =, '\'' and key fragments. The hand-picked ones include:

  • empty, spaces only, tab, newline;
  • old and new forms, with trailing space, tab, CR, VT, FF and newline;
  • 'a.b'= (implicit bool), and values with spaces inside the quotes;
  • values ending in '\'', '\', '\ and '\!;
  • unterminated quotes, adjacent quoted words, '', '=';
  • uppercase keys.
Run Values Accepted by plain git Security fails Accepted-but-refused List changed
Head (measured) 4,054 47 0 0 0
Control, at 7696ed8 (measured) 354 30 21 — 90
  • inferred, from git's parse_config_env_list: a caller value that leaves a quote open makes the appended 'core.commitGraph=false' end that quote. What follows is then unquoted text, which git reports as "bogus format". So no caller value can make git skip the appended entry and still run: it either parses the entry last or refuses. The fuzz agrees.

3. No early return: PASS

4. The decision diff's NUL drop: UTF-16 fixed; --text still reopens binary content: FAIL

work/delta-927-r9/diff-probe.mjs compares the head's decisionDiff with main's args (git diff --no-color --no-ext-diff) through the same redactCut. It checks for a 16-character middle part of a fake ghp_ token, in the shown text with NULs dropped, also filtered to printable ASCII.

File Head 7696ed8 main
UTF-16LE, UTF-16BE, LE with BOM, LE at an odd offset redacted shown "Binary files differ"
UTF-32LE, UTF-32BE redacted shown binary
Mixed: ASCII token= then a UTF-16 token redacted shown binary
UTF-16 token next to a non-ASCII character redacted shown binary
Token characters split by 0x01, in a file with a NUL shown shown binary (hidden)
The same with no NUL (control) shown shown shown
ed25519 private key, PKCS#8 DER (48 bytes, has a NUL) hunk shown: 32 of 48 bytes verbatim, ~17 of the 32 seed bytes the same, with NULs binary (hidden)
RSA-2048 private key, PKCS#1 DER (1,192 bytes, has a NUL) hunk shown: 691 of 1,192 bytes verbatim the same, with NULs binary (hidden)

All measured. The keys are generated fresh in the probe. For each key, the shown hunk is exactly the key's UTF-8 decoding with NULs dropped: every byte that isn't part of an invalid sequence, in position, with one U+FFFD per invalid byte.

  • What the NUL drop does (measured): for text in UTF-16 and UTF-32, in either byte order, with or without a BOM, at any offset, mixed or not, the drop closes A5. It shows no binary content that 7696ed8 didn't already show; it only removes the NULs.
  • Keeping --text (judged): its goal is right. A NUL, or a -diff/binary attribute, must not hide a text change from the owner (Git readers: one runner with replace refs off for every engine git call; display/prompt readers ignore attributes; count no-newline files by size (#802 follow-ups) #807 F2). But --text also puts a real binary file's bytes on the owner's dashboard, and main never showed them. A binary private key is the clearest case.
    • Principle 3: this is a route main closed, reopened.
    • Fix the class: this is A5's class (a binary file's content shown where main showed "Binary files differ"), and the NUL drop fixes only its UTF-16/32 instance.
    • inferred: it predates round 9, since it came with --text. Round 9 is where the class was named and closed for one encoding only.
  • The body's Dashboard line is incomplete (principle 5). It says the diff "shows a -diff or binary-marked file's lines … redacted as a NUL-free view". It doesn't say that a binary file's raw bytes are shown, or that no rule redacts them.

5. Fail-without: PASS

measured: I wrote the head's test/git-config-count.test.ts as a new file, test/git-config-count-r9.test.ts, and the new test/decision-diff-utf16.test.ts, both with the Write tool, into a fresh worktree at 7696ed8. Blob hashes are identical to the head's: c1a7ef3f and 48d06e97.

Where Result
At 7696ed8 46 of 48 fail: all 36 non-canonical counts, all 5 canonical ones, the 3 parameters cases, and both UTF-16 cases. The 2 that pass are the malformed-parameters case and the agreement case, as the body says.
At the head The two files, plus decision-diff-overflow, git-runner, decision-detail and child-env-sites: 69 of 69 pass.
  • The UTF-16 failure at 7696ed8: no part of the token: "…+token: ghp_Utf16Tk9Lm3Qr7Ws2Xv5Zb8Nc4Hd6Jf1Gp0a\n…". That is the bug the test is for.

  • graphOff mutants (measured, work/delta-927-r9/graphoff-mutant.mjs, in memory, no repo file edited): every pattern git-runner.test.ts pins still matches all five mutants below, so only the count test tells them apart. The count test's checks catch each one:

    Mutant Failing cases
    No fail-closed line 13
    Refused count kept as is 13
    Round-8 parse 5
    No GIT_CONFIG_PARAMETERS append 17
    Parameters prepended instead of appended 1

    The unmutated head gives 0. A11 is closed.

6. Changed existing test lines: no loosening

  • test/git-runner.test.ts (measured, 2 lines): both changed lines are expected objects in assert.deepEqual. Each gains GIT_CONFIG_PARAMETERS: "'core.commitGraph=false'" and keeps every other key. An exact-object compare with one more required key is tighter.

  • test/git-config-count.test.ts, rewritten in place (this PR's own file from round 8, not on main; measured diff):

    • Round 8's refused counts (0x1, 1e0, 1 , abc, -1, 1.5, ) are all still there. Each still asserts git exits 128 with the caller's pair not applied, both env-only and through the runner. The only change is the expected GIT_CONFIG_COUNT, from "left as is" to refused, which is the orchestrator's decision.
    • Round 8's accepted counts (01, 1, +1, ``) moved from "the pair is added and git reads z" to "refused, git exits 128". That is the decided fail-closed direction: tighter for security, and it costs availability only for counts nothing in the engine writes.
    • 1 keeps its positive case.
    • Two narrowings, both inferred minor:
      • the copies' agreement check now compares only the GIT_CONFIG* keys instead of the whole object (the fixed env is pinned by git-runner.test.ts);
      • the canonical cases no longer go through gitSpawnSync, only through gitEnv with an env-only git.

    Neither is a check main had.

7. Scan, registry, typecheck, denylist, size, other tests

  • Scan (measured, the round-4 scan.mjs copy): files 520 unaccounted 0 at the head, and also on the merge with main 1cee9bd. That merge's tree is f9adb8dd, the same tree the body names.

  • Registry (measured, work/delta-927-r9/reg.mjs):

    Against Size (vs the head's 110) Lost
    7696ed8 110 0
    main 1cee9bd 107 0

    The 3 extra entries against main are this PR's RUNNERS, OUTSIDE_THE_RUNNER and the lint mark.

  • measured: npm run typecheck is clean, and check:denylist reports 1504 files clean.

  • Size (measured, npm pack --dry-run --json): 1,534,158 / 5,130,975 at the head, against 1,527,486 / 5,112,322 at main. That is +6,672 / +18,653, well inside the unchanged budget.

  • Tests (measured): scratch HOME, state, held-out dir, TMPDIR and git config, with recording stand-ins first on PATH. The stand-in log stayed empty.

    • push-gate, push-gate-hook, review, plan-decisions, redact, redaction-one-module: 103 of 104. The one failure was redact.test.ts "every pattern runs in linear time on 100 KB of hostile input", a timing test.
    • Re-run alone, redact.test.ts gives 11 of 11. This PR changes neither src/events/redact.ts nor that test (measured: empty diff against main). So the failure is load-related, not this PR's (inferred).

8. Merge-tree with origin/main and the open PRs

measured: git merge-tree --write-tree 4e4287e6 <ref>, then the scan on each tree.

Ref Merge Scan
origin/main 1cee9bd clean 0
#997 fec9384, #957, #932, #918 701c15a, #794, #746 dafd697 clean 0
#944 fb913e4 CONFLICT src/os/git.ts (see §3) 0
#910 9b4775d CONFLICT src/doctor.ts (as in round 8: it conflicts with main too) 0
#979 85a5bbd CONFLICT change-files.ts, scan/secrets.ts 2 (their own calls)
#903 f4bb6dd CONFLICT review.yaml, merge-flow.ts, worktrees.ts 3 (their own calls)
#893 1d90008 CONFLICT 5 files 5 (their own calls)

9. CI at 4e4287e: green

  • measured: gh pr checks 927 shows 28 pass and 3 skipped (bootstrap, bootstrap (windows-wsl), publish). None is pending or failed. Nothing was rerun.

Body claims

  • Match (measured): the Round 9 claims for the counts, the parameters, no early return, both fail-withouts (44/46 and 2/2), the scan, the registry, typecheck, the denylist (1504) and CI.
  • Incomplete:
    • the Dashboard line (§4);
    • "Break an existing install's push". With the decided fail-closed rule, an environment holding a count git accepts but that isn't canonical (GIT_CONFIG_COUNT= empty, 01, +1, -0) now fails every push through the gate, and every engine git. main pushed there (inferred from §1's measurements and the gate's code). That is the intended trade, but the body should name it.
  • Adversarial pass: the section has no round-9 entries. The attacks the round ran (the malformed parameters, graphOff alone) sit only in the Round 9 notes, and nothing tried other encodings or a real binary file, which is how §4 was missed.

Required changes

  1. Don't show a binary file's raw bytes on the owner's diff (principle 3; A5's class, not only its UTF-16 instance).
    • Keep --text's goal: a NUL or an attribute must not hide a text change.
    • Stop real binary content from reaching the dashboard. One way, by design: for a file git would call binary without --text (a NUL on either side), show it as a "strings" view.
      • Every run of bytes that isn't printable text becomes one marker: an invalid UTF-8 byte, a C0 control other than tab and newline, or a NUL.
      • A printable run shorter than about 8 characters is dropped too.
      • Then redact and cut as now.
    • UTF-16/32 text (NUL-dropped) and a text line hidden behind one NUL still read in full. A DER key shows nothing of itself.
    • The builder may pick another design that meets the test.
    • Test that fails at 4e4287e: a freshly generated ed25519 PKCS#8 and RSA PKCS#1 DER private key, committed as a file. No 6-byte run of the key's printable bytes appears in the shown diff. The UTF-16 test, and a "text with one NUL" case, still show their lines.
  2. Correct the body (principle 5):
    • the Dashboard line (binary bytes, and what is now done about them);
    • "Break an existing install's push": name the fail-closed cost for a count git accepts that isn't canonical, in the gate and in the runner;
    • add round-9 entries to "Adversarial pass".
  3. Re-measure on the new head:
    • work/delta-927-r9/diff-probe.mjs (0 key bytes, 0 token parts shown);
    • the fail-without of the new test at 4e4287e;
    • typecheck and denylist;
    • the scan at the head and on the merge with the then-current main;
    • the registry probe.

Not required, for the owner's information:

Scripts are in the session scratchpad, work/delta-927-r9/: count-probe.mjs, params-probe.mjs, hook-probe.mjs, diff-probe.mjs, graphoff-mutant.mjs, reg.mjs, mt.sh and run.sh.

…, 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>
@jayden1711

Copy link
Copy Markdown
Owner Author

Second adversarial review, round 10: CHANGES NEEDED (head 0b1c7da)

Independent second review under the owner's round cap. Everything below was run against scratch repos and in-memory inputs, with a temp HOME, state, TMPDIR and git config, on git 2.43. Main is 1cee9bd. Each claim is labelled measured, inferred or guess.

Required changes

R1 (b) reopened route, and (c) overclaim: the decision diff's hunk headers show a binary file's bytes.

  • What happens (measured). With --text, git writes a function-context text after each hunk's @@ … @@. For a file with no diff driver, that text is the nearest earlier line that starts with a letter, _ or $, up to 80 bytes. For a binary file, that line is the file's own bytes. git stops at the first invalid UTF-8 byte, but keeps control bytes and NULs.
  • Why the view misses it. diffTextView copies every @@ line as it is ("headers included"), and judges only the +, - and space lines that come after it.
  • Measured: scratch probe probe-hdr.mjs, through the built decisionDiff at this head. In each case main's git diff says only "Binary files differ".
    • A binary file whose earlier line is 46 printable bytes: the header shows all 46.
    • A line holding a 0x01 byte and a NUL: the header shows both bytes as they are. The view's own rule would show that line as [binary: N bytes not shown].
    • A short UTF-8 run (under 16 characters): the header shows it too. Inside a binary hunk, the view hides such a run.
    • For random RSA DER key lines, the header showed a 6-byte printable run in 0 of 40 (measured). The leak needs a letter-led line that is valid UTF-8 for a while. Records in a store file are such lines; random key bytes rarely are.
  • The claims this makes false: the Dashboard bullet "a binary file's bytes never show", and the list of what a binary hunk shows "only". Neither mentions the header.
  • The tests miss it (inferred). No fixture in test/decision-diff-binary.test.ts has a letter-led binary line before a changed hunk. All 7 pass at this head (measured).
  • A fix, for the builder to judge (inferred): cut each @@ line to @@ -a,b +c,d @@ whenever its function-context text would not pass as text. Do this for every hunk, not only binary ones: a text-only hunk inside a binary file can take its header from a binary line earlier in the file. Add a test that fails at 0b1c7da.

R2 (b) reopened route: the fix-forward culprit diff now sends a binary file's bytes, and a UTF-16 token, into a tracker issue body.

  • The change. On main, merge-flow.ts reads the culprit diff with git('diff', sha^1, sha), and a binary file comes out as "Binary files differ". This PR reads it with patchText, which uses --text. fix-forward.ts puts that text in the fix task's issue body, and backlog/github.ts createIssue sends it after redactString(…, TRACKER) and redact().
  • Measured (scratch probe probe-ff.mjs, the built patchText, redactString and redact at this head):
    • A culprit commit adds a UTF-16LE file holding a ghp_-shaped token, and an ed25519 DER key.
    • Main's form of the diff says "Binary files differ" for both files.
    • At this head, the body as createIssue would send it still holds NUL characters. With the NULs dropped, the token is there in the clear, though the same token in plain text is redacted (control).
    • The key's raw bytes are in the body.
  • Why this is a reopened route. It is the same class round 9 and round 10 fixed for the owner-only decision diff (A5). Here it reaches a tracker issue, which more people can read.
  • A second possible effect (guess, not run against GitHub): GitHub may refuse a body that holds NUL characters. Fix-forward would then fail to file the task for any culprit that has a binary file.
  • A fix, for the builder to judge (inferred): pass the culprit diff through the same diffTextView, with its R1 fix, before it goes into the body. Add a test that fails at 0b1c7da.

R3 (c) overclaim: unscoped lines in the changelog fragment (changelog.d/927-git-runner.md).

  • "A test fails on a git call in src/ or scripts/ that doesn't come through it." The scan reads only .ts/.mts/.cts/.mjs/.cjs/.js files (test/git-runner.test.ts:247, measured by reading). 19 .sh files under scripts/ run git (measured, grep), and the PR's own Adversarial pass 8 names its other misses: names built at run time, shell strings, and a returned name.
  • "Every git the engine runs ignores replace refs and grafts." Two things run git outside the runner: those .sh helpers, and an existing install's old push-gate hook until setup:hooks reruns (the PR's own What remains).
  • What to do: scope both lines in the fragment the way the body's Adversarial pass and What remains already do. The Intent line in the body has the first wording too.

Attacks

# Attack (claim aimed at) Result Label
A1 Hunk header function-context text of a binary file ("a binary file's bytes never show") Shown, with control bytes and NULs (R1) measured
A2 Fix-forward culprit diff with a UTF-16 token and a DER key, sent to the tracker Token readable once NULs drop, key bytes present; main says "Binary files differ" (R2) measured
A3 Repo config diff.suppressBlankEmpty=true ends a hunk at a blank context line, so the rest of the hunk shows raw 21 of 21 key runs shown at this head. Main: the same git-dir write (info/attributes with diff) shows all 21 as well, so the capability is no wider than main's measured
A4 core.useReplaceRefs=true via repo config, the caller's GIT_CONFIG_PARAMETERS, the caller's -c, and GIT_REPLACE_REF_BASE; runner and env-only git Real commit read, no graft, in all 6 measured (git 2.43)
A5 The same on git before 2.42, where core.useReplaceRefs may set the global flag back Not run; no older git here guess
A6 Runner bypass: the scan at this head git-runner, decision-diff-binary and decision-diff-utf16 pass 20 of 20. Only src/decision-detail.ts changed this round, through gitExecFile measured
A7 Scan scope (changelog claim) Only JS/TS read; 19 .sh files run git (R3) measured
A8 UTF-16 and UTF-32 decoding of binary lines: up to 25% of units can be any code point, and C1 controls are not rejected Not built; low for real formats inferred
A9 "What this can never do": replace refs and grafts for any caller Held for every form in A4 measured, git 2.43 only
A10 "Compromised component", Dashboard False for headers (R1) measured

Follow-ups (not required)

  1. Decision diff: pin -c diff.suppressBlankEmpty=false (A3), as src/comment-policy.ts already does. Main has the same exposure through the git dir.
  2. Replace refs on git before 2.42: measure whether core.useReplaceRefs=true beats --no-replace-objects there, or state the minimum git version (A5).
  3. Binary view: the UTF-16 and UTF-32 checks let 25% of units be any code point, and don't reject C1 controls (A8).
  4. Changelog: say that a binary file in the decision diff now shows the text lines it decodes to, where main showed "Binary files differ".

The body's eight follow-ups stand as listed.

@jayden1711

Copy link
Copy Markdown
Owner Author

Evaluator verdict (delta final check, round 10): CHANGES NEEDED

Head checked: 0b1c7da. It is two signed-off commits on 4e4287e (633b03d and 0b1c7da). The round changes src/decision-detail.ts and adds test/decision-diff-binary.test.ts, nothing else (measured, git diff --stat 4e4287e6 0b1c7da0). Judged under the owner's round cap: only a break of main, a reopened route main closed, or an overclaim is REQUIRED.

Summary:

  • Fixed:
    • the DER-key case;
    • the NUL glue from the second review;
    • the UTF-16/32 cases;
    • the body's fail-closed cost and adversarial-pass entries.
  • Still open:
    • The view itself can crash the dashboard process.
    • Two routes still put a binary file's bytes on the owner's diff, where main shows "Binary files differ". So the body's "a binary file's bytes never show" is false.

Round-9 findings

Finding Status at 0b1c7da
Final check §4: a binary file's raw bytes on the owner's diff (DER keys) Fixed for the content lines (measured). work/delta-927-r9/diff-probe.mjs: the token is not visible in any of its 11 encodings, and neither key's hunk is its decoding. My own hdr-probe.mjs: a DER key replaced in place shows no 6-byte printable run of either key in 0 of 30 (ed25519), 0 of 6 (RSA-2048) and 0 of 6 (RSA-4096). A 64 KB random file with one byte changed mid-file shows none in 0 of 20. The class is not closed: see Required 2 and 3.
Final check: body (Dashboard line, push fail-closed cost, round-9 adversarial entries) The fail-closed cost and the 8d/8e entries are there (measured, body read through the API). The Dashboard line now overclaims (Required 4).
Second review §3: dropping NULs glued byte runs (AWS, Google and 0x hex shapes) Fixed (measured). The three shape tests fail at 4e4287e (expected: false, actual: true) and pass at the head. ver-927-r9/probes/diff-probe.mjs at the head: no token is visible in any case. The key-body case shows 10 lines, as on main (A7).

Fail-without: matches the body

  • measured: test/decision-diff-binary.test.ts was written with the Write tool as a new file into a fresh worktree at 4e4287e. Its blob is dd4a9086, identical to the head's. Built and run there, 5 of 7 fail:

    • the DER test;
    • the AWS, Google and hex shapes;
    • the one-NUL text file.

    The two UTF-32 cases pass, as the body says.

  • measured, at the head: decision-diff-binary, decision-diff-utf16, decision-diff-overflow, decision-detail, change-files-renames, git-config-count, git-runner and child-env-sites pass 139 of 139. That ran with a scratch HOME, state, TMPDIR and git config, and recording stand-ins first on PATH. The stand-in log stayed empty.

Changed or removed test lines

None this round (measured: the only test change is the new file). change-files-renames.test.ts and its never-grow list are untouched (measured, empty diff), so the CI fix in 0b1c7da changed the code, not the lint.

Required changes

  1. REQUIRED (a): the view can crash the dashboard process.

    • What happens (measured, work/delta-927-r10/long-probe.mjs): decodeWide builds a UTF-32 line with String.fromCodePoint(...points). For a UTF-32 file whose single line holds 150,000 or more code points (about 600 KB), that spread throws RangeError: Maximum call stack size exceeded. It fires at 150k, 200k and 300k. 100k returns.
    • Why it takes the process down: the throw happens inside the gitExecFile callback, so it is not a promise rejection. It reached process.on('uncaughtException') in the probe. src/dashboard.ts has no such handler (measured, grep), so the dashboard exits (inferred).
    • Who can cause it: any agent that commits such a file, as soon as the owner opens that change's diff. On main the same change shows "Binary files differ".
    • Why it matters: this breaks a page that works on main, and while the dashboard is down its off switches are unreachable (principle 2).
    • Fix: decode without a spread (in chunks, or a loop), and make the view fail closed: catch any throw and show the hunk as markers.
    • Test that fails at 0b1c7da: decisionDiff on a UTF-32 file with one line of at least 200,000 code points returns a diff.
  2. REQUIRED (b) and (c): the hunk header's function context shows a binary file's bytes.

    • What happens (measured, work/delta-927-r10/hdr2-probe.mjs): git writes the text of an earlier line of the file after @@ … @@. diffTextView pushes every @@ line as it is.
    • What shows: for a binary file (a NUL in it) changed below such a line, the owner's diff shows that line's bytes: printable runs, control bytes, and a raw NUL.
      • Example header: @@ -10,7 +10,7 @@ Q\u0000S3cr….
      • Main's args give Binary files a/f.bin and b/f.bin differ.
    • Not new this round: 4e4287e shows the same header (measured). It came with --text. The class is the one round 9 required closed, and the body now says it is.
    • Fix: run the header's text after the closing @@ through the same rule as a line. Show it only when it is clean UTF-8 text with no NUL or control byte; otherwise drop it or mark it. -c diff.<driver>.xfuncname can't do this for the default driver.
    • Test: a binary file with a NUL, changed below a line that starts with a letter and holds a printable run and a control byte. The run must not show.
  3. REQUIRED (b) and (c): one git diff setting turns the view off for the rest of a hunk.

    • What happens (measured, work/delta-927-r10/cfg-probe.mjs): with diff.suppressBlankEmpty=true in git config, git prints an empty context line as an empty line. diffTextView ends the hunk at the first line that has no +, -, space or \. So the rest of a binary hunk is pushed as it is, and its control bytes and printable runs show. Main shows "Binary files differ".
    • Who has the setting: the probe set it in the repo's config. The owner's own ~/.gitconfig is read the same way (inferred: the runner keeps HOME).
    • Why it counts: that makes it a route main closed, reopened for any install with that setting (principle 3).
    • Fix: pin -c diff.suppressBlankEmpty=false in decisionDiff's args, next to core.attributesFile, or treat an empty line inside a hunk as context.
    • Test: that setting in the scratch repo's config, with a binary hunk that has an empty context line before a changed line.
  4. REQUIRED (c): correct the body to match.

    • "Binary files: since 633b03d, a binary file's bytes never show" (Dashboard), and the Round 10 design text: false for items 2 and 3 until they are fixed.
    • 8e: add these three probes and their results.

Other checks

Follow-ups (not required)

Each is either open on main too, or a disclosed, fail-closed trade.

  1. The view is slow on many short binary lines. 2.7 million one-byte lines (8 MB) take 5.3 s, synchronous on the dashboard, against 0.67 s at 4e4287e (measured). It cuts and returns; nothing crashes.
  2. Small-valued 16-bit and 32-bit integer arrays read as UTF-16/32 text. They are byte-identical to Latin text, so any view that keeps F2's goal shows them. The body scopes this rule (measured: hdr-probe.mjs case c).
  3. Latin1 and control-byte hunks lose lines (the body's follow-up 3). The binary test also fires on hunks without a NUL, which main shows in full. Limiting it to NUL hunks would keep main's view for them. This is a disclosed, marked visibility loss; it is fail-closed and not a reopen, because main hid more with one NUL. The owner may want to rule on it.
  4. The body's eight follow-ups: each one checked, and none is a break, reopen or overclaim. Items 1, 5, 6 and 7 are open on main too. Items 2 and 3 fail closed. Item 4 is git: an instance's git runs nothing its checkout's config names, and .git/ is the coordinator's (#926) [PROTECTED: owner merges] #944's. Item 8 is docs.

Scripts are in the session scratchpad, work/delta-927-r10/: hdr-probe.mjs, hdr2-probe.mjs, hdr3-probe.mjs, cfg-probe.mjs, long-probe.mjs, mt.sh and run.sh. The round-9 probes ran from their own folders.

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>
@jayden1711

Copy link
Copy Markdown
Owner Author

Second adversarial review, after the cut: PASS (head 99b27ec)

This re-reviews the owner's cut ("Cut --text, land the runner"), with the round cap's rules. The review used scratch repos and a temp HOME, state, TMPDIR and git config, on git 2.43. Main is 8df2304, which this head already merges.

My round-10 required items

  • R1 (hunk-header bytes): gone (measured). probe-hdr.mjs, through the built decisionDiff, has five letter-led binary lines: printable, control byte plus NUL, NULs, invalid UTF-8, and an AWS-shaped key. Each diff now has no @@ line at all, because git calls the file binary. For random RSA DER lines, 0 of 40 showed a key run.
  • R2 (tracker body): gone (measured). probe-ff2.mjs runs the culprit diff the way merge-flow.ts now does (gitOrNull, then gitSpawnSync(['diff', sha^1, sha])):
    • its output is byte-identical to main's form of the command;
    • it says "Binary files differ" for both the UTF-16 token file and the DER key;
    • once redacted the way createIssue sends it, it holds no NUL and no token.
  • R3 (changelog scope): gone (measured, read).
    • The fragment scopes the runner to the engine's JS and TS code.
    • It names the .sh helpers and the old push-gate hook until setup:hooks reruns.
    • It states what the scan doesn't read.

Other checks

  • Is any other path showing a binary file's bytes? No (measured by grep and reading).
    • patchText (--text) has one caller, the patch hash at src/coordinator.ts:1523.
    • patch_hash is only emitted, in change.proposed and eval.verdict. No other place in src/ or web/ reads or compares it.
    • The other --text readers in src/ and scripts/ are all on main too: the evaluator's "Inspect with" line, sizeFiles, comment-policy.ts and the push gate.
    • decisionDiff differs from main's only in gitExecFile and the import (measured, by reading both).
  • diff.suppressBlankEmpty and info/attributes (measured):
    • With diff.suppressBlankEmpty=true in the repo config, the decision diff shows 0 key runs; it is "Binary files differ" as on main.
    • With info/attributes marking the file diff, it shows 14 of 14 runs, exactly as main's command does. That is the same as main, not a regression.
  • Runner core after the merge: holds (measured). probe-replace.mjs read the real commit, with no graft, in all 7 runner and env-only cases: plain env, repo config core.useReplaceRefs=true, the caller's GIT_CONFIG_PARAMETERS, the caller's -c, and GIT_REPLACE_REF_BASE. Raw git read the decoy.
  • Tests (measured): binary-diffs-as-main, git-runner, git-config-count and decision-detail pass 64 of 64. No test file on main lost a line beyond the fixture edits earlier rounds reviewed. The three removed tests were PR-only (measured, git diff --stat 8df23045).
  • Big binaries (inferred, from reading): patchText hashes --text output under the same 256 MiB buffer that sizeFiles already uses on main. So a large binary fails no earlier than on main.
  • Body and changelog: "The cut", the Dashboard bullet, the F2 bullet and the changelog match what I measured. I found no overclaim.

Required changes

None.

Follow-ups (not required)

  1. patchText's doc comment (src/coordinator/change-files.ts) still says its output is "for a person, a prompt or a hash". Since the cut it is for the hash only and must never be shown. Saying so would keep R2 from coming back through reuse.
  2. Replace refs on git before 2.42: check whether core.useReplaceRefs=true beats --no-replace-objects there, or state the minimum git version (guess, not run; no older git here).
  3. The root updater's verification clone (scripts/machine/worklane-update.cjs) is JS that sets no GIT_GRAFT_FILE. The body covers it, and it is root's own bare repo (inferred). The changelog's "every git the engine's JS and TS code runs" could name it.
  4. Showing a binary file's text lines safely, as the body's follow-up 3 says, if it is ever wanted.

@jayden1711

Copy link
Copy Markdown
Owner Author

Evaluator verdict (delta final check, the cut): CHANGES NEEDED (body only)

Head checked: 99b27ec. It is two signed-off commits on 0b1c7da:

Main is still 8df2304 (fetched first). Judged under the owner's round cap: only a break of main, a reopened route main closed, or an overclaim is REQUIRED.

Summary:

  • Every round-10 required item is gone. Both diffs now behave exactly as on main (measured).
  • One claim in "What this can never do" and the changelog is unscoped for git before 2.42. That is a body and changelog fix, and needs no code.

Round-10 findings, re-run at the head (all measured, scratch HOME, state, TMPDIR and git config)

Finding Probe 0b1c7da Head
R1: a long UTF-32 line crashes the dashboard long-probe.mjs, UTF-32 with 300k and 1M code points; 2.7M short binary lines uncaught RangeError; 5.3 s returns in 7 to 9 ms, "Binary files differ"
R2: hunk-header function context shows binary bytes hdr2-probe.mjs, hdr-probe.mjs header showed the run, controls and a NUL no @@ header; the same as main's args in every case
R3: diff.suppressBlankEmpty ends the view's hunk cfg-probe.mjs secret shown not shown, as main. The in-tree *.bin diff case shows on the head and on main alike
Second review R2: binary bytes in the fix-forward tracker body code, and the new test patchText (--text) diff: (sha) => git('diff', \${sha}^1`, sha) ?? '', identical to main's line. patchTexthas one caller left, thepatch_hashsha256 insrc/coordinator.ts:1523` (measured, grep)
Round-9 diff-probe.mjs (11 encodings, DER keys) "Binary files" for every binary case, the same as main

Two notes on the probes:

  • adv-927-r10/probe-ff.mjs still reports leaks. It calls patchText directly, so it no longer measures the culprit path. Its own "main-style culprit diff says Binary files differ for both" line, which is now the head's code, prints true.
  • The overflow branch removed in the cut: a 10 MB text diff gives null: not in this checkout's git: stdout maxBuffer length exceeded at the head and at main 8df2304 alike (measured, buf-probe.mjs).

Diff against main

git diff 8df23045 99b27ece -- src/decision-detail.ts src/coordinator/merge-flow.ts (measured) shows only these changes:

  • src/decision-detail.ts: the import (execFile to gitExecFile), the call (same args, options and callback), and a 4-line doc comment. The body says "only in the import and the call", which leaves out the comment (a nit).
  • src/coordinator/merge-flow.ts: spawnSync('git', …) to gitSpawnSync(…) at 12 sites, plus its import. The culprit-diff line is main's.
  • gitExecFile passes the caller's options through, with gitEnv over env and gitArgs around the args (read).

Fail-without: matches the body

  • measured: I wrote test/binary-diffs-as-main.test.ts with the Write tool as a new file into a fresh worktree at 0b1c7da. Its blob is b11dd461, identical to the head's.

  • At 0b1c7da: 2 of 2 fail, both on 'the binary file shows as main shows it'.

  • At the head: it passes, along with these files: 147 of 147, plus review and merge-policy at 78 of 78.

    • decision-detail, fix-forward and fix-forward-coordinator;
    • change-files-renames, git-runner, git-config-count and child-env-sites.

    Stand-ins were first on PATH, and their log stayed empty. fix-forward-coordinator passed here, where the body reports it failing in the builder's environment.

Tests on main

  • The cut changes no test that is on main (measured). It deletes three tests, and none of them is on main (git cat-file -e 8df23045:… fails for each): decision-diff-binary, -utf16 and -overflow. It adds binary-diffs-as-main.
  • The 7 main tests the PR changed earlier are untouched since 0b1c7da. They are fixture copies of scripts/lib/git.cjs, list additions and the runner's argument string, judged in earlier rounds.
  • The removed tests and buffer branch: they existed only in this PR, so removing them restores main and loosens nothing main had.

Body and changelog claims

  • Match (measured):
    • the cut's description;
    • the removed tests;
    • the fail-without (2/2);
    • typecheck and the denylist (1534);
    • the scan (files 524 unaccounted 0 at the head and on the main merge);
    • CI;
    • the Dashboard line;
    • the changelog's "as before" for both diffs and its patch_hash note.
  • Unscoped: see Required 1.

Required changes

  1. REQUIRED (c): scope the replace-refs claims to git 2.42 and later, or make them hold on older git.
    • The claims:

      • "What this can never do": "Turn replace refs or grafts back on for any caller … --no-replace-objects is in the argument list, which no environment takes back (measured in the runner test)";
      • the changelog's "Every git the engine's JS and TS code runs ignores replace refs and grafts";
      • the same line under Intent.
    • Why they are unscoped: they were measured only on git 2.43 (CI and here). RelNotes/2.19.0.txt adds core.usereplacerefs, and RelNotes/2.42.0.txt brings "a mechanism to disable replace refs globally" (ds/disable-replace-refs). As I read git's source before 2.42, core.useReplaceRefs=true sets the global flag back on. Config is read after --no-replace-objects and GIT_NO_REPLACE_OBJECTS, so a caller's GIT_CONFIG_PARAMETERS or -c saying true would turn replace refs back on (inferred, not run; no older git here).

    • Who that affects: distributions such as Ubuntu 22.04 ship git 2.34, and the repo states no minimum git version (measured, grep).

    • Not a reopen: main turns nothing off. The body's follow-up 10 names the question, but the "can never do" line still states the claim unscoped.

    • Fix, either of:

      • (a) Body and changelog only: "with git 2.42 and later (measured on 2.43); before 2.42, a core.useReplaceRefs=true in config may turn them back on (inferred)".
      • (b) By design: add -c core.useReplaceRefs=false to the runner's fixed args in both copies. Command-line config is read last, so it should win on every version (inferred), and the claims then stand.

      (a) is enough for this round.

Other checks

Follow-ups (not required)

  1. patchText's doc comment still says "for a person, a prompt or a hash". Its only caller is now the hash, and its --text output holds a binary file's bytes. Say "hash only, never shown" so no later caller shows it.
  2. decision-detail.ts differs from main in its doc comment too, not only "the import and the call" (wording).
  3. The body's ten follow-ups: I checked each one, and none is a break or a reopen. Item 10 is the git-version question behind Required 1; it stays a follow-up once the claim is scoped. Item 9 (diff.suppressBlankEmpty) only affects how text renders now, as on main.

Scripts are in the session scratchpad, work/delta-927-r10/: long-probe.mjs, hdr-probe.mjs, hdr2-probe.mjs, cfg-probe.mjs, buf-probe.mjs, mt11.sh and run.sh.

…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>
@jayden1711

Copy link
Copy Markdown
Owner Author

Second adversarial review, wording commit: CHANGES NEEDED (head 35f873e)

Under the owner's round cap. There is one REQUIRED item, and it is a one-line body fix. The code is fine.

What holds

  • No logic changed (measured). git diff 99b27ece 35f873ec touches only changelog.d/927-git-runner.md, where the git 2.42 scope is added, and the doc comment of patchText in src/coordinator/change-files.ts, which now says "for a hash only, never shown".
  • CI at 35f873e is green (measured). gh pr checks 927 reports 28 SUCCESS and 3 SKIPPED.
  • The scoped claims are true as labelled.
    • Measured on 2.43, in my round-10 and round-11 probes: core.useReplaceRefs=true in the repo config, in a caller's GIT_CONFIG_PARAMETERS, or by -c, and GIT_REPLACE_REF_BASE all left the real commit read, through the runner and through an env-only git.
    • "Before 2.42 … may (inferred, not run)" is labelled. The 2.42 boundary itself rests on reading git, not on a run. The text says "measured on 2.43", so it does not claim more than that.
  • The cut's claims still match what I measured. These are the Dashboard and fix-forward bullets, "Binary files differ", and patchText used only for the hash.
  • Kept: my round-10 R1 to R3 (the cut, and the scoped changelog), and follow-ups 1 and 2 from round 11 (the doc comment, and git before 2.42, now follow-up 10). Also kept: the earlier diff.suppressBlankEmpty follow-up (now 9) and the safe binary view (now 3).

Required

(c) overclaim, introduced by the trim: the root updater is listed as running "with the settings written out".

  • The claim. Under "Exceptions this adds", the body lists "files that run on their own with the settings written out (four scripts and the root updater)".
  • What the code does (measured, by reading). scripts/machine/worklane-update.cjs:1674 is const VERIFY_GIT_ARGS = ['--no-replace-objects'];. That turns replace refs off only. There is no GIT_GRAFT_FILE and no core.commitGraph=false. The scan's own entry for that file (test/git-runner.test.ts:231) says so correctly: "replace refs off; that clone is root's own, which no agent can write a graft into".
  • What the trim dropped. The earlier body had Adversarial pass 9, which said the root updater "doesn't set GIT_GRAFT_FILE, but that clone is root's own". That caveat is gone. My round-11 follow-up 3, about the same file, is dropped as well.
  • What to do. Reword that line to match the scan's entry: the root updater has replace refs off only, and its verification clone is root's own (inferred: no agent can write a graft or commit-graph there). Or put the follow-up back.
  • The Intent line still holds in effect (inferred). No agent-writable graft reaches that clone. The "settings written out" wording is the false part.

Follow-ups (not required)

  1. Label the 2.42 boundary itself as inferred, in the Intent and in "What this can never do". Only 2.43 was run.

Refusals: none.

@jayden1711

Copy link
Copy Markdown
Owner Author

Evaluator verdict (delta final check, wording round): CHANGES NEEDED

Head checked: 35f873e. It is one signed-off commit on 99b27ec, with no attribution trailers (measured). Judged under the owner's round cap.

Summary:

The diff (measured, git diff 99b27ece 35f873ec)

  • Two files changed: changelog.d/927-git-runner.md and src/coordinator/change-files.ts.
  • The .ts change is the patchText doc comment only: "for a hash only, never shown … a binary file's bytes included". No code line changed.

Round-11 required item: fixed

The replace-refs claims are now scoped to git 2.42 and later (measured on 2.43), with older git marked inferred, in all four places (measured, read):

  • the changelog's Upgrading bullet;
  • the changelog's runner bullet;
  • the body's Intent;
  • "What this can never do".

"Replace refs stay off whatever the config says on git 2.42 and later" matches round 10's second review, which measured A4 on git 2.43. Follow-up 10 carries the by-design fix.

Required changes

  1. REQUIRED (a): merge main 7b13c8b and move guard: the commit scan covers every commit form and checkout the guard allows [PROTECTED: owner merges] #979's git calls onto the runner.
    • gh pr view 927 reports mergeable: CONFLICTING (measured).
    • git merge-tree --write-tree 35f873ec 7b13c8b2 conflicts in src/coordinator/change-files.ts and src/scan/secrets.ts (measured).
    • The runner scan on that merged tree reports 2 unaccounted files: src/hook.ts (1 call) and src/scan/secrets.ts (5 calls). Those are guard: the commit scan covers every commit form and checkout the guard allows [PROTECTED: owner merges] #979's raw git calls (measured, work/delta-927-r10/mt12.sh).
    • A resolution that leaves them raw makes test/git-runner.test.ts fail on main (inferred from the scan, which mirrors the test's patterns).
    • Move them onto the runner, never into OUTSIDE_THE_RUNNER, then rerun CI on the merge.
    • Why it is required: it is not a defect in this round's change, but it is needed before anything can merge.
  2. REQUIRED (c): the Agent-user line overclaims for .git/shallow.
    • The line: "can still write refs/replace/*, info/grafts, .git/shallow and the commit-graph … Within the scope above, none of these changes what a runner git reads".
    • Why it is false for .git/shallow:
      • The runner reads the shallow file; src/os/git.ts:20 says so.
      • Only the push limits and the secret scan read one checked snapshot of it, and refuse a cut in the pushed range.
      • measured, work/delta-927-r10/shallow-probe.mjs: gitSpawnSync(['rev-list', '--count', 'HEAD']) gives 5 without the file. With a .git/shallow written at HEAD~2, it gives 3.
    • The pre-trim body was precise: "Replace refs and grafts no longer change what any engine git reads. A .git/shallow that cuts the range makes the push limits refuse it."
    • Fix: restore that, keeping the 2.42 scope. For example: "Within the scope above, replace refs, grafts and the commit-graph no longer change what a runner git reads. A .git/shallow still does, as on main. The push limits and the secret scan read one checked copy and refuse a cut in the pushed range."
    • Not a reopen: main reads the shallow file too. The finding is the wording only.

The rest of the trimmed body

Checked against the code and my earlier measurements:

  • Match:
    • the cut's description, and the decision-detail diff against main (now naming the doc comment);
    • the fail-without table's binary-diffs-as-main row (2/2, measured in round 11);
    • the package size (+6,245 / +17,391, measured in round 11; 35f873e changes comments only);
    • "What this can never do" and the Dashboard and fix-forward lines;
    • the follow-ups.
  • The history table links each round's reviews.
  • The rows I checked hold: "Loosen a check" and the principle-9 exceptions are unchanged from rounds already judged.

Other checks

Follow-ups (not required)

  1. Risk class: name src/merge-policy-weakening.ts again. The PR touches it (measured, git diff --name-only), and src/** covers it, but naming it keeps critical-digest routing obvious. The pre-trim body named it.
  2. Overlap: after the main merge, re-measure the Review levels: a path that looks like a locked or protected one (UTS #39 skeleton) always waits (#763) [PROTECTED: owner merges] #910 row.

The scripts are in the session scratchpad, work/delta-927-r10/: mt12.sh and shallow-probe.mjs.

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>
@jayden1711

Copy link
Copy Markdown
Owner Author

Independent second verification review: PASS

Exact head: 021f6f18cca0458d9a4508b6df748bd266d7fcf4.

The orchestrator records this independent review separately from its own implementation work and the independent final check.

  • Measured independently on this exact head: isolated build passes; eight named test files pass 82/82, with no failures, skips or TODOs. The files cover the git runner, strict configuration counts, stale worktree locks, alternate-store and kept-worktree denials, policy snapshots, PR hygiene and feedback checks. Seven existing documented probe variants also pass.
  • Inferred from exact-main comparison: the conflict resolutions preserve main's regular-file config reader, policy snapshot and worktree ownership/placeholder/unlock behavior. The inspected git call substitutions keep their arguments and options.
  • Measured: runner implementations and their scan test are unchanged from the prior reviewed runner head. No exception-table expansion or test-assertion loosening was found.
  • Claim review: the pushed body correctly distinguishes current builder measurements from historical independent evidence, and retains the documented limitations. No new material overclaim was found in the integration delta.

Required changes: none under the owner's break/reopen/overclaim round cap. Existing nonblocking follow-ups remain recorded in the PR body.

This PASS is the independent code-and-claims verdict on the exact head above. Current-head required CI and the fresh combined check remain separate landing gates; this comment asserts neither has completed.

@jayden1711

Copy link
Copy Markdown
Owner Author

Independent final check: PASS

Exact head: 021f6f18cca0458d9a4508b6df748bd266d7fcf4.

The orchestrator records the independent final review here. The final reviewer is distinct from the implementation author, orchestrator and second reviewer.

  • Measured independently at this exact head: 21/21 focused tests pass, with no failures or skips. These cover the runner, policy snapshots, stale worktree locks, alternate stores and retained old worktrees; the real secret scanner and standalone push-gate checks ran.
  • Inferred from exact-main comparison: the integration keeps main's regular-file reader, one policy snapshot, ownership checks and unlock ordering, with unchanged git arguments/options behind runner substitutions.
  • Measured: the runner core is unchanged from the prior independently tested head. The body distinguishes historical evidence, builder measurements and pending current CI correctly.

Required changes: none under the owner's break/reopen/overclaim round cap. Existing disclosed limitations remain follow-ups. This is the independent code-and-claims PASS; exact-head required CI and fresh combined CI are separate landing gates.

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