Repository navigation
Adversarial suite: attack cases for doubt, the off switches, the stop, weakening, evals and outside-plan (#707) [PROTECTED: owner merges] - #957
Conversation
…ening, evals and outside-plan The suite fed every case the most permissive verdict, both auto-merge switches on, no stop, every eval passing, no weakening read and no plan, so removing any of those holds left it green (the scoreboard's mutants). A case can now turn one input hostile (`inputs`, schema-limited to values less permissive than the stub's), or be decided by the coordinator's own mergeDecisionFor (`flow`), which reads the stop file, the policy reader, the verdict's head, the event log and the change itself. 55 new cases and 3 new controls; each case fails under an in-memory mutant that breaks exactly its guarantee. The #820 and #839 todos become real cases (weakening.json, guard-folder-new-file); #788's stays, since the suite doesn't drive the readiness check. Test (fails without): each new case fails under its mutant from the in-memory mutant set (table in the PR); the scoreboard's own mutants, green before, now turn the suite red. Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
#820, #788, #789, #769, #770, #834, #839, #844, #845 and #883 are all merged (gh pr view), but the page still listed them as in review or as open gaps, and said the adversarial suite had not landed. It now lists them as merged and says which of them the suite has cases for. Test (fails without): none, a docs change; test/docs-guardrails.test.ts checks the new file cites. Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
Evaluator verdict: PASS (final check, head d51d3ab)Final check of head One merge-order item for the orchestrator (not a change to this PR): this PR and #886 have a semantic conflict that 1. Mutants (measured)
2. No vacuous case
3. Weakening-gate items, for the owner
4. The
|
Second adversarial review: CHANGES NEEDED (head d51d3ab)Independent second review of head Setup. A detached scratch worktree at this head. Every run had a temp HOME, temp state and held-out dirs, and recording stand-ins first on PATH (gh, claude, sudo, systemctl, docker, tailscale, loginctl, launchctl, useradd; each exits 1). No stand-in was ever called (measured). Mutants are applied in memory by the scoreboard's ESM load hook; no repo or worktree file was edited. Every mutant run printed Bottom line. The engine is not weakened: this PR changes only test/ and docs/, and main's code held every realistic history I built (measured). The lock and the weakening read both catch every way I tried to narrow the suite's own tables (measured). But the claim that each case fails if its guarantee breaks doesn't hold for 4 flow cases. Their fixtures record events in a shape a live instance never produces, so a mutant that reads the right event the wrong way stays green. Two of those mutants also pass the coordinator's own unit test file. Two more mutants break a claimed guarantee with the suite green: one in the outside-plan check, and one that stops treating a skill under Attacks
The patternThe flow cases prove that an input reaches the gate, and the PR body says so ("a flow case fails if the flow stops passing an input to the gate"). But the coverage table counts hold.doubt, hold.auto-merge-stopped, hold.outside-plan and inputs.verdict-head-only as covered. Each of those guarantees also depends on mergeDecisionFor selecting the right events: by head, latest-wins, and across a history. That selection logic is in the flow, and the fixtures can't see it: one event each, recorded at the PR's current head, which for a CI fix or a refused push is a shape a live instance never produces. X2 and X3 aren't caught by Required changes
No owner decision needed: these are fixture and claim fixes under decision-guide principles 4 and 5. Attack 10 is a pre-existing item for a separate issue. "What this can never do" and "Compromised component", line by line
|
… for the selection the flow does The second review found seven mutants of src/ that break a guarantee the suite claims, with the suite green. The flow cases recorded one event each, at the PR's current head, which a live instance never does for a CI fix run or a refused push, so the flow's selection of events (by head, latest wins, across a history) went unseen. - The flow fixture shape: a CI fix run starts on the head the PR was opened at and pushes the current head (ci_fix.finished); a refused push is of a commit that never became the head; a stop deleted by hand follows an earlier owner stop and its resume. - New flow fields, each only less permissive: a later rejection on the same head (rejectedAfter), a merge-in of the default branch (mergedIn), a passing eval recorded for another issue (evalsForAnotherIssue), a plan revision (earlierPlan), and a stop file with no reason. - A fourth review.yaml, auth-at-l2, used only by the cases that name it: a repo that names auth paths but holds them at L2, where only the plan check's engine L3 list holds an unplanned auth file. - New cases: flow-verdict-rejected-after-pass, flow-merge-in-rejection-carried, flow-stop-file-no-reason, flow-outside-plan-auth-not-held, flow-outside-plan-revised, flow-skill-eval-for-another-issue, eval-claude-skill-not-evaluated, eval-agents-md-not-evaluated, weakening-type-check-suppressed. New controls: control-merge-in-in-flow, control-planned-auth-change-in-flow. No existing case on main changes. The adversarial suite's own run: 175 tests, 165 pass, 0 fail, 10 todo. Test (fails without): each of the review's mutants X1 to X12 turns at least one case red (X1 flow-ci-fix-run, X2 flow-push-refused, X3 flow-verdict-rejected-after-pass, X4 flow-skill-eval-for-another-issue, X5 flow-outside-plan-auth-not-held, X6 flow-stop-file-deleted-by-hand, X7 flow-stop-file-no-reason, X8 flow-merge-in-rejection-carried and control-merge-in-in-flow, X9 eval-claude-skill-not-evaluated, X10 eval-agents-md-not-evaluated, X11 weakening-type-check-suppressed, X12 flow-outside-plan-revised). Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
…n't yet The scoreboard paragraph said only which merged guards the suite covers. The second review on #957 found the flow cases' reach overstated: the verdict carried across a merge-in had no case, and the flow's eval side drives only a task with no eval recorded. This lists what the flow cases drive after round 2 and what they don't: an eval the task did run (driven at the gate only), a conflict fix's reasons, labels and money paths from the log, and readiness. Test (fails without): test/docs-guardrails.test.ts checks the cited files exist (9/9 pass); the claims are measured by the in-memory mutants listed on #957. Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
…he never-grow mark #886 moved the weakening hold onto marks: weakeningOf takes the default branch's tip, and mergeDecisionFor fetches that tip from the remote. This applies the adaptation #886's PR body gives, built and measured there: every case's repo gets a bare origin with main at the base, the gate path passes the base as the tip, and weakening-lint-allowlist-grown's fixture carries the `// check-table: never-grow` mark that replaced its central CHECK_TABLES entry. The flow's merge-in also moves origin's main to the commit it merged, as the default branch does. No case's expected reasons change. The adversarial suite's own run: 175 tests, 165 pass, 0 fail, 10 todo. Test (fails without): weakening-lint-allowlist-grown fails without the mark (measured on #886's merge note). Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
Round 2 pushed: head 5d0295fThis answers the second adversarial review (comment 6311298230). The details are in the PR body's "Round 2" section.
#886 had merged by the time this round was ready. So I merged origin/main 40ef312 and applied #886's adaptation exactly as its body gives it. Typecheck, denylist, DCO and the push gate are clean (measured). |
Second independent adversarial review, round 2 (head 5d0295f)Verdict: CHANGES NEEDED. The suite adds real attack coverage, and nothing in it can loosen the gate. But 18 new in-memory mutants each weaken one of the guarantees this PR counts as covered, and the suite stays fully green under every one of them. Each mutant also lets a hostile scratch PR merge that main holds. So "12 of 49 covered" doesn't hold under the PR's own definition ("covered when every mutant ... turns the suite red"). Some of them come from a vacuous input: in every case, the default branch's tip is the base. Method (measured)
New mutants: suite green, route open
Spread across the counted guarantees:
Each is a one-line weakening of the kind a refactor makes. None is the same edit as round 1's 54 or X1 to X12. R15 and R18 are list entries, close to the named "word list" limit of hold.design-weakening. The other four there are logic. Judged
Required changes
No route that main had closed is reopened: the PR changes no file under src/, scripts/, .github/ or .worklane/ (inferred, from the file list). |
Evaluator verdict: CHANGES NEEDED (round 2/2) (delta final check, head 5d0295f)Delta final check of head Setup. Detached scratch worktrees at 5d0295f and d51d3ab. Every run had a temp HOME, temp state, held-out and TMPDIR dirs, and recording stand-ins first on PATH (gh, claude, sudo, systemctl, docker, tailscale, loginctl, launchctl, useradd; each exits 1). Only One fix needed, and it is a claim fix: the coverage table counts hold.auto-merge-stopped as covered with no known limit, but it is open on main (#982). Details and the count are under item 4. 1. All 66 mutants re-run at 5d0295f (measured)
2. The X5 fixture,
|
Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
…and the default branch's tip The second review of round 2 wrote 18 one-line mutants of src/ (R1 to R18), each opening a merge route main holds, and the suite stayed green under all of them. Part of that was a vacuous input: in every flow case the default branch's tip was the base, so #869's tip handling was never driven. - New flow fields: the default branch's tip can't be fetched (tipUnreadable), or moved past the base with files of its own (tipMoved); a verdict with no design-change answer; a push refused before the PR opened; a stop event that names no cause, and a resume request after a red-main stop, each with the stop file deleted by hand. - A new eval outcome, held-out-absent: a role's result with no held-out field at all. - A review.yaml variant, loosened-names-lock: the loosened one, also naming the locked agent-instructions category with no paths. Used only by the case that names it. - 18 new cases, one or more per mutant, and control-default-branch-moved-in-flow, which shows a moved tip alone holds nothing. - A todo line for #982: a stop file whose reason is an empty string doesn't stop auto-merge on main. Fixtures that would change only .md files also change an app file: under the loosened config such a change is docs-only, and the suite's claim check (claiming logic changes no class) holds only for a change at logic or above. The adversarial suite's own run: 195 tests, 184 pass, 0 fail, 11 todo. Test (fails without): each of R1 to R18 turns at least one new case red, as listed on #957; R1's anchor is adapted to main's computeLevel after #798. Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
… main The adversarial suite's paragraph listed what its flow cases drive. Round 3 adds the default branch's tip (one that can't be fetched, one that moved past the base with a new mark), a verdict with no design-change answer, a push refused before the PR opened, and two more stop histories. It also names #982 as open on main, and says the mutants behind the "measured" label bound what was missed rather than prove coverage. Test (fails without): test/docs-guardrails.test.ts checks the cited files exist (passes); the claims are measured by the in-memory mutants listed on #957. Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
Round 3 pushed: head fd9893cThis answers comments 6320437354 (second review) and 6321053637 (final check). Details are in the body's "Round 3" section.
Checks (measured): typecheck clean, denylist 1483 files clean, DCO and the push gate clean, docs-guardrails, merge-policy and engine-guard-locks 71/71. |
Second adversarial review, round 3: head fd9893cVerdict: CHANGES NEEDED. One REQUIRED change (c), an overclaim: the coverage count. Everything else is FOLLOW-UP. Method (measured)
New mutants: the suite and the 23 unit files stay green (measured)
Mutants the suite misses but the unit files catch (measured)These are not counted against the PR.
Does any case pass vacuously? Two input shapes (measured)
Every case's expected reason is specific to its hold. I found no case held by a reason other than the one it names. REQUIRED
Follow-ups (not required)
"What this can never do" and "Compromised component"
The mutant definitions are in the session scratchpad's |
Evaluator verdict: PASS (delta final check, head fd9893c)This is the delta final check of head
Every claim is labelled measured, inferred or guess. Every finding is labelled REQUIRED or FOLLOW-UP. No required changes. Setup.
1. All 84 mutants re-run at fd9893c itself (measured)
2. The tip cases really drive #869's tip path (measured)
3.
|
Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
…the round-3 review The round-3 review of #957 found 14 one-line mutants of src/ that leave the suite green. The flow paragraph said "the latest plan" was driven, but only one task's own plan of exact paths is. It also didn't name new top-level modules, size, or another task's plan or verdict as not driven. This narrows the plan claim, extends the not-driven list, and names the review's open items as follow-ups. Test (fails without): test/docs-guardrails.test.ts checks the cited files exist (9/9 pass); the claims are measured by the review's in-memory mutants on #957. Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
Second adversarial review, round 4: head fac8555Verdict: PASS at fac8555, under the round-cap rule. My REQUIRED (c) from round 3 is fixed.
No new overclaim.
Follow-ups (not required). These are unchanged from round 3 and listed in the body: the cases that would kill Q1 to Q13, Q19 and Q20, and the tip lint cases in the suite itself. |
Evaluator verdict: PASS (delta final check, head fac8555)This is the delta final check of head 1. The diff from fd9893c is a merge plus docs only (measured)
2. The count and limits match the review (measured against comment 6333173487)
3. The suite and docs at the head (measured)
4. merge-tree (measured)
5. CI at fac8555 (measured, not rerun)Run 39727722361 has head_sha fac8555 and concluded success:
Nothing is pending, so there was no wait. Required changesNone. Follow-ups (not required)
|
Intent: the adversarial suite (#707) fed every case the most permissive verdict, both auto-merge switches on, no stop, every instruction eval passing, no weakening read and no plan. So taking out the doubt, off-switch, stop, weakening, eval or outside-plan holds left it green (the scoreboard's mutants N1 to N4: 88/88 green). This PR adds 82 attack cases and 6 controls for those holds and for the coordinator's own reading of them. Each new case is measured to fail under an in-memory mutant that breaks exactly its guarantee. It also turns the #820 and #839 todos into real cases, and fixes the stale "in review" list in docs/guardrails.md.
Risk class: protected.
test/adversarial/**is locked as self-modification since #885 (src/review.ts SELF_MODIFICATION_PARTS), and the suite is in the critical set. Test and docs only: no file under src/, scripts/, .github/ or .worklane/ changes.Part of #707 (lane: verifiable, part A).
Round 4 (head fac8555)
Reviews of fd9893c:
(c) The count was an overclaim. It is now at most 8 of 49, not 11. The second review wrote 21 one-line mutants of src/ (Q1 to Q21). The suite stays green under all of them (measured by the reviewer). Under this body's own rule ("covered when ... no known limit is open"), these guarantees now have open limits:
TRUNCATEdropped from the deletes-data words; Q12: the flow's new-top-level testmergeDecisionForcomputes them, plans other than one task's own plan of exact paths, and another task's verdict at the same head;Follow-ups (listed, not done):
issue.seenlabels and amoney_path_sourcelist on the default branch.seed: falseand with a big change.TRUNCATE(andDROP COLUMN/DROP SCHEMA), or record its word list as a limit, as for weakening.merge-policy-tip.test) already catch these mutants.tipMoved: move the tip from a second clone, not a branch in the PR's own repo.Checks at this head (measured):
test/docs-guardrails.test.ts: 9/9;check:dco origin/main HEADall signed off; the push gate clean.The mutants weren't re-run: no test or src/ file of this PR changed this round.
Round 3 (head fd9893c)
Reviews of 5d0295f:
adv-957-r2/mutants.json;What they found.
What changed (
test/adversarial/adversarial.test.tsand the case files). Each new field only adds an event, a decoy main must ignore, or a less permissive input:tipUnreadable: the remote is gone, so the tip can't be fetched;tipMoved: the default branch moved past the PR's base with its own commit (origin'smainmoves there, and the PR doesn't have it).control-default-branch-moved-in-flowshows a moved tip alone holds nothing.verdictNoDesignAnswer: a verdict with no design-change answer (an older or failed verdict);pushRefused: "before-open": a refused push before the PR opened.stopFile: deleted-by-hand-no-by: a stop event that names no cause;stopFile: deleted-by-hand-resume-requested: a red-main stop, then an admin's resume request.held-out-absent: a role's result with no held-out field at all.loosened-names-lock: the loosened one, also naming the locked agent-instructions category with no paths. Only the case that names it uses it.{ ...locked, ...own, ...}for{ ...own, ...locked, ...}).closes: "#982". It is listed as todo, not as a case, because it fails on main.Re-run, all 84 mutants (measured).
MUTANT-APPLIED, no anchor was off, and no stand-in was called.git diff --stat cec056a5 origin/main). So their results are inferred to hold.Coverage, re-counted (limits counted against). The rule: a guarantee counts as covered when every mutant from the three sets above that breaks it turns the suite red, and no known limit is open.
carriedHead's loop past the first hop has no caseThe count:
Adversarial pass, round 3:
tipMovedadds a commit the PR doesn't have; the control shows it holds nothing alone. Label: measured.tipUnreadableonly takes the remote away.flow-default-branch-marked-table-grownalso ran in the managed-app scope, and merged there (measured).holds, by design). So the case runs in the engine scope only, as round 1'sweakening-lint-allowlist-growndoes.Checks at this head (measured):
npm run typecheck: clean;check:denylist: 1483 files clean;check:dco origin/main HEAD: all signed off;check:push: clean;test/docs-guardrails.test.ts,test/merge-policy.test.tsandtest/engine-guard-locks.test.ts: 71/71.The full suite was not run locally (owner rule); CI runs it.
Round 2 (head 5d0295f)
Review: the second adversarial review, comment 6311298230. Its 12 mutants are in the session scratchpad's
adv-957/mutants.json.What the review found. The flow cases recorded one event each, at the PR's current head. For a CI fix run or a refused push, a live instance never records that shape. So the flow's choice of events (by head, the latest one, across a history) had no case: X1, X2, X3 and X6 left the suite green. X5 (outside-plan without the engine's own L3 list) and X9 (a skill under
.claude/skills/is no eval target) did too. The coverage table overstated inputs.verdict-head-only and hold.eval.Fixed by the fixture shape, not one case at a time (
test/adversarial/adversarial.test.ts):ci_fix.finishedpushes the PR's current head (an empty commit on top), so the head is still the harness's own.git commit-tree, on no branch).rejectedAfter: a later rejection on the same head after the PASS;mergedIn: the harness merges the default branch into the PR, and the verdict is carried from the head before it;evalsForAnotherIssue: a passing eval of the same targets, recorded for another task;earlierPlan: a plan that the approved one revised;stopFile: no-reason: a stop file written by hand with no reason.auth-at-l2, used only by the cases that name it. It is the default with auth moved from L3 to L2: the repo names auth paths but doesn't hold them. The default case configs stay the three they were.New cases (9) and controls (2):
Re-run of every mutant at this head (measured). All 66: the round-1 set (N1-N4, S, D, DF, E, P, W, G1, F) and the review's X1-X12. Same method and command as the table below. Every run printed
MUTANT-APPLIED, no anchor was off, and no host-tool stand-in was called. Baseline: tests 175, pass 165, fail 0, todo 10. Every one of the 66 turns the suite red. Before the merge of origin/main (#886) I ran all 66 on 415f11d's tree: the same failing cases for each.The round-1 mutants, at this head (the new cases they now also fail are in bold):
This also closes round 1's gap: the 28 mutants that weren't re-run after the first merge of origin/main have now been run at this head.
Merged origin/main 40ef312 (with #886), and applied #886's adaptation exactly as its PR body gives it (measured: built, typecheck clean, the suite green):
originwithmainat the base;r.baseas weakeningOf's new 6th argument, the tip;weakening-lint-allowlist-grown's fixture carries the// check-table: never-growmark that replaced its centralCHECK_TABLESentry. Itssourcetext now names the mark (Merge policy: a new check table or lint test registers itself where it's declared; only removing or loosening one waits for the owner #869).One addition of mine: a merge-in also moves origin's
mainto the commit it merged, as the default branch does.Coverage, re-counted (measured by the re-run). A guarantee counts as covered when every mutant from both reviews that breaks it turns the suite red:
flow-merge-in-rejection-carried, one merge-in is covered. A chain of merge-ins is not:carriedHead's loop past the first hop has no case.inputs.evals. So the flow's choice among the task's own evals (the latest per target) has no case.Adversarial pass, round 2:
mergedInchanges how the change is measured, from the merged default branch, exactly as mergeDecisionFor does.control-merge-in-in-flowshows the merge-in alone holds nothing, andflow-merge-in-rejection-carriedshows a carried rejection still holds. Label: measured, the suite..claude/skills/*/SKILL.md-only change is class docs-only under the loosened config, so "claiming logic changed the class" failed (measured). I didn't touch the check (rule 10). The fixture now also changes the skill's helper script, so its class is logic. Observation, not changed here: under the loosened config, a skill's text alone goes out as docs-only. Label: measured, by that failure.FAKE-HOST). The flow's newgit fetchgoes to a bare repo in a temp dir.made()calls, but each case's repo is made once per run and decided many times. TheassertSameEveryTimeand injection checks pass for every new case (measured).Checks at this head (measured):
npm run typecheckclean;npm run check:denylist1443 files clean;check:dco origin/main HEADall signed off;check:pushclean;test/docs-guardrails.test.tsandtest/merge-policy.test.ts65/65;test/engine-guard-locks.test.ts3/3. The full suite was not run locally (owner rule); CI runs it.Not done here; proposed separately:
What changed
The harness (
test/adversarial/adversarial.test.ts). Two optional fields on a case or control:inputs: turns one of mergeDecision's other inputs hostile. The schema allows only values less permissive than the stub's:policyOn: false,repoOn: false,stopped: <reason>,verdict: nullor a lower verdict (patch_correct: false,test_correct: false,confidence: medium|low,unread,design_change: true|null),ciFixRuns,foreignPush: true,pushLimitHit: true,evals: <outcome>,weakening: true(the change is read byweakeningOf, as mergeDecisionFor does), andplan: [files](outsidePlanReasons, with mergeDecisionFor's own wait list).flow: the case is decided by the coordinator's ownCoordinator.mergeDecisionFor. The coordinator reads every input as on a live instance: the event log (pr.opened, worker.claims, eval.verdict and its head, plan.posted, ci_fix.started and ci_fix.finished, push.refused, pr_hygiene.merged_in, instructions.eval, merge.stopped and merge.resumed), the stop file, and theautoMergereader. The defaults are the most permissive. A flow case fails if the flow stops passing an input to the gate, not only if the gate stops reading it. This closes part of Adversarial suite: one pure gate function shared with merge-flow; name the remaining floor gaps (follow-up to #855) #860's point that the suite rebuilds mergeDecisionFor's inputs itself.Cases. 82 new cases: 80 in 6 new files, and 2 appended to main's files. That is round 1's 55, round 2's 9 and round 3's 18:
Controls. 6 new controls, each proving its mechanism doesn't hold everything:
control-planned-app-change,control-test-added-weakening-read,control-app-change-in-flow,control-merge-in-in-flow,control-default-branch-moved-in-flowandcontrol-planned-auth-change-in-flow.Existing cases: none changed. No existing case or control sets
inputsorflow, so each one is decided exactly as before (measured:grep -l '"inputs"\|"flow"'lists only the new files, controls.json's new controls, and none of the old case files).Test-file changes for the weakening gate (rule 9)
Listed so the gate and the reviewer see each one:
Removed todo Merge: a PR that removes or weakens a test, lint or guard waits for the owner (#783) [PROTECTED: owner merges] #820 ("a PR that weakens a test, lint or guard ... waits for the owner"). Each sub-item now has a passing case:
weakening-assertion-changed,weakening-assertion-removed;weakening-skip-mark-added,weakening-only-mark-added,weakening-lint-disable-added;weakening-lint-allowlist-grown,weakening-allowlist-file-grown;weakening-test-moved-out-of-runner;weakening-test-excludes-widened.Each is measured to fail under its mutant (table below).
Removed todo Tool guard: its lanes' rules move to src/guard/, so it loads 57 modules, not 83, and only those are locked as the guard (#785) [PROTECTED: owner merges] #839 ("a new file under the tool guard's own folder is locked ... under a review.yaml that lists no src/").
guard-folder-new-fileruns under all three configs, the loosened one included. It fails under G1 (src/guard/ taken out of the tool guard's lock).Kept todo Required checks count only from the App or workflow pinned for their name [PROTECTED: owner merges] #788, reworded its
closes: "Required checks count only from the App or workflow pinned for their name [PROTECTED: owner merges] #788 (merged; the suite doesn't drive the readiness check yet, so this needs a readiness case)". Pinned check sources are judged by readiness, before mergeDecision. The suite doesn't call readiness, so no case here can fail if that breaks. The todo stays.A new normalization in the injection comparison (A1):
sizelessnow also drops a weakening reason's place list (design: may weaken a check: <kind> (<places>)), keeping the kind..gitleaksignore), the weakening check rightly names that added line too.design: may weaken a check:reasons. No existing case gets one (weakening is read only forinputs.weakeningand flow cases), so no existing assertion changes.Mutant → case table (round 1, measured; superseded by the Round 2 re-run above)
Method: the scoreboard's ESM load hook (
mutant-hooks.mjs), copied into the session scratchpad with its ownmutants.json. It rewrites one compiled module as it loads; no file in the repo or the worktree is edited. Each anchor must match exactly once, or the run aborts. Every run printedMUTANT-APPLIED. Command, per mutant:Baseline at round 1's head, with no mutant: tests 164, pass 154, fail 0, todo 10 (measured).
Where each mutant was measured in round 1.
if (!i.policyOn)→if (false)if (!i.repoOn)→if (false)if (i.stopped)→if (false)doubt: no evaluator verdictpush removedif (!v.patch_correct)→if (false)if (!v.test_correct)→if (false)if (v.confidence !== 'high')→if (false)if (v.unread?.length)→if (false)if (v.design_change === undefined)→if (false)if (i.ciFixRuns)→if (false)if (i.foreignPush)→if (false)if (i.pushLimitHit)→if (false)if (o.design?.flag === true)→if (false)reasons.push(...(i.instructionEvals ?? []))removedelse if (r.incomplete)→else if (false)else if (r.dropped)→else if (false)else if (h.incomplete)→else if (false)else if (h.dropped)→else if (false)reasons.push(...(i.outsidePlan ?? []))removedreasons.push(...(i.weakening ?? []))removedhit('test-file-deleted', ...)→ no-ophit('test-assertion', ...)→ no-ophit('test-lines', ...)→ no-ophit('marker', ...)→ no-ophit('allow-file', ...)→ no-ophit('guard-lines', ...)→ no-ophit('table-grown', ...)→ no-ophit('table-shrunk', ...)→ no-opif (chars > MAX_CHANGED_CHARS)→if (false)hit('unreadable', f.path, 'not text')→ no-opNOT_RUN.test(to) ||→false ||'src/guard/**'taken out of the tool guard's globsstopped: this.autoMergeStopped()→stopped: nulllet policyOn = false→truethis.d.autoMerge?.() ?? false→?? truetrueforeignPush: ...→falseciFixRuns: ...→0pushLimitHit: ...→falseinstructionEvals: ...→[]weakening: ...→[]outsidePlan: plan ? ...→false ? ...claimedProtected: ...→falseopenedByHarness: ...→trueGuarantee coverage. Round 1's count (7 to 14 of 49) is corrected in Round 2 above: inputs.verdict-head-only was covered only for a PR with no merge-ins, and hold.eval's flow side drives only a task with no eval recorded.
Strengthened, but already counted as covered:
Partial, and not counted: merge.kill-switch-default-off (:56). F4 and F5 cover the coordinator's side: a reader that throws, or no reader, reads off. Not driven: the policy file's own default (
auto_mergemissing reads false, in src/instance.ts and src/policy-kill.ts). Label: inferred, from the imports.The scoreboard's own mutants, green on main before this PR, now turn it red: at this head N1 fails 6 cases, N2 9, N3 29 and N4 6 (N4 is the same edit as P0). Label: measured.
How it was verified
npm run typecheck: clean at this head (measured).npm run check:denylist: "1483 files clean" at this head (measured).test/docs-guardrails.test.ts: 9/9 pass (measured). Its file-cite check caught a baretest/adversarial/cite in round 1's first draft, which was fixed.test/merge-policy.test.ts: passes at this head, the weakening backstop included (measured: 71/71 together with docs-guardrails and engine-guard-locks). It scanstest/for check-shaped tables, and the new code adds none.Adversarial pass (round 1)
inputsloosen the gate, and so make a case or control pass more easily? No, by the schema: every field is a zod literal or enum of the hostile values only (policyOn: z.literal(false),patch_correct: z.literal(false),confidence: 'medium' | 'low', ...). There is no way to writetrueorhighthere. Label: inferred, from the schema; not run with a bad fixture, because proving it would mean planting a fixture in the repo (rule 8).inputs.plan, withinputs.weakening, and through the flow, under every config and scope. Label: measured, the suite is green and the controls assertauto: truewith no reasons.claudefirst on PATH that prints a refusal and exits 1. That refusal never appeared in any run's output (measured). isolate.js redirects every machine path (measured: the suite preloads it).flow-stop-file-unreadable's injection comparison failed (measured), because the reason named a different path each time. Fixed by the per-case dir.inputs.weakeningand flow cases do. Making it always-on would be truer to mergeDecisionFor, but it changes an existing control. Main'sweakeningOfsays the existingcontrol-test-change(test('totals')→test('totals add up')) waits:design: may weaken a check: removes or changes a line with an assertion or a test on it. Label: measured, by a scratch probe calling main's weakeningOf on that diff. So that control claims "merges", but the real flow holds it. I did not change it (rule 10). Proposed separately below.MUTANT-APPLIEDwith the expected file. Round 2 re-ran all of them at its head.Proposed separately (not done here; owner's call)
decide(), and fixcontrol-test-change. What: computeweakeningOffor every case, as mergeDecisionFor does. Why: the control currently asserts a merge that main's real flow would hold (measured above). The replacement: change the control to add a test rather than rename one, which is a change to an existing control (rule 10).What this can never do
Compromised component
test/adversarial/**is self-modification: any such PR waits for the owner, and the weakening gate also flags a removed case or todo line.Package size
Delta: 0 bytes. Every changed path is under test/ or docs/, and the package's
fileslist ships only dist/src, dist/web, examples/basic, templates and the notices. Label: measured,git diff --stat origin/main...HEADlists only test/adversarial/** and docs/guardrails.md; the 0-byte delta is inferred from thefileslist.Release notes
No fragment. This changes no install-visible behaviour: tests and docs only (changelog.d/README.md: "A PR that needs a release note adds its own file").
New dependencies
None.