Six cases for Filbert's C1 (f1 split into actor_role and actor_run), the earlier-merge dry run, a node_modules canary with s27, and the TOOLS.md usage line. 84 cases, 55 of 56 mutants killed (s26 equivalent), targeted gate green. Candidate is build.patch against 1223a0f37; not committed. Co-Authored-By: Claude Opus 5.5 <[email protected]>
10 KiB
Row 116 (#1610) round 2: a resolved merge decision lands as a gated local commit
Brief: docs/plans/2026-10-14_merge-path-for-approved-work.md (blob 7a610f379). Round 1's packet is agents/darkwing/work/queue-116/r1/ (82943bf61). Filbert's round 1 verdict was changes, comment 28464, queue rev 1165, for C1. Sage's sage-0207 asks for C1 as tests only and folds in D1. sage-0204 settles the base and tree question from round 1.
BASE: 1223a0f37, unchanged. No candidate path has changed on refactor from 1223a0f37 to a2ee3a80f. Over packages/bus, scripts/test-conductor.sh and the webui pinned-copy test, the only change is still 7eb5ad0cb's requirement count in packages/bus/tests/requirements.test.mjs. The candidate is build.patch against BASE, and candidate-manifest.sha256 lists the same 6 files. repro.sh exports BASE with git archive, applies build.patch and checks every manifest digest: DIGESTS_OK.
What changed in round 2
Two files changed. read.mjs, land-work.sh, fixture.mjs and CONDUCTOR.md are byte-identical to round 1, as Filbert ruled no change there; their digests in candidate-manifest.sha256 equal round 1's.
| File | Round 2 change |
|---|---|
| scripts/test-land-work.sh | six cases for C1, and a canary file in the fixture target's node_modules. 84 cases, was 78 |
| docs/TOOLS.md | D1: the usage line has --attribution, --business and --project, and the prose says what each does. The Maintenance row says 84 cases |
C1: one case per condition
Each case is a gen.mjs variant built through fixture.mjs like the rest. Each mutant is listed with the round 2 mutants below and was run against the full suite.
| Filbert | Condition in read.mjs | Case | Mutant | Mutant result |
|---|---|---|---|---|
| f1 | the answer's actor_role is null |
answer-role: the answer event has actor_role pm. Refuses not-human |
r24-answer-role | killed, only this case fails |
| f1 | the answer's actor_run is null |
answer-run: the answer event has actor_run rfixture. Refuses not-human |
r25-answer-run | killed, only this case fails |
| f2 | the answer's choice equals the close's | answer-choice: the answer says no, the close says yes. Refuses not-human |
r26-answer-choice | killed, only this case fails |
| f3 | exactly one answer | two-answers: the same answer event twice. Refuses not-human |
r27-one-answer | killed, only this case fails |
| f4 | raise.task_ref agrees with the work ref |
raise-task-disagrees: the raise cites vikunja:7/99, the ref is work/vikunja-7-42. Refuses work-ref |
r28-raise-task-ref | killed, only this case fails |
| f5 | later counts only later decisions |
earlier-merge: a merge decision E on the same ref, raised before D and withdrawn. A dry run of D exits 0 |
r29-later-only | killed, only this case fails |
I split f1 into two cases and two mutants because read.mjs checks actor_role and actor_run as separate conditions, and a mutant that drops one leaves the other in place. r27 changes answers.length === 1 to >= 1, the narrower form of round 1's r10, which also accepted no answer.
The earlier-merge case goes as far as Filbert asked. E is a merge decision with the same task_ref and question, and a withdrawn close. fixture.mjs writes it before D, so its seq is lower. The dry run passes every check and runs the gate, then writes no ref. With r29, later picks E up and the case refuses superseded.
The canary
Filbert's s26 ruling suggested it. The suite's target node_modules was an empty directory, so "the target's node_modules is still there" couldn't see its contents being deleted. Now it holds a file named canary, and the case also checks that the file is there. s27 is the mutant that shows the case can fail: cleanup does rm -rf "$WT/node_modules/", which follows the link and empties the target's directory. s26, which drops the explicit unlink, stays equivalent, as Filbert confirmed on git 2.55.0.
Tests
scripts/test-land-work.sh, 84 cases, all passing (log: test-land-work.log). The round 1 groups are unchanged except these:
| Group | Round 2 addition |
|---|---|
| The decision | answer-role, answer-run, answer-choice and two-answers refuse not-human, and raise-task-disagrees refuses work-ref. 25 variants, was 20 |
| A dry run | earlier-merge exits 0 with a withdrawn earlier merge decision on the same ref |
| Cleanup | the target's node_modules is still there, with its canary file |
Mutants
mutants.py has 56 mutants, the 49 from round 1 plus r24 to r29 and s27. It applies each to the file, runs the whole suite under a 600 second timeout in its own process group, and restores the file from .orig with a sha256 check. I ran the seven new ones first, then all 56 against the 84-case suite. mutants-run.log has both runs and each mutant's failing cases. The full run ended restored: True, and 55 of 56 are killed. s26 survives, and Filbert ruled it equivalent in comment 28464. Four round 1 mutants now fail one more case than they did, each a new case: r10 also fails two-answers, r14 raise-task-disagrees, and r20 and s16 earlier-merge.
| Mutant | Change | Result |
|---|---|---|
| r01-schema-digest | skip the schema digest compare | killed, 1 of 84 fail |
| r02-schema-meta | skip the meta version and digest check | killed, 1 of 84 fail |
| r03-read-only | open read-write | killed, 2 of 84 fail |
| r04-business-scope | drop business = ? from every query |
killed, 1 of 84 fail |
| r05-one-raise | accept zero or several raises | killed, 1 of 84 fail |
| r06-not-a-merge | skip the action check | killed, 1 of 84 fail |
| r07-resolved-op | accept any single close op | killed, 1 of 84 fail |
| r08-human-by | skip resolved-by == human | killed, 1 of 84 fail |
| r09-human-via | skip via == cli | killed, 1 of 84 fail |
| r10-human-answer | accept a missing human answer | killed, 2 of 84 fail |
| r11-merge-option | skip choice == approvalChoice | killed, 1 of 84 fail |
| r12-one-work-ref | accept several work refs | killed, 1 of 84 fail |
| r13-work-ref-shape | loosen the work ref pattern | killed, 1 of 84 fail |
| r14-task-ref-agrees | skip the task_ref agreement | killed, 2 of 84 fail |
| r15-target | skip the target check | killed, 1 of 84 fail |
| r16-later-supersedes | ignore supersedes |
killed, 1 of 84 fail |
| r17-later-task | ignore a later decision on the task | killed, 1 of 84 fail |
| r18-later-question | ignore a later question naming the ref | killed, 1 of 84 fail |
| r19-no-verdict | skip the no-verdict refusal | killed, 1 of 84 fail |
| r20-latest-verdict | take the earliest verdict | killed, 31 of 84 fail |
| r21-approve | skip verdict == approve | killed, 1 of 84 fail |
| r22-verdict-task | skip the verdict body's task_ref | killed, 1 of 84 fail |
| r23-full-oid | accept a short commit | killed, 1 of 84 fail |
| r24-answer-role | skip the answer's actor_role check | killed, 1 of 84 fail |
| r25-answer-run | skip the answer's actor_run check | killed, 1 of 84 fail |
| r26-answer-choice | skip the answer's choice == the close's | killed, 1 of 84 fail |
| r27-one-answer | accept several answers | killed, 1 of 84 fail |
| r28-raise-task-ref | drop the raise's task_ref from the agreement | killed, 1 of 84 fail |
| r29-later-only | later counts earlier decisions too |
killed, 1 of 84 fail |
| s01-base-early | skip the early --base check |
killed, 1 of 84 fail |
| s02-ref-moved | skip ref-moved | killed, 1 of 84 fail |
| s03-merge-base | ignore a missing merge base | killed, 1 of 84 fail |
| s04-empty-diff | accept an empty diff | killed, 1 of 84 fail |
| s05-root-top-file | allow a top-level file | killed, 1 of 84 fail |
| s06-root-new-dir | allow a new top-level directory | killed, 1 of 84 fail |
| s07-root-src-v1 | allow src/ and v1/ |
killed, 1 of 84 fail |
| s08-dirty-changed | skip the changed-path check | killed, 3 of 84 fail |
| s09-dirty-staged | skip the staged check | killed, 1 of 84 fail |
| s10-dirty-untracked | skip the untracked check | killed, 1 of 84 fail |
| s11-dirty-recheck | skip the recheck before the commit | killed, 2 of 84 fail |
| s12-apply-conflict | ignore an apply failure | killed, 1 of 84 fail |
| s13-patch-drift | skip the patch-id compare | killed, 1 of 84 fail |
| s14-gate-node-modules | skip the node_modules check | killed, 1 of 84 fail |
| s15-gate-step-fails | ignore a failing gate step | killed, 5 of 84 fail |
| s16-gate-skips-queue | run the queue suite too | killed, 16 of 84 fail |
| s17-gate-scripts | run no scripts/test-*.sh | killed, 8 of 84 fail |
| s18-base-moved-cas | update-ref without the old value | killed, 2 of 84 fail |
| s19-dry-run | ignore --dry-run |
killed, 3 of 84 fail |
| s20-checkout-sync | skip the checkout sync | killed, 1 of 84 fail |
| s21-subject | a fixed subject | killed, 1 of 84 fail |
| s22-attribution | drop the attribution line | killed, 2 of 84 fail |
| s23-cleanup-worktree | keep the worktree | killed, 6 of 84 fail |
| s24-cleanup-network | keep the Docker network | killed, 1 of 84 fail |
| s25-cleanup-scratch | keep the scratch directory | killed, 6 of 84 fail |
| s26-cleanup-link | skip the explicit node_modules unlink | survived, equivalent (Filbert, comment 28464) |
| s27-cleanup-follows-link | cleanup follows the link and empties the target's node_modules | killed, 1 of 84 fail |
Gate
A targeted builder gate under #271 (gate.sh, logs in gate/), the same steps as round 1. Round 1 ran the webui pinned-copy test by hand beside gate.sh; this round gate.sh runs it as its fourth step. Sage's landing gate stays full.
| Step | Result |
|---|---|
packages/bus node --test tests/*.test.mjs |
156 pass, 0 fail |
| scripts/test-land-work.sh | 84 passed, 0 failed |
| scripts/test-conductor.sh | 17 passed, 0 failed |
packages/webui node --test tests/pinned-copy.test.mjs |
10 pass, 0 fail |
GATE_PASSED (gate-run.log). No step failed, so the flake rule had nothing to count. The suite also passed 84 of 84 on its own before the mutants (test-land-work.log), and s26, the one survivor, passed all 84 in the full mutant run.
Replay
Round 1's replay stands, and I didn't rerun it. It ran read.mjs and land-work.sh, which are byte-identical in this candidate, against scratch copies of the live rows. Filbert checked it under sage-0204: at 1c9a4910d the landed diff is f7c8537b3's own diff and the dry run wrote no ref, and at 4bc908991 the tree equals f7c8537b3^{tree}. The commit stayed local in both. The evidence is in agents/darkwing/work/queue-116/r1/ (build.md's Replay section, replay-1c9a/, replay-4bc9/).
Going live
Nothing goes live. The tool is a conductor command run by hand. No service, role or session calls it. Push, the merge to next or main, re-pins and restarts stay out of this row.