Skip to content

[Tracking] guarded-write chain (epic 1375): rules, port sources, chain-level rows #1991

Open

Description

@easonLiangWorldedtech

Part of the split-plan bookkeeping for the guarded-write (epic 1375, units U1 to U9) chain.

What belongs here

  • chain-level rules and design decisions that no single unit owns
  • port sources and the per-unit derivation notes for content that is ported rather than rebased
  • chain-level review rows that CodeRabbit raises against a unit but whose cause spans units
  • unit-scope notes for units that have no per-unit issue yet must name their unit in the first line

What does not belong here

  • unit-scope defects: those go on that unit's own issue (all nine units now have one - see the unit map below)
  • anything that requires a maintainer decision: raise it in the PR thread instead

Writing rules for this issue

  • no @mention in any comment here: mentions notify people who are not subscribed to this issue
  • do not reference a fork-repo issue number here; a bare number resolves against this repository and silently points at something unrelated
  • when recording an upstream pull request, write it without a leading hash (for example upstream PR 1066)
  • prefer editing an existing comment over posting a new one: edits notify nobody and recompute cross-references
  • this issue's history was consolidated into this body on 2026-10-13; add new chain records as edits to the matching section below rather than as new comments

Unit tracking issues (fws chain)

One per unit, so every PR now has a linked issue in its sidebar: U1 PR 1910 -> #2016, U2 PR 1911 -> #2017, U3 PR 1912 -> #2018, U4 PR 1913 -> #2019, U5 PR 1914 -> #2020, U6 PR 1915 -> #2021, U7 PR 1918 -> #2022, U8 PR 1916 -> #2023, U9 PR 1917 -> #2024. All nine PR descriptions were rewritten for human reviewers following maintainer feedback on PR 1910: each leads with what the PR does and why in plain terms, links its unit tracking issue with a Closes line (U9 additionally closes the epic as the last unit), and keeps the measured verification numbers under Test Procedure / Verification at this head.

Decision requests

One canonical write lock, or an accepted residual window

Two chain registrations converge on the same end state and neither can be closed inside a single unit. The runtime has no atomic compare-and-swap rename, so a pre-rename verification callback followed by a plain rename cannot be made sufficient by any unit, and a symlink refusal that walks ancestors once cannot be made sufficient while publication is path based. The only shapes that make both claims true are a single lock that every writer in the repository honours, including writers outside the extension host, or an explicit, documented residual window on both controls. A unit-level change cannot choose between those, and a registered row stays red at every head until one of them is chosen, because a citation does not clear a pre-merge row - only code at the head does. Until the choice is made, the units that wire these controls cannot reach an approving review at their own head.

Chain-level defect classes

1. A rollback that persists unapproved streamed content (defect class, not one call site)

A rollback path awaits workspace.applyEdit() or TextDocument.save(), discards the boolean, and continues as if the operation succeeded. When the editor refuses the restore, the buffer still holds content the user never approved and the following save writes it to an existing file, while the caller reports a completed rollback.

Units affected: U8 (PR 1916) in this chain, and the same primitive as consumed by PR 1921, PR 1929 and PR 1932 in the other two chains.

Two facts that are different and must not be merged into one attribution: the shared primitive is DiffViewProvider.restorePreStreamBuffer() / saveBufferClean(), whose primitive owner is PR 1930 (commit e41ebe0), with port direction 1930 -> 1916 / 1921 / 1929 / 1932; the semantic baseline is PR 1928 discardUnapprovedStream() and its editorFailure channel, i.e. a false return from applyEdit() means the rollback failed. The baseline was already broadcast, so it is not a new rule.

Acceptance for any unit that carries the row: restorePreStreamBuffer() throws when applyEdit() returns false; document.save() and the tab close are not called; the failure reaches the layer that reports it (revertChanges -> revertDiffChangesBeforeReset -> rollbackError); a test asserts the user does not receive a "successful rollback". A save that resolves false must propagate. For PR 1932 additionally: the discardFileTab() shape (restore and save hoisted above the tab loop) must stay intact, and prettier whitespace changes do not affect it. Fix it in the callee so every caller inherits the contract, rather than adding another inline check at each call site - this is the same lesson as the rollback fix in 9c54765: fixing one call site does not fix the class.

2. A confined publication must pin the directories the write itself created

The caller can only record the identity of directories that already exist, and a recursive mkdir reports nothing it made, so the pre-commit re-check walked a list that omitted exactly the components that appeared during the write. A local process that replaces one of those with a link to outside the scope sends the commit somewhere the confinement decision never made, while every recorded identity still matches.

Fixed on U6 (PR 1915) in commit 3289a72: after the mkdir the primitive records the identity of each directory it created and the re-check walks the recorded list plus those; the stat-with-mapping is shared by both call sites so no second branch set appears.

Inherited by any unit whose branch carries this version of the primitive - U1 and U8 among them. Ports are owed, not copied: each unit's call shape differs, so a port needs its own red-first test and its own negative controls. Carry a port when that unit is pushed for some other legitimate reason; do not push a unit solely to deliver it.

Acceptance for a port: (1) a confined write whose parent chain is created by that write, with one component swapped for an out-of-scope link after the mkdir, fails with the ancestor-replaced error and renames nothing; (2) the same shape without the swap publishes normally; (3) a swap of a pre-existing ancestor still fails. Negative controls, one per half of the fix: skipping the recording turns test 1 red, and recording without feeding the list to the re-check also turns it red.

3. Fail-closed DACL restore must reach the old-generation copies of the publish primitive

In the old-generation copies of src/services/file-safety/safeWriteText.ts the DACL restore runs after the commit rename and, on failure, only records a flag and warns, so a replacement is published even though the captured DACL could not be restored. The caller gets no signal: the primitive returns success and the only post-commit throw is PublishNotDurableError. A call-site preflight would be a second mechanism duplicating the primitive's own capability logic, and requesting a backup does not fail closed because the replacement is still published.

Ownership: the chain already carries the correct shape. The earliest unit in merge order implements a typed DaclRestoreError thrown after the commit rename when the restore fails, plus a fail-closed inspection error before the commit, and its own pre-merge row passes with that shape. The old-generation branches carry independently re-derived copies that predate it, so the fix is a convergence port of one shape, not a new design.

Acceptance, not bound to a commit: (1) a restore failure after the commit rename reaches the caller as a typed error, and no caller can observe success for a publish whose DACL was not restored; (2) the branch converges on the chain's single shape rather than adding a second mechanism in the caller; (3) the direct-save path surfaces the same failure instead of swallowing it; (4) backup retention on restore failure does not regress, and capture stays fail-closed before the commit; (5) every branch carrying its own copy of the block gets the port re-derived for that branch, not copied byte for byte. Negative-control shape: one mutant per production call site, each reddening only the tests naming that behaviour; run each mutant against the pre-fix spec to show the branch was previously uncovered; if a mutant reddens far more tests than the target, assume the mutant is wrong first; restore mutants byte for byte and verify with a hash.

4. Publish must not follow a swapped ancestor link when a caller asks for symlink refusal

The refusal walks the ancestors once, before anything is resolved, staged or locked, while the two later checks look only at the final component and publication is path based - the directory creation and the rename both take a path. An ancestor replaced by a link after the walk therefore redirects the commit, so the control the caller asked for is not what the caller gets. The runtime has no renameat equivalent, so this is a design decision in the primitive rather than a local patch: either publish through handles with no-follow semantics, or stop claiming the protection for paths that cannot be proven stable after the check.

Ownership correction measured at the earliest unit head: the primitive in the earliest unit of the declared merge order has no refusal option, no confinement option, no ancestor walk and no external callers, so the claim this registration narrows is vacuously true there and no change at that head can produce a test that is red first. The earliest unit that actually carries the control is the one whose helper resolves the caller path against a declared scope root and walks ancestors once before anything is staged or locked, and that is where the cheap half belongs - document precisely what the control does not cover, add a test that swaps an ancestor after the final component check and is red before the change, and decide whether the credential path keeps the claim. Handle-relative no-follow publication is not available in this runtime, so the narrowing is the only implementable half.

Carrier correction measured branch by branch: the confinement control is not new to the unit that first documented the window. The unit before it in the declared merge order already carries the identical control - the same scope-root resolution with the missing-ancestor walk, the same pre-lock and in-lock checks in the same order - and the same credential call sites that pass it. The two earliest units carry no such control at all. The documentation narrowing therefore has to be ported to that earlier unit and re-derived: where that copy still decides from a truthiness test, keep the boundary wording and drop the sentence about a declared but empty root, which only exists in the later copy.

Acceptance, not bound to a commit: (1) either publication is handle relative and no-follow for the directory creation and the rename, or the control is removed from the credential path so it no longer claims protection it cannot prove; (2) the chosen boundary is documented on the option, including what it does not cover; (3) a test swaps an ancestor after the final component check and shows the outcome the option now promises, red before the change; (4) the shape lands in the earliest unit that carries the control and is re-derived per branch when ported forward; (5) callers that pass the option keep their existing behaviour for ancestors that do not change under them. Negative-control shape: one mutant per production call site, each reddening only the test naming that behaviour, plus a run of the new test against the pre-fix production file; restore byte for byte and verify with a hash.

5. The symlinked-ancestor walk is not bounded by the authorized scope root (owed on U7, PR 1405)

A symlink above the authorized root - a linked workspace root, a linked temporary directory on a developer machine or a CI agent profile - refuses writes that the caller opted into for a reason. The refusal exists to stop a link redirecting a payload out of the scope; applied above the authorized root it stops the write without protecting anything, because nothing above that root is part of the containment decision.

Acceptance: (1) a write whose ancestor chain contains a symlink strictly above the authorized scope root publishes normally, with the payload at the named path; (2) a symlink strictly inside the authorized scope still refuses, with the same error the refusal gives today; (3) the credential-carrying case keeps its current behaviour: a link at the final component is an error, and a link inside the scope is an error; (4) the walk stops at the scope root rather than at the filesystem root, and the code says which root it stopped at.

Negative control: red first - the test for case 1 must fail against the current walk; then green with the bound in place; then red again with the load-bearing piece removed - drop the lower bound so the walk runs to the filesystem root again, and case 1 must fail. A second control for the other half: widen the bound so it also ignores links inside the scope, and case 2 must fail. Both mutants restored byte-exact.

Notes for whoever takes it: heavy lift, deliberately not bundled with the comment and documentation changes already landed on that unit. Not tied to any commit: carry it on the unit that legitimately pushes this primitive next, re-derived against that unit's call shape rather than copied. Related chain facts: the lock key and the publication destination are different paths, and the fold of a lock key has to match the way the lock entry is named (see the port-source record below).

6. A shared fixed staging-directory name lets a workspace-shipped symlink redirect the staging chmod and file

A repository that ships a directory entry named .file-safety-staging as a symlink to any directory the user owns has that outside directory set to 0o700 by the first confined write into the containing directory, and the staged temp file is created in the outside directory before the commit rename.

The chain's single shape is a per-write unique staging-directory name, which removes the hazard by construction: the entry the write creates, chmods and writes into is one it made. Every unit branch of the declared merge order (U1 through U9) already carries that shape.

Carriers of the defect: the old-generation copies on PR 1395 and PR 1405 still use the shared fixed name. PR 1408 carried the convergence port in commit 87ee84e (raised as inline finding 4240865298 against its head); this records the port still owed on the two earlier old-generation branches.

Acceptance for a port: (1) two self-staged writes into one directory receive different staging names, each of the generated shape, and each write's chmod and staged file land inside its own directory; (2) a pre-existing entry named .file-safety-staging - plain directory or symlink - is never chmod-ed or written into by this layer; (3) the test is red before the change on that branch. Negative-control shape: one mutant - revert the staging-directory name to the shared fixed name and confirm exactly the new ownership test reddens and nothing else; restore byte for byte and verify with a hash.

7. Empty declared root: reject semantics are canonical

Correction after two units fixed this independently: the two copies diverged. The unit that merges earlier in the declared order rejects an empty or whitespace-only declared root with the confinement error naming that root, before the lock key, the lock, directory creation or staging, because the helper's stance is that a failure must surface rather than be decided from a guess about the caller. The later copy currently resolves an empty string to the process working directory instead. The canonical shape is therefore the rejecting one, and the later copy must be re-derived to it when the earlier one lands, since merging the later copy as it stands would silently replace reject semantics with working-directory semantics in the landed default. Until then the later unit stays as it is rather than spending another review cycle, because it cannot merge before the earlier one anyway.

8. U9 teardown: the waiter cannot tell whether the pass it waited for succeeded (PR 1917 / PR 1918 - fix once and port)

runTeardown cannot tell a waiter whether the pass it waited for actually succeeded. The tracked promise is resolved by release() in the finally block, so it settles successfully whether the owner's cleanup or finalize returned or threw. The waiter's await this.teardownInFlight therefore resolves in both cases and it returns false - "not my pass" - with no signal that the pass it just waited through left the document mid-teardown. The owner's own caller does see the throw, so the failure is not swallowed outright; what is lost is the waiter's ability to react, which is exactly the case a cancellation lands in: the second caller arrives, waits, and walks away believing the buffer was reverted and the tabs closed.

The two structural halves of the original note are already in place and should not be re-derived: finalize now runs inside the tracked promise, so a cancellation arriving after the callback but before the restore and reset no longer starts a second full pass, and the gate is held until both settle.

Acceptance: a test where the owner's cleanup rejects and the waiter is already parked: the waiter must either see the failure or run the recovery itself - asserting only that the owner rejects is not enough, because that half already holds. Negative control: make the waiter's path indistinguishable again (resolve the gate unconditionally and drop whatever channel carries the outcome) and that test must fail.

Owed changes and ports

Stop accepting a bare staging path (U6, PR 1915)

The text writer still accepts a caller-supplied staging path by prefix alone, for compatibility, and the wording says it is checked rather than trusted. Verified at the head of U6: no production caller passes a bare path - the JSON writer takes the handle from the staging-file API and hands the handle down. So the compatibility acceptance has no user, and it keeps a bypass in the primitive: a prefix check on a path a caller invented is not the opaque capability the design relies on, because the caller can name a path it did not create.

Acceptance: (1) a bare path is refused with an error that names the handle API; (2) the handle path still works unchanged; (3) three assertions that the handle cannot be forged by naming a path - a path this call did not create, a link at the staging location, and the target itself. Negative control: restore the prefix acceptance and drop the handle identity check, and the unforgeability assertions go red.

Port source: folding a lock key the way the lock is placed (owed on U8, PR 1916)

Source: U6 (PR 1915) commit 5131bc0. Derived into U8 (PR 1916) commit 19aec00 - not byte-identical, because U8 computed the same-identity comparison inline while U6 has it as a helper.

The rule, which is what a port must reproduce rather than the code shape: the advisory lock is the entry <path>.lock beside the file, so a key must be folded the way that entry is named. On Windows the filesystem folds two spellings of one directory entry twice over - case anywhere, and short (8.3) names inside a component, which is how a CI agent hands out its runner profile directory - while a string comparison folds only case. So the fold canonicalises the deepest EXISTING ancestor and appends the segments below it, case-folded on Windows, and the walk starts at the parent, never at the file: resolving the final component through a symlink folds a link together with its referent, and those are two different lock entries.

Owed on U8: the mixed-ancestor acceptance test (an existing ancestor spelled short with a tail that does not exist yet) has not been derived there, because in U8 the same-identity comparison is only consulted when publishing over a link. The test must be re-derived against that shape, not copied. Verification limit recorded on the U8 commit: the four windows failures that motivated the port do not reproduce on a host whose temp directory has no short-name ancestor, so only CI exercises them.

Checkpoint-queue depth has no cap, and no caller in PR 1404 can observe one (PR 1404, per-write-checkpoints B1)

Raised on PR 1404 at head e406c09 (threads 607pkQ on src/core/task/Task.ts and 607p-r on src/core/checkpoints/index.ts) as a size bound on enqueueCheckpointWork. Not implemented, and the reason is a measurement rather than a preference: enqueueCheckpointWork (index.ts:251-282) is entered only from checkpointSave and checkpointRestore, and neither caller reads the queue's length. checkpointSave gates the start of each queued operation on its predecessor's settlement plus its own expiresAt and isCanceled (:302-311); checkpointRestore is passed no options at all, because a queued restore must never be dropped. A depth cap added in this unit would drop work that no caller in this unit can see being dropped, and its only observable effect would be a save returning the same no-commit result an expired request already returns.

Owed on: whoever owns the queue's depth contract. Queue depth becomes observable in unit 2 of the 1408 split (refs/tmp/s3-1408-u2-guardedwrite), where a caller can count queued entries, so the cap, its unit of measure (entries or bytes) and the drop policy for a bounded queue (fail, drop-oldest, or reject the caller) belong there and port back rather than being guessed where nothing can observe them. Size is not the reason for this deferral: the reconciliation push on that branch measured 325 of the 500 counted lines against the merge base, and the cost of a cap that does not exist yet was deliberately not quoted.

  • 1395 owes one test-only row out of its own review body (Regression Evidence), named here so it survives the reviewer. No test injects a non-committed publish rejection into saveDirectly. Inside describe("saveDirectly method") in src/integrations/editor/__tests__/DiffViewProvider.spec.ts at head 39d4b95128d3485ff5b9f8eac37f7432da490b11, the only injected safeWriteText rejections are DaclRestoreError at :886 and PublishNotDurableError at :906, both asserted as warnings; the rejection tests at :798 and :823 fail the guards instead, and :830 asserts that the publish never ran. Cost: 0 counted executable lines, because packageForPath in scripts/stryker-diff.mjs excludes __tests__/, so the row cannot move 1395's measured width of 359 counted lines of the 500-line cap. Condition: it lands in the push that follows @edelauna's answer on 1395's three open rows (4510971756, 4510971765, 4510971873), not in a push made to satisfy a review-body item. Owner: whoever holds 1395 when that answer arrives. Recorded 2026-11-09 by s3-1395, with the disposition comment 6746153431 on the PR.
  • 1395 owes the five named rows from fix(utils): key the advisory file lock by identity, not by path spelling #2368's lock-identity work, accepted by name and held, not scheduled. Names quoted as they read at 2368 head d76bbfd4f9f7a81cf517a9727c7cbc3bf5926677 (ref vps2f-filelock-identity-key, blob bd54acfe41fc of src/utils/__tests__/fileLock.test.ts, 688 line-endings / 33,378 bytes / 21 rows): contends an unrelated sibling named like the link target against the link's lock; keeps a waiting writer at the caller-named artefact across a leaf-link replacement and merges into the referent that answers when it proceeds; refuses a second key on a held artefact and a late release from the writer that let go; keeps identities whose digits concatenate alike apart; falls back to the caller's own path when no ancestor has an identity. Package sent by u5a-2359 on 2026-11-09 with nothing written into 1395's tree. Why held rather than scheduled: the primitive does not exist on this branch, and 1395 measured that rather than taking the package's word. At 1395's head 39d4b95128, src/utils/fileLock.ts (blob d8bdbf910b30, 121 line-endings / 4,985 bytes) contains no statIdentity, no canonicalLockKey, no lockArtefactPath, no canonicalLeafSpelling, no nearestExistingDirectory and no lockfilePath; it calls lockfile.lock(absoluteFilePath, { at line 48 with realpath: false at line 51; and src/utils/lockLeafSpelling.ts is absent from that tree. Under that implementation the library derives <file>.lock beside the file, so the first three names assert a caller-named-artefact contract this unit cannot exhibit, and the last two need an identity key that does not exist here. The port is therefore blocked on the same decision as 1395's open row 4510971873 and 1405's escalated row 4504216100 - one ruling on the lock-key identity contract, seen from three branches - and not on a push window. Target file chosen by this lane now so it is not re-litigated later: src/utils/__tests__/fileLock.test.ts (blob 92637fe7a0df, 34 line-endings / 1,088 bytes / 2 rows, the file that exercises acquireFileLock and withFileLock, rather than the 2,398-line-endings writer spec), and the package's own gating note travels with it: rows 1-3 are POSIX-gated and run as skips on Windows, so a Windows-only green says nothing about them. Cost 0 counted executable lines whenever it lands, because the destination sits under __tests__/. Two corrections to the version of this row posted earlier today. The earlier row measured the destination at the wrong commit: it quoted 2,399 lines, which is src/services/file-safety/__tests__/safeWriteText.spec.ts blob 5b07bffb1181 at 1395's current head 39d4b95128 (110,036 bytes, 2,398 line-endings), while the package was measured against 89384b314acd, where the same path is blob 86a53b388acb at 99,844 bytes and 2,190 line-endings, or 2,191 counted as split-lines. So the 2,191-against-689 figure is one counting convention applied to two files, not a disagreement between lanes. The earlier row also named the destination without its __tests__/ directory. Recorded by s3-1395; nothing pushed and 1395's head is unchanged and clean at 39d4b95128.
  • 1405 owes five accepted test-only rows out of its own review, named so they land rather than drift. Rows 4509757197 (POSIX 0o664/umask 022 and new-target/umask 077 published-permission assertions at src/utils/__tests__/safeWriteJson.test.ts:1081-1104, gated to non-win32 with the mask restored in finally), 4509757204 (Windows junction cleanup by fs.rmdir with mock removal in finally), 4509757233 (the error-stream mocks must close the stagedFd they are handed; stagedFd occurs 0 times in that 1,626-line test file while the writer creates it at safeWriteJson.ts:487), 4510030249 (real-writer export test through the public entry point, platform-gated for symlink privilege) and 4510030255 (catalog-loaded test asserting the rendered common:errors.settings_export_failed notification, the key already used at src/core/config/importExport.ts:360). Cost: 0 counted executable lines each, because packageForPath in scripts/stryker-diff.mjs excludes __tests__/, so none of them can move 1405's measured 497-of-500 width. Condition: they land in 1405's next push window. Answered in-thread 2026-11-09 17:32:58Z-17:33:23Z (replies 4515845286, 4515845747, 4515847610, 4515847974, 4515848354); no thread resolved and nothing pushed. Recorded by s3-1395.
  • 1405 owes four deferred production rows and one escalated design row, each priced against its own headroom. Deferred to 1405's follow-up unit on the S4a chain (unit tracking issue Tracking S4a: guarded write CAS core with per-path FIFO chain #1399), because 1405 measures 497 counted executable lines of the 500-line cap at head 8186900bfc - three counted lines of headroom, no test file charged - and each row needs a production edit beside its test: 4509757215 (the catch cleanup at safeWriteJson.ts:411-436 unlinks newFileToCleanupWithinCatch without recording whether this writer created the stage, so an EEXIST collision deletes another writer's file), 4509757220 (_flushFileToDisk at :531-542 reopens with r+ and only logs on failure, so a read-only stage is published unsynchronised; synchronise the descriptor from :487 instead), 4509757226 (fchmodSync failure is swallowed at :493-497, so a 0o664 target can publish 0o644 under umask 022), and 4510030261 (showErrorMessage is awaited inside the export catch at importExport.ts:360, so a rejecting notification escapes exportSettings). Escalated rather than answered: 4504216100 (Major) asks that writers stay serialized on the caller-named publication path across a symlink replacement - the same lock-key identity question 1395 has open as row 4510971873 and that fix(utils): key the advisory file lock by identity, not by path spelling #2368's identity-key rows are being written now - so it is put to @edelauna naming blobs f9073dcd64e9 (safeWriteJson.ts) and 49807ea154d4 (fileLock.ts) at 8186900bfc against 1395's 571d345098c3 and d8bdbf910b30 at 39d4b95128. Two further rows on that PR were closed as already delivered at the head the reviewer pinned, measured line by line: 4504216094 and 4509757207. Answers posted in-thread 2026-11-09 17:32:58Z-17:33:23Z; nothing resolved, nothing pushed. Recorded by s3-1395.

Ownership rulings

The safeWriteText primitive has one owner chain (registered for U4, PR 1913)

Three pre-merge rows raised on PR 1913 at head c36fd64 land on the shared primitive src/services/file-safety/safeWriteText.ts: (1) Security Boundaries - a symlink race requires confineTo to be carried into safeWriteText and enforced at the final publish action; (2) Persistence Integrity - the committed semantics at safeWriteText.ts:509-511; (3) Lifecycle Resource Cleanup - the swallowed backup unlink at safeWriteText.ts:553-558.

Ruling: these are primitive-level, not U4-level. The primitive already has one fixed defect in this chain (the pin-before-commit escape, fixed in U6, with the port owed to U1 and U8 and re-derived per unit rather than copied byte for byte), so a second unit editing the same block independently is exactly the double fix the chain rule forbids. The primitive fix belongs to the earliest unit in the declared merge order that can carry it, and ports forward from there. U4 does not fix these three rows; it references this ruling and resolves its threads.

Acceptance for whoever takes the primitive fix: symlink race - swap an ancestor for an out-of-scope link between the last check and the publish action -> AncestorReplacedError and no rename; no swap -> publishes. Committed semantics - a failure after the commit point must not be reported as a rollback, and must not leave the caller believing the previous content is back. Swallowed backup unlink - ENOENT and ENOTEMPTY stay silent, every other error is surfaced, and the loop continues. Negative control shape per row: mutate the exact production line, expect exactly the tests that name that behaviour to go red, and run the same mutant against the pre-fix spec to prove the branch was uncovered. A mutant that reddens far more tests than the target means the mutant itself is wrong.

Note for the reader: an open-thread count on a PR and the number of rows in that PR's pre-merge table are two different measurements and must not be used interchangeably.

Registrations that are not defects (U3, PR 1912)

1. The Linked Issues check reports an error because the branch is based directly on the repository default branch, not on a preceding unit. CodeRabbit therefore sees the whole stack (13 files, roughly 3.6k added lines, 39 commits) instead of one unit's delta, and its linked-issues validation reports an error for a pull request whose diff spans requirements owned by other units. Handling chosen: the pull request body carries a requirement-to-unit table so a reviewer can tell which requirement this unit serves. Acceptance: when the earlier units of this chain merge, the row disappears without any code change - if it does not, the cause is something else and this record is wrong.

2. A review finding overstated the effect of a cleanup on a committed publication. The finding said the cleanup path can delete the already-committed target. On the POSIX path it cannot: the staged name is gone once the rename lands, so the unlink reaches a name that no longer exists and the only observable effect was a false leftover warning. The shape criticism was still correct - a cleanup that reaches for a path it may not own needs its ownership stated, not inferred - so the guard stayed. Acceptance: none required. Recorded so the next reviewer of a sibling unit does not re-derive the overstated consequence, and so a future row about the same shape is judged on the shape, not on the effect that turned out not to exist.

Closed before registration (verified at head, do not re-derive)

  • ApplyPatchTool refreshing a stale observation: the hunk read now observes only when there is no prior entry or the prior version equals the token just read, which is the same stale policy ApplyDiffTool applies, and the comment at the site states why an older observation must not be refreshed.
  • PostCommitDurabilityError leaving the backup behind: the delete path catches it and unlinks error.backupPath before rethrowing.

Both verified by reading the two files at the head of the U9 branch on 2026-10-10, not from the queue entry.

Split plan as ruled, and the priced cut: U5 remainder (PR 2357, branch vps2f-1914-u5b)

Recorded 2026-11-09 by lane u5a-2359. The full priced record is comment 6751466344 (posted 21:06:35Z, patched in place at 21:07:37Z to replace a placeholder digest, which the patched text names as superseded); this section carries the cut as ruled, the per-unit prices, the merge order, the preconditions and the verify exits, because those are queue facts and the queue lives here.

Pricing basis (the same for every number below)

Base 970b3a47ba69a9dfe9e9e5095bda75be3a2d739b — the main that contains PR 2385, verified rather than assumed: merge-base e487cd7b2a 970b3a47b answers e487cd7b2a (main is an ancestor of 2385's head) and merge-tree --write-tree main 2385head writes 311cfce72578b74c40a7e8b73ce745cf52387ada, equal to 2385's own tree with zero conflict entries. Content head d6fa7117e07b70455da24e4722b47eb0b202fa6e.

Charged by selectFromGit from scripts/stryker-diff.mjs, blob 7dfe0006d2d5c7e4d2282f65f68b8ed5f16866bc, 30,108 B / 869 line-endings, one distinct blob in every run; caps MAX_CHANGED_LINES = 500 (line 11) and MAX_MUTANTS = 400 (line 12). Interpreter node v22.23.4 (C:/work/Zoo-Code-fork/.tmp-node22/node_modules/node/bin/node.exe). Every priced head is a single-parent git commit-tree off the base holding the unit's content, so the first-parent override in resolvePullRequestBase (lines 287-291) never fires and no figure below is main's contribution; parent counts were re-read with git rev-list --parents and cat-file -p, not from a helper field (the helper's own field read 0 in the first twelve runs because of a mis-escaped whitespace regex — instrument fault, discarded in favour of git). In each run the tree was moved to the commit being priced and git rev-parse HEAD printed beside the asked sha; all runs matched. Priced in worktree wt-r302-2357-cut-price at pnpm-lock.yaml blob 2035d1d167a15ad0ea1972bd1a974abdace092fb (612,370 B) with node_modules junctions into wt-r297-2385-head-install's store (typescript 5.9.3, vitest 4.1.12) — a junction tree, not a fresh frozen install, named as such.

The cut: eight units, whole files only, each priced standalone against the base above

unit files counted lines / selectors a+d (code + spec) zdt split verify exit
U5c-1 src/utils/fileLock.ts, __tests__/fileLock.spec.ts, __tests__/fileLock.canonicalKey.spec.ts 62 of 500 / 21 of 400 592 (217 + 375) 0 — SOFT-OVERSHOOT, rationale required in the PR body
U5c-2 src/utils/jsonFileRecovery.ts, __tests__/jsonFileRecovery.test.ts 26 / 3 176 (47 + 129) 0 — UNDER-SOFT
U5c-3 src/utils/safeWriteJson.ts, __tests__/safeWriteJson.interruption.test.ts, __tests__/safeWriteJson.lockKey.spec.ts 131 / 46 614 (366 + 248) 0 — SOFT-OVERSHOOT
U5c-4 src/utils/__tests__/safeWriteJson.test.ts 0 / 0 517 (0 + 517) 0 — SOFT-OVERSHOOT
U5c-5 src/integrations/misc/indentation-reader.ts, src/core/tools/ReadFileTool.ts + their 2 specs 51 / 31 227 (136 + 91) 0 — UNDER-SOFT
U5c-6 src/core/tools/__tests__/readFileTool.spec.ts + the src/eslint-suppressions.json row 0 / 0 1,140 (0 + 1,138 + 2 config) 1 — HARD-OVERSHOOT, must split
U5c-7 src/core/task/observationRegistry.ts, src/core/task/Task.ts + their 3 specs 25 / 15 384 (117 + 267) 0 — UNDER-SOFT
U5c-8 src/services/mcp/McpHub.ts, src/services/mcp/__tests__/McpHub.spec.ts 27 / 14 211 (68 + 143) 0 — UNDER-SOFT

The eight units add up to exactly the whole remainder: 322 counted executable lines of 500, 130 selectors of 400, 3,861 a+d (951 code + 2,908 spec + 2 config), and the whole remainder priced as one tree (be00e75c1463cf6194215400c4ff12e5ff5c152e, single parent off the base) prints 322 / 130 / 3,861. So the cut neither loses nor buys anything in the gate's currency; it only moves which reviewer reads what.

Counted lines are not the whole picture and the zero rows are named rather than averaged: the gate charges 0 for every path under __tests__/ and for src/eslint-suppressions.json because packageForPath returns undefined for them (14 such paths in the whole remainder, listed per unit in comment 6751466344). That is the same instrument that reports 0 for a PNG visual baseline: the 1321 and 1521 lanes' five PNGs measured zero gate charge for the same reason, and a zero charge from this gate is a statement about packageForPath, not about whether the change matters. Binary baselines stay out of the a+d arithmetic and stay named.

The one number that does not comply, measured three ways

U5c-6 is over the 1,000 hard line and no whole-file shape brings it under: the spec file alone is 1,138 a+d, with the suppressions row 1,140, and folding it into U5c-5 makes that unit 1,367. zdt split verify exits 1 on all three shapes with VIOLATION budget-hard: standalone 1140 a+d exceeds the 1000 hard cap — must split. The remaining lever is to cut inside readFileTool.spec.ts, which the split rule forbids, so this is the one row that needs a ruling: defer part of that spec rewrite to a later unit, carry the overshoot with the rationale in the PR body, or allow the file to be cut.

Merge order, stated as a chain

U5c-1 -> U5c-2 -> U5c-3 -> U5c-4 -> U5c-8, with U5c-5 -> U5c-6 and U5c-7 placeable anywhere after the base and before the integration PR. Each unit is a branch off the merge base above (never off the previous unit's head), each mid PR merges before the next opens so the bot never sees a cumulative head, and every price quoted here is the unit measured standalone against its own base.

  • U5c-1 first: jsonFileRecovery.ts imports withFileLock and safeWriteJson.ts imports acquireFileLock from src/utils/fileLock.ts, and U5c-1 is where the landed artefact spelling and this branch's canonical lock key become one file.
  • U5c-2 before U5c-3: safeWriteJson.ts imports recoverInterruptedReplacement from ./jsonFileRecovery.
  • U5c-4 after U5c-3: it is the suite for the code U5c-3 lands.
  • U5c-8 after U5c-3: 8 of McpHub.ts's 67 changed rows name safeWriteJson, and the change relies on safeWriteJson resolving the publish target before staging because it passes the workspace root as confineTo.
  • U5c-6 after U5c-5: its suppressions row is the core/tools/__tests__/readFileTool.spec.ts count going 96 to 94 (blob ae3d504003d7e616b129986c252cc97a35b7b568 to f3714cd1f521c1d34e2dbc252cd7b970d226f6a6), a decrease, and a decrease can only land with or after the spec change that earns it.
  • U5c-7 carries the one advisory row in the whole remainder (src/core/task/Task.ts:4458, mutation exclusions must be reasoned next-line directives), and it travels with Task.ts, not to a cleanup unit.

Chain and knip rows that bind the queue

  • main + 2357 and main + 1913 both merge clean in either landing order (6d47a72e8f9b…, 9fe64bcd8a4a…, then 010df646f1… and 81e24cd6c1…, kept under refs/tmp/u5a-r23-*), and git merge-file --diff3 across the 22 paths those two heads both touch at their merge base 416179d17de83dfc9bfc1212cc8640da570db90a needed text resolution in 0 hunks. The only both-changed path, src/utils/__tests__/safeWriteJson.test.ts, auto-merges to 60,408 B / 1,354 line-endings with 51 test headers, 0 conflict markers, 0 duplicate test titles, and all 4 of 1913's rows present.
  • Replay is a different question: 2357 onto main stops on safeWriteJson.ts and safeWriteJson.test.ts, 1913 onto main stops on the same two paths, 2357 onto main+1913 stops on Task.ts, and 1913 onto main+2357 is clean. If the chain is rebuilt by rebase, 2357's content moves first; as merges the order is free.
  • knip.json needs no unit: 2357 head and 2385 head carry the identical blob 111f022102043c67fb581407a315ae16cca305b7 (2,687 B) and main carries 1f4956faa6cc789a34d0d2d0e9f4397ce0ca1bc6 (2,654 B); the only element 2357 adds names services/file-safety/safeWriteText.ts, which 2385 lands. The rule adopted from the 1410 measurement stands for this chain: code without its entry element is green (exit 0, 0 B stdout+stderr, e3b0c44298fc1c14), the entry element without its code is red (exit 1, 657 B, 0b983e73f8bb3998, naming the unresolved import and the config hint), so a unit carries a file and its entry element together or the chain does not move.
  • The four open review rows on PR 2385 that nothing on that PR answers stay answered as unanswered and queue behind 2385's merge rather than onto it: 2385 sits at 471 of the 500 counted lines, 29 lines of headroom. They are not part of any unit here.

Precondition met: the two landed-file unions are now code, not merge output

src/utils/fileLock.ts union blob 757041f107ef004d0607cf57890e4d7c6a5eb644 (14,393 B / 298 line-endings; the earlier mechanical union was 14,289 B / 297 le): the landed lockArtefactSpelling() export, the artefactSpelling option and the fsSync import are restored, and _lockCanonicalKey now takes const lockfilePath = options?.artefactSpelling ?? lockArtefactSpelling(absoluteFilePath) and passes it as the artefact, so the canonical-key split and the landed spelling pin coexist and the exports match the landed set (LOCK_STALE_MS, FileLockOptions, lockArtefactSpelling, acquireFileLock, withFileLock).
src/utils/safeWriteJson.ts union blob cba3807bd1b463329da5ecee16d994d4410212fa (29,457 B / 628 line-endings; mechanical union 29,513 B / 630 le): captures the spelling before the write can move the leaf and acquires acquireFileLock(lockKey, { artefactSpelling }) — the canonical key, the captured spelling — and keeps this branch's let stagedBytes declaration above the protected block.
A third resolution was forced by the tests, not by the merge: __tests__/safeWriteJson.lockKey.spec.ts mocks ../fileLock, so the new import threw before any assertion. Blob 3033fb237c386ecc83d9ea670e79a695604c1c10 (7,900 B / 202 le) becomes 2f74b807da1ae5a291dfdd62ac05d41b8ce5fc00 (8,872 B / 213 le): the mock exports lockArtefactSpelling, and the peer-commit test now asserts both arguments of the acquisition — the key that excludes the peer, and the caller's own spelling — instead of the key alone.

Proof, named leg by leg. src declares check-types as tsc --noEmit with src/tsconfig.json; run by node_modules/typescript 5.9.3 under node v22.23.4 from the declaring package directory: base tree exit 0, 0 B stdout and 0 B stderr (byte pair e3b0c44298fc1c14, 21.2 s); compiled-union tree exit 0, 0 B / 0 B, same pair (12.2 s). Suites: vitest 4.1.12 via the package's own test:unit script form (vitest run --config vitest.unit.config.ts) under node v22.23.4 over fileLock.spec.ts, fileLock.canonicalKey.spec.ts, safeWriteJson.test.ts, safeWriteJson.interruption.test.ts, safeWriteJson.lockKey.spec.ts, jsonFileRecovery.test.ts — first leg exit 1 (3 failed, 94 passed, 4 skipped, 8,167 B, 9cd022f8ea936e2f), after the mock export exit 1 (1 failed, 96 passed, 5,354 B, be8c3cc7403cac8f), final leg exit 0, 6 files, 97 passed, 4 skipped (3,793 B, 1c039251c459e561). Byte pairs from all-pass legs are not equal to each other because vitest prints elapsed times; they are quoted as pairs, not as exit codes.
Two-sided control on the identity behaviour: acquiring on the caller's lexical path instead of the lock key — 2385's shape with the spelling kept — exits 1 with safeWriteJson.lockKey.spec.ts > safeWriteJson lock key under a peer commit failing (5,396 B, c9435cb527f143da), so the canonical-key leg is test-covered and load-bearing. Ignoring the caller-supplied spelling inside _lockCanonicalKey leaves all 97 tests passing (exit 0, 3,793 B, 077ce36457e53f3d): nothing in the content this chain carries distinguishes a caller-supplied spelling from a derived one — fileLock.spec.ts names .lock 16 times and lockfilePath / artefactSpelling zero times — so the artefact-spelling leg lands compiled but untested, which is the same gap the four queued 2385 rows ask about, and it belongs to whoever owns that coverage.

Contracts and verify exits (the skill requires one contract per unit before verifying, and the verify exit is the claim)

Eight contracts scaffolded with zdt split contract against these same base/head pairs and verified with zdt split verify --contract <unit> --worktree wt-r302-2357-cut-price. Ports pin the content source as the commit pair 5319fe91fb0caf4aa4ec66aac68fa03744ebbc23 to d6fa7117e07b70455da24e4722b47eb0b202fa6e (never a stacked PR head); the two union units are plan-anchored rework units pinning design.plan to this issue's comment 6751466344, because their content intentionally diverges from any port source. Files: zdt split verify exits — U5c-1 0, U5c-2 0, U5c-3 0, U5c-4 0, U5c-5 0, U5c-6 1, U5c-7 0, U5c-8 0. Every port unit reports OK (content subset of source) with byte-identical on each file; the rework units report REWORK (plan-anchored); no unit reported a foreign file or a duplicate helper.
The diagnostic the rule predicts was reproduced rather than described: re-anchoring U5c-1 to the port source and dropping its design block makes the same verify exit 1 with DRIFT +16/-13 src/utils/fileLock.ts — unit added 16 line(s) / deleted 13 line(s) NOT in the source diff, which is the union's own content showing up as foreign against a port contract. Contract files and full reports are in .tmp-b1-1404/r24-contracts/.

State

Nothing was pushed, no branch was written, no PR opened, no review requested. The priced heads are local single-parent synthetic commits kept under refs/tmp/u5a-r24-*, refs/tmp/u5a-r24c-* (compiled-union units) and refs/tmp/u5a-r23-*; the priced worktree wt-r302-2357-cut-price is back at the base with git status --porcelain empty. If main moves, or 2385 lands with content different from 970b3a47ba…, this table has to be re-priced before the first split PR opens, and that re-price — not preference — decides the cut.

ClineProvider.ts per-construct cut maps — 1565 and 1921, priced (f0-1528, task-81)

This section carries the two maps the provider-family rule asks for. The rule as adopted: no lane in the provider family (1557, 1565, 1919, 1921, 1406) pushes src/core/webview/ClineProvider.ts content until its PR carries a per-construct cut proposal priced against a compiled union. Both maps below are measured, not typed: every number comes from selectFromGit against synthetic single-parent commit-tree commits run in the same span as this append, on node 22.23.4, and the two heads are the ones GET /pulls/1565 and GET /pulls/1921 name. 1557, 1919 and 1406 still owe their own maps.

1565 — src/core/webview/ClineProvider.ts at head 856dc80ebd7b3dfce445977d4e447729a3a68a74

Checked out at 856dc80ebd7b3dfce445977d4e447729a3a68a74 (git rev-parse HEAD printed beside the ASKED sha; scratch single-checkout at 1565, the PR worktree at 1921), lockfile blob 0fb1d6941590e544afe9d81aaaad80ebeb4fe4d7 (612369 B), file blob ef7ad52c20988aec3c2e11b1e20bb0cc8a08ca04 (7694 lines). Priced under node 22.23.4 (.nvmrc pin; PATH default is v25.9.0) through scripts/stryker-diff.mjs blob 7dfe0006d2d5c7e4d2282f65f68b8ed5f16866bc, against synthetic commit-tree commits whose single parent is named with each row.

shape constructs (lines in the head file) gate charge, extension package a+d
head as it stands none 2230 against the 500 cap 0 (file 2,462 + 253 = 2,715)
introduce deleteProviderProfile as its own module (3615-3911, 297 lines, 285 of them changed by the PR) its own body: 154 executable new file only, 297 additions
introduce loadViewState as its own module (1132-1301, 170 lines, 170 of them changed by the PR) its own body: 96 executable new file only, 170 additions
introduce disposeInternal as its own module (1701-1828, 128 lines, 108 of them changed by the PR) its own body: 106 executable new file only, 128 additions
introduce setViewStateId as its own module (1033-1122, 90 lines, 90 of them changed by the PR) its own body: 43 executable new file only, 90 additions
introduce savePersistedViewState as its own module (780-847, 68 lines, 68 of them changed by the PR) its own body: 68 executable new file only, 68 additions
introduce setValues as its own module (5650-5718, 69 lines, 68 of them changed by the PR) its own body: 36 executable new file only, 69 additions
introduce broadcastResetToAllInstances as its own module (5817-5871, 55 lines, 54 of them changed by the PR) its own body: 55 executable new file only, 55 additions
introduce rekeyPersistedViewStateEntry as its own module (954-1003, 50 lines, 50 of them changed by the PR) its own body: 50 executable new file only, 50 additions
introduce _updateViewLocalStateFromMutation as its own module (5737-5778, 42 lines, 42 of them changed by the PR) its own body: 40 executable new file only, 42 additions
introduce refreshViewLocalStateForUpdatedProfile as its own module (4193-4230, 38 lines, 38 of them changed by the PR) its own body: 35 executable new file only, 38 additions
introduce setValue as its own module (5573-5607, 35 lines, 34 of them changed by the PR) its own body: 20 executable new file only, 35 additions
introduce getPersistedViewStates as its own module (738-769, 32 lines, 32 of them changed by the PR) its own body: 18 executable new file only, 32 additions
batch unit 1565-U1 off base (parent 73931fe8be25, commit be354c8f20f3) deleteProviderProfile@3615-3911 + loadViewState@1132-1301 + disposeInternal@1701-1828 + setViewStateId@1033-1122 1259 (gate threw: extension has 1259 changed executable lines (limit 500). Split…) +19806 / -361 = 20167
batch unit 1565-U2 off base (parent 73931fe8be25, commit a647eada9282) savePersistedViewState@780-847 + setValues@5650-5718 + broadcastResetToAllInstances@5817-5871 + rekeyPersistedViewStateEntry@954-1003 + _updateViewLocalStateFromMutation@5737-5778 + refreshViewLocalStateForUpdatedProfile@4193-4230 + setValue@5573-5607 + getPersistedViewStates@738-769 1182 (gate threw: extension has 1182 changed executable lines (limit 500). Split…) +19510 / -361 = 19871
residual after all twelve move out (ab3205d91c52, parent 73931fe8be25) the file with the twelve removed 1610 file 1416 + 281

Measured facts the map turns on.

  1. The gate counts new-side executable lines only: a candidate that removes lines and adds none prices at 0 (every deletion-only candidate returned packages: []), which is why an earlier unit that already carries a construct makes the later unit charge nothing for it.
  2. The file alone charges 1371 changed executable lines; the extension package charges 2230 (the gate threw the same sentence locally). The rest of the family's content in that same package is therefore 2230 − 1371 = 859 lines, and that number alone is already over the 500 cap for a unit that carries no ClineProvider.ts at all.
  3. Moving a construct into a new file inside the same PR does not reduce the charge — measured sweep, cumulative from the largest down: 1 constructs → 2239, 2 constructs → 2253, 3 constructs → 2271, 4 constructs → 2271, 5 constructs → 2276, 6 constructs → 2277, 7 constructs → 2305, 8 constructs → 2308, 9 constructs → 2327, 10 constructs → 2328, 11 constructs → 2329, 12 constructs → 2330. The moved lines are still changed lines; only their file changed.
  4. Pricing the same constructs as their own unit off the family base 73931fe8be253b93ec11103a0672c615e849f895 (single-parent commits, printed above) gives 1565-U1 = 1259, 1565-U2 = 1182, i.e. the unit's body charge plus the 859 lines the other files ride in with it: 1259 − 399 = 860, 1182 − 322 = 860 ✓ which is the arithmetic that shows the cut cannot be finished inside this file.
  5. The residual unit — the file with all twelve constructs already moved out — still charges 1610 (1416 + 281 a+d on the file). Fourteen more units of that size would not be smaller than the cap; the file's own charge is the binding number, not the constructs.

Where removal breaks compilation, and what the union costs. The synthetic trees are plumbing-only — a body lifted into a function shell — so nothing was compiled here; the break is established by the reference census (git grep -c at the head, whole src), and the census is what makes a single-construct cut non-standalone: 1565-U1: deleteProviderProfile:48, loadViewState:38, disposeInternal:3, setViewStateId:104; 1565-U2: savePersistedViewState:9, setValues:230, broadcastResetToAllInstances:19, rekeyPersistedViewStateEntry:3, _updateViewLocalStateFromMutation:3, refreshViewLocalStateForUpdatedProfile:20, setValue:641, getPersistedViewStates:18. setValue at 641 references, setValues at 230, removeClineFromStack at 164 and setViewStateId at 104 cannot leave the class without also moving or exporting their call sites, so the priced unit for those is the union of construct plus call sites, not the construct — the rows above are those unions, and they are what a compilable cut costs.

Reading against the a+d table. 400 a+d is the soft line with the rationale in the body; 1,000 is the hard line and means split again; a unit may not split a file. The file's own a+d at head is 2,462 + 253 = 2,715, already past 1,000, so under that rule no single unit can carry ClineProvider.ts as it now stands, and the only shape that respects both the rule and the 500-line cap is the pre-cut: a unit that moves whole constructs into new modules (its a+d is the moved span twice over, 1565-U1 +19806/-361, 1565-U2 +19510/-361 measured with the rest of the family content riding along, so each must be trimmed to its own constructs before it is pushed), followed by units that carry the residual content file by file rather than construct by construct.

1921 — src/core/webview/ClineProvider.ts at head e4b102b2f129d07923eb5b8cf825074909ff4c4a

Checked out at e4b102b2f129d07923eb5b8cf825074909ff4c4a (git rev-parse HEAD printed beside the ASKED sha; scratch single-checkout at 1565, the PR worktree at 1921), lockfile blob 0fb1d6941590e544afe9d81aaaad80ebeb4fe4d7 (612369 B), file blob 5e071eed90bcbd09eac8a4c22d8694b4a4d7b63b (8046 lines). Priced under node 22.23.4 (.nvmrc pin; PATH default is v25.9.0) through scripts/stryker-diff.mjs blob 7dfe0006d2d5c7e4d2282f65f68b8ed5f16866bc, against synthetic commit-tree commits whose single parent is named with each row.

shape constructs (lines in the head file) gate charge, extension package a+d
head as it stands none 2328 against the 500 cap file 2,990 + 429 = 3,419; whole PR +18,257 / -791 = 19,048
introduce deleteProviderProfileUnlocked as its own module (3643-4099, 457 lines, 443 of them changed by the PR) its own body: 234 executable new file only, 457 additions
introduce loadViewState as its own module (1179-1297, 119 lines, 119 of them changed by the PR) its own body: 65 executable new file only, 119 additions
introduce savePersistedViewState as its own module (827-909, 83 lines, 82 of them changed by the PR) its own body: 83 executable new file only, 83 additions
introduce handleModeSwitchUnlocked as its own module (2857-3016, 160 lines, 74 of them changed by the PR) its own body: 158 executable new file only, 160 additions
introduce _restoreProfileActivationState as its own module (3106-3172, 67 lines, 67 of them changed by the PR) its own body: 67 executable new file only, 67 additions
introduce setViewStateId as its own module (1104-1171, 68 lines, 61 of them changed by the PR) its own body: 39 executable new file only, 68 additions
introduce resetState as its own module (6225-6312, 88 lines, 58 of them changed by the PR) its own body: 52 executable new file only, 88 additions
introduce rekeyPersistedViewStateEntry as its own module (1036-1098, 63 lines, 56 of them changed by the PR) its own body: 63 executable new file only, 63 additions
introduce updateProfileModel as its own module (3366-3450, 85 lines, 54 of them changed by the PR) its own body: 85 executable new file only, 85 additions
introduce broadcastResetToAllInstances as its own module (6172-6221, 50 lines, 49 of them changed by the PR) its own body: 50 executable new file only, 50 additions
introduce _snapshotProviderManagerState as its own module (3181-3221, 41 lines, 41 of them changed by the PR) its own body: 41 executable new file only, 41 additions
introduce removeClineFromStack as its own module (1394-1430, 37 lines, 37 of them changed by the PR) its own body: 19 executable new file only, 37 additions
batch unit 1921-U1 off base (parent 73931fe8be25, commit 8c2fa698e8ef) deleteProviderProfileUnlocked@3643-4099 + loadViewState@1179-1297 + savePersistedViewState@827-909 1040 (gate threw: extension has 1040 changed executable lines (limit 500). Split…) +15930 / -362 = 16292
batch unit 1921-U2 off base (parent 73931fe8be25, commit 2744e198ca6b) handleModeSwitchUnlocked@2857-3016 + _restoreProfileActivationState@3106-3172 + setViewStateId@1104-1171 + resetState@6225-6312 + rekeyPersistedViewStateEntry@1036-1098 1037 (gate threw: extension has 1037 changed executable lines (limit 500). Split…) +15717 / -362 = 16079
batch unit 1921-U3 off base (parent 73931fe8be25, commit 6360f9c6bee7) updateProfileModel@3366-3450 + broadcastResetToAllInstances@6172-6221 + _snapshotProviderManagerState@3181-3221 + removeClineFromStack@1394-1430 853 (gate threw: extension has 853 changed executable lines (limit 500). Split …) +15484 / -362 = 15846
residual after all twelve move out (a8e31f23ee17, parent 73931fe8be25) the file with the twelve removed 1602 file 1765 + 522

Measured facts the map turns on.

  1. The gate counts new-side executable lines only: a candidate that removes lines and adds none prices at 0 (every deletion-only candidate returned packages: []), which is why an earlier unit that already carries a construct makes the later unit charge nothing for it.
  2. The file alone charges 1671 changed executable lines; the extension package charges 2328 (CI agrees — job mutation-diff, 147,511-byte log line 1845: Mutation gate failed: extension has 2328 changed executable lines (limit 500). Split the PR or obtain a maintainer-reviewed narrow exclusion., and line 1861 No files were found with the provided path: reports/mutation/, so no mutants ran and no mutant number may be quoted for this head). The rest of the family's content in that same package is therefore 2328 − 1671 = 657 lines, and that number alone is already over the 500 cap for a unit that carries no ClineProvider.ts at all.
  3. Moving a construct into a new file inside the same PR does not reduce the charge — measured sweep, cumulative from the largest down: 1 constructs → 2335, 2 constructs → 2303, 3 constructs → 2298, 4 constructs → 2417, 5 constructs → 2438, 6 constructs → 2443, 7 constructs → 2461, 8 constructs → 2464, 9 constructs → 2495, 10 constructs → 2520, 11 constructs → 2540, 12 constructs → 2557. The moved lines are still changed lines; only their file changed.
  4. Pricing the same constructs as their own unit off the family base 73931fe8be253b93ec11103a0672c615e849f895 (single-parent commits, printed above) gives 1921-U1 = 1040, 1921-U2 = 1037, 1921-U3 = 853, i.e. the unit's body charge plus the 657 lines the other files ride in with it: 1040 − 382 = 658, 1037 − 379 = 658, 853 − 195 = 658 ✓ which is the arithmetic that shows the cut cannot be finished inside this file.
  5. The residual unit — the file with all twelve constructs already moved out — still charges 1602 (1765 + 522 a+d on the file). Fourteen more units of that size would not be smaller than the cap; the file's own charge is the binding number, not the constructs.

Where removal breaks compilation, and what the union costs. The synthetic trees are plumbing-only — a body lifted into a function shell — so nothing was compiled here; the break is established by the reference census (git grep -c at the head, whole src), and the census is what makes a single-construct cut non-standalone: 1921-U1: deleteProviderProfileUnlocked:2, loadViewState:26, savePersistedViewState:10; 1921-U2: handleModeSwitchUnlocked:15, _restoreProfileActivationState:7, setViewStateId:118, resetState:15, rekeyPersistedViewStateEntry:3; 1921-U3: updateProfileModel:30, broadcastResetToAllInstances:4, _snapshotProviderManagerState:6, removeClineFromStack:164. setValue at 641 references, setValues at 230, removeClineFromStack at 164 and setViewStateId at 118 cannot leave the class without also moving or exporting their call sites, so the priced unit for those is the union of construct plus call sites, not the construct — the rows above are those unions, and they are what a compilable cut costs.

Reading against the a+d table. 400 a+d is the soft line with the rationale in the body; 1,000 is the hard line and means split again; a unit may not split a file. The file's own a+d at head is 2,990 + 429 = 3,419, already past 1,000, so under that rule no single unit can carry ClineProvider.ts as it now stands, and the only shape that respects both the rule and the 500-line cap is the pre-cut: a unit that moves whole constructs into new modules (its a+d is the moved span twice over, 1921-U1 +15930/-362, 1921-U2 +15717/-362, 1921-U3 +15484/-362 measured with the rest of the family content riding along, so each must be trimmed to its own constructs before it is pushed), followed by units that carry the residual content file by file rather than construct by construct.

What both maps say together

  • The constructs are real and nameable, and the twelve largest carry 1,039 of 1565's 2,462 new-side changed lines and 1141 of 1921's 2,994 — but charging is per changed executable line, not per file, so relocating them inside the same PR moves the number the wrong way (2,230 → 2,330 on 1565; 2,328 → 2,303 → 2,557 on 1921, measured sweeps above).
  • The only shapes that actually reduce a unit's charge are (a) an earlier unit that already carries the construct's lines, which prices the later one at 0 for them, and (b) units that carry whole files instead of constructs. A cut that keeps both a 500-line executable budget and the no-splitting-inside-a-file rule therefore has to partition the family's other 44 files (1565) and 54 files (1921) as well — the 859 and 657 lines that are not this file.
  • Nothing here was compiled: the synthetic trees lift bodies into a function shell, so the compilation claim rests on the reference census, and the census is printed per unit. A lane that wants a compiled union has to wire the exports and re-price; these rows are the ceiling, not the finished unit.
  • Both PRs stay mergeable_state: blocked. 1921's mutation-diff refuses to run at all (size, not mutants). 1565's charge is 2,230. Neither is cleared by a test-only commit — 1921's newest commit e4b102b2f129d07923eb5b8cf825074909ff4c4a is test-only and charges 0 executable lines, and the answer on review row 4510170750 says so in the same words.

Round 25 (lane u5a-2359) — the first unit of the #2357 remainder is open as PR 2393, and every price below is taken against the sha its own branch forks from

Base-tip instrument, stated once and used everywhere in this section: GET /repos/Zoo-Code-Org/Zoo-Code/branches/main → e487cd7b2a497aa1d9b60f9b0cb497f7839bb629. pulls/2393.base.sha reports the same value but is read as a fossil, not as the source, per the maintainer's ruling of 2026-11-09. Pricing instrument, unchanged: the repo's own scripts/stryker-diff.mjs, blob 7dfe0006d2d5c7e4d2282f65f68b8ed5f16866bc, 30,108 B / 869 line-endings, caps MAX_CHANGED_LINES = 500 (line 11) and MAX_MUTANTS = 400 (line 12), run against a checked-out tree equal to a single-parent synthetic commit (commit-tree -p <base>, one parent, printed before every row), so the gate sees exactly the tree the unit would ship.

1. The shape of U5c-6's 1,138, measured before it was carried

src/core/tools/__tests__/readFileTool.spec.ts: base blob 52e04415ea5350906ea5064616501a46f3d235fc — the same blob at e487cd7b2a… and at 970b3a47ba… — 57,863 B / 1,692 line-endings; head blob b97b17d063a0567e09ea6ce6e10184ccd9fc5714, 101,419 B / 2,824 line-endings. git diff --numstat gives 1,135 insertions / 3 deletions on the path against either base. Rename detection finds nothing to blame the number on: git diff -M --find-renames --numstat --summary between base blob and head blob reports no rename from/to line, -C --find-copies-harder reports no copy from line, and the detector stays silent even at --find-renames=10%, the loosest threshold it accepts; git log --follow --name-status across 5319fe91fb..d6fa7117e0 shows only M for this path, never R, so the file was never moved. 2,044 of the head file's 2,824 lines appear verbatim in the base file (72%): the added lines are new content on top of an untouched body.

Three whole-file shapes priced: spec + its src/eslint-suppressions.json row = 1,140 a+d, zdt split verify exit 1; spec alone = 1,138 a+d, exit 1; spec folded into U5c-5 = 1,367 a+d, exit 1. Each prints VIOLATION budget-hard: — standalone 1140 a+d exceeds the 1000 hard cap — must split (the figure in the sentence is the figure of the shape priced). The only smaller shape available requires cutting inside a file. It is therefore carried with a documented deviation, not deferred and not cut: deferring it defers the proof for U5c-5's ReadFileTool changes, which are ready. The overshoot is a reviewer-load disclosure and not a CI leg: the gate charges this unit 0 counted executable lines of 500 and 0 selectors of 400, because packageForPath returns undefined for both src/core/tools/__tests__/readFileTool.spec.ts and src/eslint-suppressions.json; both rows are named here rather than averaged.

2. The artefact-spelling coverage closes in U5c-1, not behind #2385

New file src/utils/__tests__/fileLock.artefactSpelling.spec.ts, 2,499 B / 68 line-endings, blob 1c2e12dc66aabf0931aa72549f799f3a997e01de: one acquisition with a caller-supplied artefactSpelling asserted verbatim as the lockfilePath passed to proper-lockfile, three acquisitions with no spelling supplied, and expect(artefacts).toEqual([supplied, derived, derived]) so the two artefacts are shown to differ. prettier --check exit 0 (66 B, pair 17aa973d3f004560); eslint --max-warnings=0 with paths relative to src exit 0, 0 B + 0 B, pair e3b0c44298fc1c14, with src/eslint-suppressions.json byte-identically unchanged (sha256-16 055a26991146ecea before and after).

Two-sided control. Dropping the caller spelling (const lockfilePath = lockArtefactSpelling(absoluteFilePath), the shape #2385 shipped) → exit 1, 2 tests failed, both of them the new spec's, 752 B + 3,928 B, pair b839a9857b8b5e8d. Keeping the option and stopping the derivation (options?.artefactSpelling ?? (absoluteFilePath + ".lock")) → exit 0, pair 3b9761a61d6e3198: for a leaf that exists, lockArtefactSpelling and path + ".lock" produce the same string, so that mutant is equivalent in this scenario and this spec does not distinguish the derivation route from a leaf-lexical fallback. Recorded, not smoothed. The caller-supplied tempPath leg #2385 asks for on the safeWriteJson side is still uncovered today and is left for U5c-3 or U5c-4 to carry.

3. Two columns per unit: the price against 970b3a47ba… as planned, and against e487cd7b2a… as each branch actually forks

unit a+d vs 970b3a47ba… a+d vs e487cd7b2a… counted lines / selectors against the real base band against the real base
U5c-1 (as opened: union + fileLock.spec.ts + the new spelling spec) 593 (62 / 21) 618 (596+22) 81 / 29 400–1,000, rationale in the body
U5c-1 full shape (with fileLock.canonicalKey.spec.ts) 660 (62 / 21) 685 (663+22) 81 / 29 400–1,000
U5c-2 jsonFileRecovery 176 (26 / 3) 176 (173+3) 26 / 3 under soft
U5c-3 safeWriteJson union + 2 specs 614 (131 / 46) 625 (606+19) 132 / 46 400–1,000
U5c-4 safeWriteJson.test.ts 517 (0 / 0) 517 (507+10) 0 / 0 400–1,000
U5c-5 ReadFileTool + indentation reader + 2 specs 227 (51 / 31) 227 (219+8) 51 / 31 under soft
U5c-6 readFileTool.spec.ts + suppressions row 1,140 (0 / 0) 1,140 (1,136+4) 0 / 0 over 1,000, carried with the deviation above
U5c-7 observationRegistry + Task + 3 specs 384 (25 / 15) 384 (384+0) 25 / 15 under soft; one advisory printed (src/core/task/Task.ts:4458, Stryker-disable-comment form)
U5c-8 McpHub confinement + spec 211 (27 / 14) 211 (201+10) 27 / 14 under soft

Against the real fork base only the two units that carry #2385-resolved files grow: U5c-1 by +25 a+d (593 → 618) with counted lines 62 → 81 and selectors 21 → 29, and U5c-3 by +11 a+d (614 → 625) with counted lines 131 → 132 and selectors unchanged at 46. The reason is the same in both: main does not contain #2385, so #2385's landed lines become this chain's own contribution and the gate charges them.

Compliance verdict, the deviation decision and the chain order are taken from the real-base column: order U5c-1 → U5c-2 → U5c-3 → U5c-4 → U5c-8, with U5c-5 → U5c-6 and U5c-7 placeable anywhere after U5c-1. U5c-6 is the only unit over the hard cap and it is carried.

4. U5c-1 opened: PR 2393, one push, no --no-verify

GET /repos/Zoo-Code-Org/Zoo-Code/pulls/2393 → head.sha ce52d04464e31c7f5d127413d1f0aebe4598cf73, head.ref vps2f-1914-u5c1-filelock, base.sha e487cd7b2a497aa1d9b60f9b0cb497f7839bb629; head repo easonLiangWorldedtech/Zoo-Code, base repo Zoo-Code-Org/Zoo-Code, draft: false, 1 commit, 3 changed files, +596 / −22. Body: 9,372 B / 59 line-endings sent, stored 9,372 B / 59 line-endings, sha256-16 f570f28f95b026c6, first 9,372 bytes byte-identical to what was sent and no trailing byte added because the sent text already ended in a line-ending. Contract zdt split verify against base e487cd7b2a… and this head: exit 0, PASS, standalone: 618 a+d / 3 files SOFT-OVERSHOOT (rationale required in PR body), three REWORK (plan-anchored) rows; anchored as rework because with the port anchor the same verifier exits 1 with DRIFT +46/-0 src/utils/fileLock.ts and VIOLATION foreign-content — those 46 lines are #2385's landed artefact spelling, which this unit carries as its own contribution.

Arms read twice, with the instrument named: a read with no filter parameter returned 17 legs, then check-runs?per_page=100&filter=all returned 18, and a later no-filter read also returned 18 as legs started — a leg count is not a fact about the commit until the filter and the moment are named. As of the filter=all read: CodeRabbit skipped (its own path filtering on this PR, not agreement with the code), CodeQL, check-translations, knip, Build test VSIX, dependency-review, invisible-chars, theme-fixtures, Analyze (javascript-typescript) and both Zoo Code / …reconcile legs success; compile, platform-unit-test (ubuntu-latest), platform-unit-test (windows-latest), webview-visual, e2e-mock, extension-host-visual and mutation-diff were in_progress with conclusion: null. mergeable: true, mergeable_state: blocked, no review_decision field returned, no labels. Nothing was resolved and nothing was requested.

Hooks, judged by what they did, not by their names: pre-commit ran lint-staged (pnpm --dir src exec eslint --max-warnings=0 --no-warn-ignored and prettier --write over the 3 staged files) and completed, and the committed tree d0ae766244e9af95715c587ef569d4149b1bda0a is the same tree as the priced unit commit, so the prices above are prices of the pushed head. pre-push ran the root check-types (tsc -p tsconfig.json --noEmit then turbo check-types --log-order grouped --output-logs new-only) and printed Tasks: 12 successful, 12 total / Cached: 11 cached, 12 total / Time: 4.19s — 11 legs were cache hits and only zoo-code:check-types, the declaring package's own tsc --noEmit, actually executed, and a cache hit proves nothing about the legs it did not run. The declaring package's own type check was also run directly under node v22.23.4 (C:/work/Zoo-Code-fork/.tmp-node22/node_modules/node/bin/node.exe, typescript 5.9.3, src/tsconfig.json): exit 0, 0 B + 0 B, pair e3b0c44298fc1c14. Carried-file suites under the same interpreter (vitest 4.1.12, vitest.unit.config.ts, cwd src, CI=1): exit 0, 2 files, 13 tests passed, 0 failed, 676 B + 689 B, pair fe3741548fe696a7; repeat runs differ only by their Duration lines (6fba006f3f98fa3f, a038e8237a4decd1). knip as the repo declares it (knip --treat-config-hints-as-errors, cwd = repo root): at this head exit 1, 327 B + 104 B, pair 5cf096ddc7615422; at the base commit exit 1, the same 327 B + 104 B, the same pair — this unit changes nothing knip reports; the two exit-1 legs are pre-existing at main tip. CI's own knip leg returned success on this head.

5. Two dependencies that only compiling against the real base showed

src/utils/__tests__/fileLock.canonicalKey.spec.ts imports resolveLockKey from src/services/file-safety/safeWriteText.ts, and that file does not exist at e487cd7b2a…. Measured with the declaring package's own compiler: with the spec in the tree, tsc -p tsconfig.json --noEmit exits 2 with utils/__tests__/fileLock.canonicalKey.spec.ts(8,32): error TS2307: Cannot find module '../../services/file-safety/safeWriteText' or its corresponding type declarations. (170 B stdout, pair 1a497dcbb3defe18); without it, exit 0 with 0 B + 0 B. So the spec needs #2357's code for what it asserts and #2385's file for its import: it travels in a unit that forks from a base containing #2385, and it was left out of U5c-1 rather than shipped broken. The unit that carries it is 67 added lines, 0 counted executable lines.

6. Rule written into the plan

Re-price each unit immediately before it opens, against the sha its branch will fork from — never against a stacked head and never against a hypothetical main; take the compliance verdict, the deviation decision and the chain order from that same reading, and take the base tip from GET /branches/<base.ref> (or the merge ref's first parent) rather than from pulls/N.base.sha.

Review state for everything asked in this round: there is no review event from anyone except the bot and the author, and no non-empty review body from anyone except the bot. #2357 and #2385 were not touched by this unit.

ClineProvider.ts per-construct cut maps — 1557, 1919 and 1406, priced on the live base tip (f0-1528, task-85)

Second half of the family rule the board adopted: no lane in the provider family (1557, 1565, 1919, 1921, 1406) pushes src/core/webview/ClineProvider.ts content until its PR carries a per-construct cut proposal priced against a compiled union. 1565 and 1921 are priced above; these three are drawn the same way, with the base tip taken from GET /branches/main (e487cd7b2a497aa1d9b60f9b0cb497f7839bb629, committed 2026-11-09T04:09:13Z) instead of pulls/N.base.sha, and with the source of every number named. Nothing was pushed for any of the three. Pricing ran under node 22.23.4 with pnpm 10.8.1 against single-parent commit-tree commits whose parent is printed with each row, using gate blob 7dfe0006d2d5c7e4d2282f65f68b8ed5f16866bc and a private copy of it (blob e615b9779a9d, 30,120 B) that differs from the gate in 2 lines and only those: MAX_CHANGED_LINES and MAX_MUTANTS raised, so a whole manifest can be read instead of a throw.

Semantics first: a width number belongs to a package, and the gate reads the biggest one

Verified in the same copy the pricing ran through: line 11 export const MAX_CHANGED_LINES = 500, line 12 export const MAX_MUTANTS = 400, line 265 if (packageEntry.changedExecutableLines > MAX_CHANGED_LINES) {, and the mutant caps at lines 713 and 756, all strictly greater-than. The gate's outcome is the deciding package — the largest per-package charge — never a sum across packages, which is the rule published today as comment 6752571370 on this issue. Every width figure below therefore names its package, and the deciding package for all five family PRs is extension:

PR gate manifest at its own window (caps raised) deciding package this file's share of it
1565 types=28, extension 2,230, webview 45 extension 2,230 (1,730 over the cap) 1,371
1921 types=14, extension 2,328, webview 45 extension 2,328 (1,828 over) 1,671
1557 types=28, extension 1,852, webview 41 extension 1,852 (1,352 over the cap) 1,006
1919 types=14, extension 2,216, webview 45 extension 2,216 (1,716 over the cap) 1,629
1406 types=7, extension 468, webview 21 extension 468 (under the cap by 32) 3

Read that table as a correction as much as a completion: the section above this one, and the pointer comments on 1565 and 1921, quoted "1,610" and "1,602" for the residual. Those are the deciding-package (extension) figures for the synthetic residual units, not file-only figures. The file-only numbers, measured now with the same per-file method the gate uses internally, are 750 for 1565 (residual commit ab3205d91c52, parent 73931fe8be25: ClineProvider.ts 750, src/extension/api.ts 290, src/activate/registerCommands.ts 247, src/core/webview/webviewMessageHandler.ts 221) and 944 for 1921 (residual commit a8e31f23ee17, parent 73931fe8be25: ClineProvider.ts 944, src/activate/registerCommands.ts 329, src/core/webview/webviewMessageHandler.ts 293, src/core/config/ProviderSettingsManager.ts 19). Both pointer comments have been patched to say which figure is which.

What these three maps added to the instruments, before any construct is named

  1. The gate resolves its own base, and it is not pulls/N.base.sha. Lines 284-298 of the gate read, verbatim: // GitHub checks out the synthetic pull request merge commit, but pull_request.base.sha is frozen at event-creation time. When main advances afterwards, that stale base attributes unrelated upstream lines to the pull request. The merge commit's first parent is the base actually merged into. — then resolvePullRequestBase returns the head's first parent only when the head has two parents, and selectFromGit immediately computes mergeBase = git merge-base baseSha headSha and diffs mergeBase...headSha. Measured three ways on each head and identical each time: 1557 prices extension 1,852 from the live main tip, 1,852 from its merge-base 73931fe8be25, and 1,852 from a synthetic merge commit of main tip + head priced against main tip (CI's own shape, parents printed). 1919 gives 2,216 three times; 1406 gives 468 three times.
  2. What does not re-root is a hand-rolled git diff mainTip head, and the gap is drift, not noise. Counting new-side executable lines myself over that raw window gives 1,963 (1557), 2,494 (1919) and 864 (1406) instead of 1,852 / 2,216 / 468, and the delta is main's own movement landing in files the PR touches: src/scripts/verify-coverage-cache-inputs.mjs (+105), src/core/task/Task.ts (+81 to +98) and src/api/providers/qwen-code.ts (+84) charge in a main-tip window and do not charge in the merge-base window. A unit forked from main tip pays that drift; price it against the sha it forks from, which is the same conclusion the 2357 lane reached from its side.
  3. selectFromGit reads the working tree, not only git objects. Re-running it on 1406's residual commit while the scratch worktree sat at the PR head failed with ENOENT: no such file or directory on a path inside src/core/...; the pricing rows below are valid because the worktree was checked out to each synthetic commit immediately before its pricing, and the per-file figures are object-based (git show) and therefore immune to that trap.
  4. Deletion-only pricing holds on all five heads. A commit parented on the PR head that deletes the named construct bodies and adds nothing returns packages: [] — zero executable lines (1406 6dcf0a5ad48f, 1557 ec203ea7b602, 1919 4413f37f1503; the same shape was measured on 1565 and 1921 in the section above).
  5. CI agrees line for line. 1557's mutation-diff run #8590 concluded failure: 147,477 B log, 1,886 lines, line 1844 the only gate sentence — Mutation gate failed: extension has 1852 changed executable lines (limit 500). Split the PR or obtain a maintainer-reviewed narrow exclusion. — with line 1846 ##[error]Process completed with exit code 1. and line 1860 No files were found with the provided path: reports/mutation/.; 5 advisories, no mutants. 1919's run #8306: 147,620 B, 1,887 lines, line 1845 the same sentence at 2216, line 1861 the same artifact notice, 5 advisories, no mutants. 1406's run #8576 concluded success: 151,988 B, 1,937 lines, zero gate-failure sentences, zero ##[error] lines, 6 advisories, artifact upload succeeded. So 1406 is not a size refusal at all, and the mutant question stays closed for the whole class — survivors in changed code and the core preflight ENOENT print as advisories, and a cap is the only thing that fails this leg.

1557 — head 1b04a75b6e4c, ref vps2/f5-api-wiring, 213 commits, +16,329/−729 over 38 files (open, mergeable true / blocked)

pulls/1557.base.sha = 73931fe8be25 and merge-base(mainTip, head) = 73931fe8be25 are the same commit, so this PR's window is unambiguous and main's later movement belongs to drift, not to it. git rev-parse HEAD printed beside the ASKED sha before and after every synthetic commit (it matched the head read from GET /pulls/1557, and the scratch worktree ends clean); lockfile blob 0fb1d6941590 (612,370 B). The file at head: blob 5afd0f335f33, 7,083 lines, 1852 + 252 = 2,104 a+d at the merge-base (against main tip: 1852 + 252).

measured (package named) value
gate manifest at the merge-base, caps raised types=28, extension 1,852, webview 41 — deciding package extension
ClineProvider.ts share of the deciding package 1,006 changed executable lines out of 1,851 changed lines
the rest of the deciding package 1,852 − 1,006 = 846 (over the cap on its own): extension/api.ts 256, core/webview/webviewMessageHandler.ts 247, activate/registerCommands.ts 240, core/config/importExport.ts 40, core/config/ContextProxy.ts 20, core/config/ProviderSettingsManager.ts 20
CI's own verdict for this head extension 1,852 — mutation-diff Changed-code mutation testing#8590 failure, 147,477 B log over 1,886 lines, line 1844 the only gate sentence, No files were found with the provided path: reports/mutation/ at line 1860, 5 advisories, no mutants ran — and it equals the locally measured extension figure
relocating constructs inside the same PR (parent = head, extension) 1 → 1,861, 12 → 1,952, baseline 1,852
deletion-only (parent = head) packages: [] — 0 executable lines in every package

Constructs, named from the declaration line. The first pass over 1557 and 1919 labelled every private async x( member async and reported a repo-wide count of that word (10,732 references for 1919, 10,870 for 1557) as its census, which is not a fact about any construct. The re-scan below reads the declaration; the spans, changed-line counts and executable counts were identical between the two passes, so no priced number is affected — only the names and the censuses.

construct (head line range) span changed at merge-base executable in body references in src
getPersistedViewStates@677-707 31 31 18 16
savePersistedViewState@718-785 68 68 68 11
rekeyPersistedViewStateEntry@862-911 50 50 50 3
loadViewState@1059-1153 95 95 54 22
dispose@1515-1598 84 63 45 1,666
postMessageToWebview@2301-2343 43 18 32 637
deleteProviderProfile@3275-3424 150 134 69 35
snapshotSiblingOverlaysBeforeSharedSettingsWrite@3468-3534 67 67 62 6
setValue@5016-5047 32 29 15 506
setValues@5057-5137 81 80 46 178
_updateViewLocalStateFromMutation@5156-5194 39 39 37 4
broadcastResetToAllInstances@5237-5276 40 39 40 17
sum of the 12 named constructs 780 713 536 —

Units of their own off e487cd7b2a49 — single parent printed with each; a unit forked from main tip pays the drift of fact 2, which is why these totals are larger than the PR's own number, and every total below is the deciding extension package:

unit constructs body executable extension charge a+d commit (parent)
1557-U1 3275-3424@deleteProviderProfile@3275-3424 + 1059-1153@async@1059-1153 + 5057-5137@async@5057-5137 + 718-785@async@718-785 + 3468-3534@async@3468-3534 + 1515-1598@dispose@1515-1598 + 862-911@async@862-911 394 1,352 +15,264/−1,406 = 16,670 00ecaae9baff (e487cd7b2a49)
1557-U2 5237-5276@broadcastResetToAllInstances@5237-5276 + 5156-5194@_updateViewLocalStateFromMutation@5156-5194 + 677-707@getPersistedViewStates@677-707 + 5016-5047@async@5016-5047 + 2301-2343@async@2301-2343 142 1,100 +14,854/−1,406 = 16,260 d4673711b26b (e487cd7b2a49)
residual unit: this file with every named construct gone all 12 536 moved unit 1,527, of which ClineProvider.ts itself 569 file 1138 + 318 vs main tip 21d0f1e71bab (e487cd7b2a49)

Plainly, for 1557. 1,006 of the deciding package's 1,852 counted lines live in this file; with every named construct removed the file still charges 569 (over the 500 cap on its own) while the rest of the package adds 846 more, and the unit that would carry the residue off main tip prices at 1,527. Relocating the constructs inside the same PR moves the number the wrong way (1,852 → 1,861 → 1,952). The file's own a+d, 2,104, is past the 1,000 hard reviewability line, so with no-splitting-inside-a-file no unit can carry this file as it stands — and for a PR whose deciding package charges 1,852, no side of a two-way whole-file split can pass while the file producing the charge cannot be cut. The two numbers a maintainer needs to answer split-versus-exclusion for 1557 are 569 for the file and 1,527 for the unit that would carry it.

Where a removal would not compile, and why the price is a union. Nothing here was compiled — a body lifted into a function shell is not a compiling module — so compilability rests on the reference census, which is the honest instrument available at this stage: dispose 1,666, postMessageToWebview 637, setValue 506, setValues 178, deleteProviderProfile 35, loadViewState 22, broadcastResetToAllInstances 17, getPersistedViewStates 16, savePersistedViewState 11, snapshotSiblingOverlaysBeforeSharedSettingsWrite 6, _updateViewLocalStateFromMutation 4, rekeyPersistedViewStateEntry 3. A construct with hundreds of call sites cannot leave the class without moving or exporting those call sites, so for those the priced unit is the union of construct plus call sites. The rows above carry whole bodies: that is the ceiling on what a compilable cut costs, not a unit somebody has built.

1919 — head 05aa4e103858, ref vps2-f2a, 177 commits, +15,611/−761 over 32 files (open, mergeable true / blocked)

pulls/1919.base.sha = e943530c8486 and merge-base(mainTip, head) = e943530c8486 are the same commit, so this PR's window is unambiguous and main's later movement belongs to drift, not to it. git rev-parse HEAD printed beside the ASKED sha before and after every synthetic commit (it matched the head read from GET /pulls/1919, and the scratch worktree ends clean); lockfile blob 520e42edbb09 (612,369 B). The file at head: blob d70299fba017, 7,961 lines, 2901 + 429 = 3,330 a+d at the merge-base (against main tip: 2920 + 442).

measured (package named) value
gate manifest at the merge-base, caps raised types=14, extension 2,216, webview 45 — deciding package extension
ClineProvider.ts share of the deciding package 1,629 changed executable lines out of 2,912 changed lines
the rest of the deciding package 2,216 − 1,629 = 587 (over the cap on its own): core/webview/webviewMessageHandler.ts 289, activate/registerCommands.ts 266, core/config/ProviderSettingsManager.ts 19, core/webview/profileDeletionError.ts 5, core/config/ContextProxy.ts 3, core/webview/ClineProviderFactory.ts 3
CI's own verdict for this head extension 2,216 — mutation-diff Changed-code mutation testing#8306 failure, 147,620 B log over 1,887 lines, line 1845 the only gate sentence, No files were found with the provided path: reports/mutation/ at line 1861, 5 advisories, no mutants ran — and it equals the locally measured extension figure
relocating constructs inside the same PR (parent = head, extension) 1 → 2,171, 12 → 2,422, baseline 2,216
deletion-only (parent = head) packages: [] — 0 executable lines in every package

Constructs, named from the declaration line. The first pass over 1557 and 1919 labelled every private async x( member async and reported a repo-wide count of that word (10,732 references for 1919, 10,870 for 1557) as its census, which is not a fact about any construct. The re-scan below reads the declaration; the spans, changed-line counts and executable counts were identical between the two passes, so no priced number is affected — only the names and the censuses.

construct (head line range) span changed at merge-base executable in body references in src
savePersistedViewState@816-898 83 82 83 10
rekeyPersistedViewStateEntry@1025-1082 58 51 58 3
setViewStateId@1088-1155 68 59 39 114
loadViewState@1163-1281 119 119 65 26
removeClineFromStack@1378-1414 37 37 19 164
handleModeSwitchUnlocked@2818-2959 142 54 140 10
_restoreProfileActivationState@3049-3115 67 67 67 7
_snapshotProviderManagerState@3124-3164 41 41 41 6
updateProfileModel@3309-3393 85 54 85 30
deleteProviderProfileUnlocked@3586-4042 457 443 234 2
broadcastResetToAllInstances@6109-6147 39 38 39 3
resetState@6151-6229 79 49 45 14
sum of the 12 named constructs 1,275 1,094 915 —

Units of their own off e487cd7b2a49 — single parent printed with each; a unit forked from main tip pays the drift of fact 2, which is why these totals are larger than the PR's own number, and every total below is the deciding extension package:

unit constructs body executable extension charge a+d commit (parent)
1919-U1 3586-4042@async@3586-4042 + 1163-1281@async@1163-1281 + 816-898@async@816-898 382 1,243 +15,018/−5,130 = 20,148 59974e7a9e76 (e487cd7b2a49)
1919-U2 3049-3115@async@3049-3115 + 1088-1155@async@1088-1155 + 2818-2959@async@2818-2959 + 3309-3393@updateProfileModel@3309-3393 + 1025-1082@async@1025-1082 389 1,250 +14,779/−5,130 = 19,909 c0d9ae863bed (e487cd7b2a49)
1919-U3 6151-6229@resetState@6151-6229 + 3124-3164@async@3124-3164 + 6109-6147@broadcastResetToAllInstances@6109-6147 + 1378-1414@removeClineFromStack@1378-1414 144 1,005 +14,555/−5,130 = 19,685 85e34ae25460 (e487cd7b2a49)
residual unit: this file with every named construct gone all 12 915 moved unit 1,802, of which ClineProvider.ts itself 941 file 1751 + 548 vs main tip 0e96af1853ac (e487cd7b2a49)

Plainly, for 1919. 1,629 of the deciding package's 2,216 counted lines live in this file; with every named construct removed the file still charges 941 (over the 500 cap on its own) while the rest of the package adds 587 more, and the unit that would carry the residue off main tip prices at 1,802. Relocating the constructs inside the same PR moves the number the wrong way (2,216 → 2,171 → 2,422). The file's own a+d, 3,330, is past the 1,000 hard reviewability line, so with no-splitting-inside-a-file no unit can carry this file as it stands — and for a PR whose deciding package charges 2,216, no side of a two-way whole-file split can pass while the file producing the charge cannot be cut. The two numbers a maintainer needs to answer split-versus-exclusion for 1919 are 941 for the file and 1,802 for the unit that would carry it.

Where a removal would not compile, and why the price is a union. Nothing here was compiled — a body lifted into a function shell is not a compiling module — so compilability rests on the reference census, which is the honest instrument available at this stage: removeClineFromStack 164, setViewStateId 114, updateProfileModel 30, loadViewState 26, resetState 14, savePersistedViewState 10, handleModeSwitchUnlocked 10, _restoreProfileActivationState 7, _snapshotProviderManagerState 6, rekeyPersistedViewStateEntry 3, broadcastResetToAllInstances 3, deleteProviderProfileUnlocked 2. A construct with hundreds of call sites cannot leave the class without moving or exporting those call sites, so for those the priced unit is the union of construct plus call sites. The rows above carry whole bodies: that is the ceiling on what a compilable cut costs, not a unit somebody has built.

1406 — head c1b051b2786a, ref feat/change-journal-b2, 44 commits, +3,721/−43 over 43 files (open, mergeable true / blocked)

pulls/1406.base.sha = 3ee042d91e0d and merge-base(mainTip, head) = 3ee042d91e0d are the same commit, so this PR's window is unambiguous and main's later movement belongs to drift, not to it. git rev-parse HEAD printed beside the ASKED sha before and after every synthetic commit (it matched the head read from GET /pulls/1406, and the scratch worktree ends clean); lockfile blob 520e42edbb09 (612,369 B). The file at head: blob 550a5937e9cc, 5,481 lines, 4 + 0 = 4 a+d at the merge-base (against main tip: 22 + 24).

measured (package named) value
gate manifest at the merge-base, caps raised types=7, extension 468, webview 21 — deciding package extension
ClineProvider.ts share of the deciding package 3 changed executable lines out of 4 changed lines
the rest of the deciding package 468 − 3 = 465 (under the cap on its own): core/checkpoints/changeJournal.ts 217, core/tools/ApplyPatchTool.ts 110, core/checkpoints/index.ts 100, core/task/Task.ts 17, core/tools/WriteToFileTool.ts 14, core/tools/EditFileTool.ts 7
CI's own verdict for this head no gate sentence: mutation-diff Changed-code mutation testing#8576 success, 151,988 B over 1,937 lines, zero ##[error] lines, 6 advisories, artifacts uploaded
relocating constructs inside the same PR (parent = head, extension) 1 → 513, 4 → 832, baseline 468
deletion-only (parent = head) packages: [] — 0 executable lines in every package

Constructs, named from the declaration line. The first pass over 1557 and 1919 labelled every private async x( member async and reported a repo-wide count of that word (10,732 references for 1919, 10,870 for 1557) as its census, which is not a fact about any construct. The re-scan below reads the declaration; the spans, changed-line counts and executable counts were identical between the two passes, so no priced number is affected — only the names and the censuses.

construct (head line range) span changed at merge-base executable in body references in src
getTaskWithId@2704-2747 44 0 44 197
getTaskWithAggregatedCosts@2749-2761 13 0 13 3
getStateToPostToWebview@3105-3433 329 2 306 71
setValues@3715-3717 3 0 2 57
sum of the 4 named constructs 389 2 365 —

Units of their own off e487cd7b2a49 — single parent printed with each; a unit forked from main tip pays the drift of fact 2, which is why these totals are larger than the PR's own number, and every total below is the deciding extension package:

unit constructs body executable extension charge a+d commit (parent)
1406-U1 2704-2747@getTaskWithId@2704-2747 + 2749-2761@getTaskWithAggregatedCosts@2749-2761 + 3105-3433@getStateToPostToWebview@3105-3433 + 3715-3717@async@3715-3717 365 1,213 +6,112/−13,483 = 19,595 ccfe9df6c687 (e487cd7b2a49)
residual unit: this file with every named construct gone all 4 365 moved unit 850, of which ClineProvider.ts itself 2 file 5 + 396 vs main tip cc9f86498f56 (e487cd7b2a49)

Plainly, for 1406. At its own merge-base this PR changes 4 lines of ClineProvider.ts, 3 executable; the 22 / 24 split is main's own movement arriving as deletions. Its deciding package charges 468 and CI says success, so neither remedy in the refusal sentence applies: this is the family member that barely touches the file, and the residual unit's ClineProvider.ts charges 2 executable lines. What binds 1406 is reviewability — +3,721/−43 = 3,764 a+d over 43 files against a 400 soft line — and what a split would partition here is its own charging files, named above (changeJournal.ts 217, ApplyPatchTool.ts 110, checkpoints/index.ts 100 and the rest). Refreshed onto current main it would also inherit the drift of fact 2 — 382 executable lines of other people's code at the moment of writing.

Where a removal would not compile, and why the price is a union. Nothing here was compiled — a body lifted into a function shell is not a compiling module — so compilability rests on the reference census, which is the honest instrument available at this stage: getTaskWithId 197, getStateToPostToWebview 71, setValues 57, getTaskWithAggregatedCosts 3. A construct with hundreds of call sites cannot leave the class without moving or exporting those call sites, so for those the priced unit is the union of construct plus call sites. The rows above carry whole bodies: that is the ceiling on what a compilable cut costs, not a unit somebody has built.

Reading the five maps together (1565, 1921, 1557, 1919, 1406)

  • Deciding (extension) package charge in each PR's own window: 2,230 (1565), 2,328 (1921), 1,852 (1557), 2,216 (1919), 468 (1406); ClineProvider.ts share of it: 1,371, 1,671, 1,006, 1,629, 3. CI's own gate sentences reproduce the first four exactly — 2,230 at line 1845 of a 147,507 B log, 2,328 at line 1845 of 147,511 B, 1,852 at line 1844 of 147,477 B, 2,216 at line 1845 of 147,620 B — and 1406's leg passes at 468 with advisories.
  • After every named construct is removed, the file itself still charges 750 (1565), 944 (1921), 569 (1557), 941 (1919), 2 (1406), and the residual units price at 1,610, 1,602, 1,527, 1,802, 850 in the deciding package. Four of the five files are over the 500 cap on their own once the constructs are gone; 1406 is the one family PR that never needed the argument.
  • The measurement favours the cheap half of the refusal sentence and prices it: deletion-only diffs charge 0 (packages: [], reproduced on all five heads), so a deletion-dominant re-scope — units whose diffs are removals, delivering that code inside modules earlier units already carry, then the residual modifications cut file by file — can move the constructs out near-free while the residual modifications are what cost. The pointer comments on 1565 and 1921 (issue comments 6752498253 and 6752498829) put exactly that choice in front of maintainers and say no re-scope has been built.
  • Drift is priced rather than feared: the gate takes merge-base(base, head) (gate lines 297-298), so a PR's own number is drift-free, while every unit forked from e487cd7b2a49 pays it — 111 executable lines for 1557, 278 for 1919, 382 for 1406 at the moment of writing.
  • Nothing here was pushed and nothing here changes a mergeability state: 1557, 1919 and 1406 are mergeable true / mergeable_state blocked; two of their mutant legs refuse on size before running a mutant; 1406's passes with advisories.

Pricing instruction for this family, stated as a rule and not a note (f0-1528, task-90)

Price with merge-base semantics, re-root when you synthesise, and report drift separately when you name it. The reason is the gate's own code, blob 7dfe0006d2d5c7e4d2282f65f68b8ed5f16866bc: lines 284-286 carry the comment that GitHub checks out the synthetic pull-request merge commit while pull_request.base.sha is frozen at event-creation time, so a stale base "attributes unrelated upstream lines to the pull request"; resolvePullRequestBase (lines 287-291) returns the head's first parent only when the head has two parents, and selectFromGit (lines 293-298) then computes mergeBase = git merge-base baseSha headSha and diffs mergeBase...headSha. The consequence for anyone pricing by hand:

  • Pricing each family head from the live main tip, from its own merge-base, and from a synthetic merge commit of main tip + head priced against main tip gives the same manifest three times — 1,852 for 1557, 2,216 for 1919, 468 for 1406 (and 2,230 for 1565, 2,328 for 1921) — and each equals the number CI prints in its own gate sentence (1557 line 1844 of a 147,477 B log, 1919 line 1845 of 147,620 B, 1565 line 1845 of 147,507 B, 1921 line 1845 of 147,511 B; 1406 has no sentence because its leg passed).
  • A hand-rolled git diff mainTip head, which does not re-root, prints 1,963 / 2,494 / 864 for the same three heads. That entire gap is main's own movement landing in files the PRs touch — src/scripts/verify-coverage-cache-inputs.mjs, src/core/task/Task.ts and src/api/providers/qwen-code.ts are charging files in those windows and are not charging files at the merge-bases — so the honest figure is drift of 111, 278 and 382 executable lines, named as drift, not mixed into a PR's own charge.
  • Therefore: quote a PR's charge from merge-base(base, head) (or from the synthetic merge commit, which is the same number and is what CI sees); price a synthesised unit against the sha its branch actually forks from and print that parent with the row; and when a main-tip window is used, report the drift separately rather than letting it ride inside the number.
  • One instrument trap belongs with the rule. selectFromGit reads the working tree, not only git objects: re-running it on 1406's residual commit cc9f86498f56 while its scratch worktree sat at the PR head threw ENOENT: no such file or directory on a path under src/core/. A re-run must check the priced commit out first, or it is measuring a different tree. Figures computed per file from git show <commit>:<path> plus executableChangedLines are object-based and immune, which is why the per-file charger tables in this section were built that way.

Activity

  1. easonLiangWorldedtech commented on Nov 8, 2026

    @easonLiangWorldedtech
    ContributorAuthor

    Chain budget record - U3 / PR #1912 is over the executable-line cap: advisory, cap-class (planned-and-issued)

    Gate line, verbatim (mutation-diff job 128607732918, step Enforce executable-line scope and run advisory mutation testing, PR head 1fb5fcc47; re-run at head 085fbf85e4ad):

    Mutation gate failed: extension has 589 changed executable lines (limit 500). Split the PR or obtain a maintainer-reviewed narrow exclusion.
    

    Three things this record exists to make visible:

    1. Cap-class, not a code signal. Nothing is being carried past the gate on a surviving mutant - the run never reached mutation. Its own log warns core Stryker preflight could not start: spawnSync /tmp/stryker-launch-…/node_modul…, which is the known Stryker 10 + @stryker-mutator/vitest-runner runtime-activation defect. The single advisory survivor the job prints, packages/core/src/value.ts:1: Survived BooleanLiteral mutant, is in a file outside this PR's 13-file delta; it is read as an artifact of that failed preflight. That reading is unverified, and deliberately not chased.
    2. The newest commit does not move the number. 085fbf85e adds a JSDoc @internal tag to the primitive export so the production Knip view stops reporting it as an unused export. It is a comment: 589 before it, 589 after it.
    3. A trim is not available inside this unit. 89 executable lines would have to come out of the extension package's delta. Composition of this delta by my own count (added lines from git diff --unified=0 main..HEAD, blank and comment-only lines excluded - 767 for non-test files against the gate's 589, so the gate's metric is narrower and stays the authority): src/services/file-safety/safeWriteText.ts 538, src/utils/safeWriteJson.ts 177, src/services/mcp/McpHub.ts 22, src/core/task/observationRegistry.ts 23, src/core/task/Task.ts 7, and 2,295 across the six test files (safeWriteText.spec.ts alone 1,466). The only blocks large enough to reach 89 are the primitive itself or its evidence. Cutting either is a decision about where the primitive and its evidence live, not a scope trim, and it collides with this unit's seam ruling - main's protocol owns the JSON write mechanism, our path-safety layer grafts onto it, and both feature sets stay in the unit that introduces them. So the split plan already recorded above (U1-U9, one issue per unit: U3 -> Tracking fws U3: observation registry with read completeness #2018) stands as the plan, the advisory stands with it, and if maintainers want feat(task): observation registry with read completeness (U3, #1375) #1912 under the cap the narrow exclusion is the cheap route - which needs a maintainer signature, not an author one.

    Chain-level row to keep next to this one: #1912's src/utils/safeWriteJson.ts now carries main's replacement protocol plus our confinement layer grafted onto it - confinement before any mkdir, lock-key confinement, re-confinement under the lock, the ancestor/dir pins, identity-only re-verification (TargetMovedError, AncestorReplacedError), _copyTargetMode parity, and the POSIX-only best-effort parent-directory fsync that is a logged warning and never a PostCommitDurabilityError out of the JSON path. #1405 (U7) head aa44471e74 resolves that same file at its rows 3 and 7. Whoever lands first should expect the other to re-run the file, not re-decide it. The identity clause this unit depends on is written up on #2356 (comment 6716641767), which extends #2350 and leaves the not-yet-existing-basename half open on purpose.

  2. 18 remaining items

  3. easonLiangWorldedtech commented on Nov 9, 2026

    @easonLiangWorldedtech
    ContributorAuthor

    The seven mutation-red PRs are refusals of the changed-line cap - measured from the failing jobs' own logs

    Correction to a framing this lane has been carrying: 1410, 1411, 1412, 1560, 1562, 1567 and 2226 are red on mutation-diff, and I had been reporting them as mutant-budget refusals rather than width refusals. The failing jobs print the opposite. Read as GET /repos/Zoo-Code-Org/Zoo-Code/commits/<head>/check-runs -> the run's html_url job id -> GET /repos/Zoo-Code-Org/Zoo-Code/actions/jobs/<job id>/logs (base-repository endpoints, logs are plain text):

    PR head at the read job printed failure line stamp
    1410 5e9f608 126045949717 Mutation gate failed: extension has 778 chan 778 ... (limit 500). Split the PR or obtain a maintainer-reviewed narrow exclusion. 2026-11-03T16:37:32Z
    1411 029c25a 126047494028 Mutation gate failed: extension has 675 chan 675 ... (limit 500). Split the PR or obtain a maintainer-reviewed narrow exclusion. 2026-11-03T16:54:41Z
    1412 dd8b4fd 126035662423 Mutation gate failed: extension has 1065 cha 1065 ... (limit 500). Split the PR or obtain a maintainer-reviewed narrow exclusion. 2026-11-03T16:26:24Z
    1560 4dcc7ad 127165332375 Mutation gate failed: extension has 1707 cha 1707 ... (limit 500). Split the PR or obtain a maintainer-reviewed narrow exclusion. 2026-11-05T09:38:56Z
    1562 c762f95 128833307901 Mutation gate failed: extension has 2130 cha 2130 ... (limit 500). Split the PR or obtain a maintainer-reviewed narrow exclusion. 2026-11-08T19:16:14Z
    1567 24dee10 128527443185 Mutation gate failed: extension has 2227 cha 2227 ... (limit 500). Split the PR or obtain a maintainer-reviewed narrow exclusion. 2026-11-07T21:01:45Z
    2226 bc724b8 127165093137 Mutation gate failed: extension has 522 chan 522 ... (limit 500). Split the PR or obtain a maintainer-reviewed narrow exclusion. 2026-11-05T09:37:53Z

    Every one of the seven ends ##[error]Process completed with exit code 1. and every one prints the same predicate with its own number, so at these heads the mutants are not what fails the leg - the counted changed executable lines are (778, 675, 1065, 1707, 2130, 2227, 522 against a limit of 500).

    The gate does carry a second cap, and it is the one that produced the wrong guess: main tip's scripts/stryker-diff.mjs (blob 7dfe0006d2d5c7e4d2282f65f68b8ed5f16866bc) declares MAX_CHANGED_LINES = 500 at line 11 and MAX_MUTANTS = 400 at line 12, and throws from the changed-line test at line 265 (if (packageEntry.changedExecutableLines > MAX_CHANGED_LINES)). What the surviving mutants do in these jobs is surface as advisories: each log carries 4-5 annotations titled Mutation test advisory (1410's is packages/core/src/value.ts:1: Survived BooleanLiteral mutant (replacement: false). See the job summary for the complete list and resolution guidance.), and 4-5 advisory lines against 1 failure line is the shape of this class.

    Why the distinction decides the repair:

    • For a changed-line refusal the admissible moves are to split so a package's changed executable lines fall under 500, or to obtain the maintainer-reviewed narrow exclusion the message itself names. Killing mutants does not move a 2130 or a 2227.
    • For a MAX_MUTANTS refusal, or for the advisory lines above, the admissible moves are the other set: narrow the mutated surface, add the assertion that kills the survivor, or delete the branch when the mutant is equivalent.
    • Naming which cap printed is therefore the triage step, and it is cheap: the reason is one GET .../actions/jobs/<id>/logs away, and it is not in the check-run object (its title is empty and its annotations are only Process completed with exit code 1.).

    Green-side calibration from the same instrument, priced the honest way: 1545's candidate tree re-rooted onto main tip with git commit-tree <merged tree> -p e487cd7b2a49... (so the merge tree is charged against the base tip instead of against a merge commit's first parent) prints changedExecutableLines: 315 with 88 selectors for the extension package, and the identically rooted tree without my new test files prints the same 315 and the same 88 - test-only additions cost 0 counted lines and 0 selectors here. A PR at 315 of 500 has headroom that a 522-line sibling does not, which is what tells you whether a PR can absorb another commit or must be split first.

    Arms state for the same seven heads, read in the same round from base-repo check-runs (never statusCheckRollup, never the fork): check-translations, platform-unit-test (ubuntu-latest), platform-unit-test (windows-latest), compile, knip and e2e-mock are completed/success on all seven, and mutation-diff is the only failing leg. Two things worth correcting there too: no check-run named integration (or matching /integration/i at all) is produced for any of these seven heads or for 1545 at either its old or its new head, so "all six required contexts green" should be read as five green plus one leg that does not run for these PRs; and 1567's mutation-diff run is on page 3 of 3 of that endpoint (202 legs for that head), not page 2 as this lane earlier said.

    Local runs not spent, stated as owed rather than as done: whole-repository pnpm test on 1545 is still outstanding - what was spent there is the two affected suites at the head tree and on a locally built main-tip merge tree (exit 0, 188 tests each), forced uncached root check-types on both trees (11 and 12 tasks, 0 cached), the src package's own tsc --noEmit and lint leg, eslint --max-warnings=0 on the two changed files under main's rule files, and knip. Stryker itself was not run locally for any of these seven: from these Windows worktrees node_modules/.bin/stryker resolves ENOENT and the .cmd shim fails with EINVAL (measured earlier in this campaign by this lane, not re-measured in this round), so every mutant number quoted above comes from CI's logs, not from a local run.

  4. easonLiangWorldedtech commented on Nov 9, 2026

    @easonLiangWorldedtech
    ContributorAuthor

    Correction to two imprecise words in my comment above (6751738910), on the field the reason is not in.

    I wrote that the reason is not in the check-run object because "its title is empty and its annotations are only Process completed with exit code 1.". Re-read against GET /repos/Zoo-Code-Org/Zoo-Code/check-runs/126045949717 for 1410: there is no top-level title, text, summary or annotations_count on a check-run item at all - the fields are output.title, output.summary, output.text (all three printed null here, so nothing in them to quote) and output.annotations_count, which printed 2. GET /check-runs/126045949717/annotations returned those 2: .github:11 level failure Process completed with exit code 1. and .github:1 level notice about the ubuntu-latest image migrating to Ubuntu 26 on 2026-10-19. So the accurate sentence is: the check-run item reports no reason text (its three output prose fields are null) and its 2 annotations are the exit-code line plus a runner-image notice, which is why the printed cap number had to come from GET /actions/jobs/126045949717/logs, where Mutation gate failed: extension has 778 changed executable lines (limit 500)... is a log line at 2026-11-03T16:37:32Z.

    Substance of the comment is unchanged - all seven print the changed-line cap in their logs - and no mutant count is quoted for any of them, because none of them prints one in a field this endpoint returns.

  5. easonLiangWorldedtech commented on Nov 9, 2026

    @easonLiangWorldedtech
    ContributorAuthor

    Width queue for the seven mutation-red PRs - every number below was printed by the command named in the same span

    Class, stated plainly: all seven refusals are width refusals, not mutant-count refusals. Each log prints Mutation gate failed: extension has N changed executable lines (limit 500), and surviving mutants appear only as warnings titled Mutation test advisory. The check-run object carries no reason. A check-run item has no top-level title, text, summary or annotations_count; those live under output, and on 1410's leg output.title, output.summary and output.text were all null while output.annotations_count read 2 (GET /repos/Zoo-Code-Org/Zoo-Code/check-runs/126045949717), its two annotations being .github:11 level failure reading Process completed with exit code 1. and a runner-image notice at .github:1. The job log is the only instrument that names a reason, so every figure below cites a log byte length and a line number. A count read while a leg is running is a partial count; all seven legs here are completed/failure. These rows follow my two earlier comments on this issue, 6751738910 (the class correction) and 6751831118 (the field-path correction).

    How each figure was obtained and cross-checked. GET /pulls/N gave the current head; GET /repos/Zoo-Code-Org/Zoo-Code/commits/<head>/check-runs?per_page=100&filter=all gave the mutation-diff leg and in all seven the leg's head_sha equals the current head, so no row attaches a stale CI number. Each log came from GET /repos/Zoo-Code-Org/Zoo-Code/actions/jobs/<job id>/logs via a raw fetch that does not truncate non-JSON bodies; the bytes received and the line of the quoted sentence are printed per row. Line numbers are identical in the raw text and in the same text with ANSI escapes stripped, because stripping removes inline escapes and never newlines. Every figure was then re-derived locally: git merge-tree --write-tree <base tip> <head> (exit 0 for all seven), then git commit-tree <tree> -p <base tip> to re-root the merged tree to a single parent - said so, because resolvePullRequestBase at lines 287-291 of scripts/stryker-diff.mjs and the workflow's own BASE_SHA line both price a merge-shaped head against the base - then checkout in .tmp-cr-audit/s3lane/wt-width-queue and pricing with that tree's own scripts/stryker-diff.mjs under node 22.23.4, printing git rev-parse HEAD beside the ASKED sha and naming each tree's pnpm-lock.yaml blob (520e42edbb09 at 612,369 B on 1410, 1411, 1412, 1560, 2226; 0fb1d6941590 at 612,370 B on 1562, 1567). MAX_CHANGED_LINES and MAX_MUTANTS were raised only in a private copy so the manifest prints instead of throwing; the unpatched copy threw CI's own figure for all seven.

    PR CI extension lines over 500 by selectors side A: files / a+d / counted side B: files / a+d / counted largest file a+d mutants
    1410 778 278 238 12 / 3,736 / 530 39 / 2,706 / 327 804 under hard no verdict
    1411 675 175 185 15 / 2,349 / 298 42 / 3,592 / 456 850 under hard no verdict
    1412 1065 565 242 45 / 1,851 / 468 49 / 6,905 / 1118 827 under hard no verdict
    1560 1707 1207 343 23 / 2,820 / 379 13 / 12,669 / 1387 4,266 over hard no verdict
    1562 2130 1630 386 23 / 4,718 / 590 13 / 14,407 / 1599 4,880 over hard no verdict
    1567 2227 1727 480 33 / 6,929 / 656 17 / 15,570 / 1671 5,674 over hard no verdict
    2226 522 22 149 15 / 258 / 30 4 / 2,687 / 500 1,649 over hard no verdict

    1410 - current head 5e9f608204f54df3c98ac9c185bbe0590c88b761

    • CI figure: extension = 778 changed executable lines (cap 500), so overage 278 lines, 56 percent over. Citation: GET /repos/Zoo-Code-Org/Zoo-Code/actions/jobs/126045949717/logs, fetched raw with no truncation, receiving 121,483 B across 1,478 lines. The refusal is line 1436, stamped 2026-11-03T16:37:32.0997399Z:

      Mutation gate failed: extension has 778 changed executable lines (limit 500). Split the PR or obtain a maintainer-reviewed narrow exclusion.

    • Log current: the mutation-diff check-run head_sha equals 5e9f608204f54df3c98ac9c185bbe0590c88b761, the head GET /pulls/1410 reports now, so the refusal belongs to this head. The commit CI priced is HEAD_SHA = d96122032a10835dd12fdfe48ffd91e761880f39 (log line 1434). It is no longer today's refs/pull/1410/merge (now 5ee0e05eb32c85382147bdc0b21c209e6eef7ae8, rebuilt against main e487cd7), so the number is re-derived locally below. Both sides come from BASE_SHA="$(git rev-parse "$HEAD_SHA^1")" (log line 1427), i.e. GitHub prices the PR-merge commit against the base-branch tip. This log does not print PR_HEAD_SHA. Note pulls/1410.base.sha is 3ee042d91e0da702d7539eab7dd3e377bba73190 while GET /branches/main reports e487cd7b2a497aa1d9b60f9b0cb497f7839bb629: the recorded base is frozen at event-creation time, which is the drift the gate's own comment describes.
    • Locally re-derived: merge-tree --write-tree of that base tip and this head exited 0 with tree d57194ffb82de8251a7cde3190110fe64661a516; commit-tree -p 3ee042d91e0da702d7539eab7dd3e377bba73190 re-rooted it to a single parent (said so) as d4d862145759ce632f87bba8d22d98209189c0b5; that tree was checked out and priced with its own scripts/stryker-diff.mjs (blob 06d06fb7ac923880dbb01ef355aa8adc09b91e39), with only the two MAX_* constants raised in a private copy so the manifest prints instead of throwing. The unpatched copy throws extension has 778 changed executable lines (limit 500), the same number CI printed. Counted selectors for extension: 238.
    • Mutants: no verdict exists for this PR either way. Log line 1038 prints core Stryker preflight could not start: spawnSync /tmp/stryker-launch-WmMlZT/node_modules/.bin/stryker ENOENT. The 4 lines titled Mutation test advisory sit at 1038, 1039, 1040, 1041: the preflight ENOENT, one Survived BooleanLiteral mutant advisory at packages/core/src/value.ts:1, a bare advisory line, and a Could not write the job summary: EISDIR ... line. No mutant count is printed anywhere in the log, so nothing is quotable against the 400-mutant cap.
    • First cut, whole files only, nothing split inside a file, both sides priced against 3ee042d91e0da702d7539eab7dd3e377bba73190 (the fork point a unit branch of this PR would use). Side A = checkpoint journal + rollback + shadow service (source and their specs): 12 files, 3,736 a+d, gate-counted 530 (over 500 by 30). Side B = the rest: 39 files, 2,706 a+d, gate-counted 327 (under 500). The PR as a whole is 6,442 a+d over 51 files, and the largest single file, src/core/tools/__tests__/applyPatchTool.execute.spec.ts, is 804 a+d which is under the hard line, so whole-file cutting is viable for this PR. At the 400 a+d soft line neither side is small enough on its own, so each needs its body rationale and, where marked, a further cut.

    1411 - current head 029c25ac314aa3fbc6997dd4fe82797375169f6f

    • CI figure: extension = 675 changed executable lines (cap 500), so overage 175 lines, 35 percent over. Citation: GET /repos/Zoo-Code-Org/Zoo-Code/actions/jobs/126047494028/logs, fetched raw with no truncation, receiving 126,075 B across 1,549 lines. The refusal is line 1507, stamped 2026-11-03T16:54:41.3616472Z:

      Mutation gate failed: extension has 675 changed executable lines (limit 500). Split the PR or obtain a maintainer-reviewed narrow exclusion.

    • Log current: the mutation-diff check-run head_sha equals 029c25ac314aa3fbc6997dd4fe82797375169f6f, the head GET /pulls/1411 reports now, so the refusal belongs to this head. The commit CI priced is HEAD_SHA = 62c7b4f21e7ee57266643ac4221356e98a7f3fe5 (log line 1505). It is no longer today's refs/pull/1411/merge (now 049beae3caf49d2c6fe54b5bd674459806338888, rebuilt against main e487cd7), so the number is re-derived locally below. Both sides come from BASE_SHA="$(git rev-parse "$HEAD_SHA^1")" (log line 1498), i.e. GitHub prices the PR-merge commit against the base-branch tip. This log does not print PR_HEAD_SHA. Note pulls/1411.base.sha is 3ee042d91e0da702d7539eab7dd3e377bba73190 while GET /branches/main reports e487cd7b2a497aa1d9b60f9b0cb497f7839bb629: the recorded base is frozen at event-creation time, which is the drift the gate's own comment describes.
    • Locally re-derived: merge-tree --write-tree of that base tip and this head exited 0 with tree f3adeefd5fea4e2a3cb48a744105a53966057a94; commit-tree -p 3ee042d91e0da702d7539eab7dd3e377bba73190 re-rooted it to a single parent (said so) as 84f35216fa63093f77f494267f804a7f2653be46; that tree was checked out and priced with its own scripts/stryker-diff.mjs (blob 06d06fb7ac923880dbb01ef355aa8adc09b91e39), with only the two MAX_* constants raised in a private copy so the manifest prints instead of throwing. The unpatched copy throws extension has 675 changed executable lines (limit 500), the same number CI printed. Counted selectors for extension: 185.
    • Mutants: no verdict exists for this PR either way. Log line 1109 prints core Stryker preflight could not start: spawnSync /tmp/stryker-launch-fN5w76/node_modules/.bin/stryker ENOENT. The 4 lines titled Mutation test advisory sit at 1109, 1110, 1111, 1112: the preflight ENOENT, one Survived BooleanLiteral mutant advisory at packages/core/src/value.ts:1, a bare advisory line, and a Could not write the job summary: EISDIR ... line. No mutant count is printed anywhere in the log, so nothing is quotable against the 400-mutant cap.
    • First cut, whole files only, nothing split inside a file, both sides priced against 3ee042d91e0da702d7539eab7dd3e377bba73190 (the fork point a unit branch of this PR would use). Side A = the tool-side change-card surface (tools source and their specs): 15 files, 2,349 a+d, gate-counted 298 (under 500). Side B = the rest: 42 files, 3,592 a+d, gate-counted 456 (under 500). The PR as a whole is 5,941 a+d over 57 files, and the largest single file, src/core/tools/__tests__/applyPatchTool.execute.spec.ts, is 850 a+d which is under the hard line, so whole-file cutting is viable for this PR. At the 400 a+d soft line neither side is small enough on its own, so each needs its body rationale and, where marked, a further cut.

    1412 - current head dd8b4fd6a730f2edd8e501bea1f19fc1839c4a21

    • CI figure: extension = 1065 changed executable lines (cap 500), so overage 565 lines, 113 percent over. Citation: GET /repos/Zoo-Code-Org/Zoo-Code/actions/jobs/126035662423/logs, fetched raw with no truncation, receiving 121,500 B across 1,478 lines. The refusal is line 1436, stamped 2026-11-03T16:26:24.0986992Z:

      Mutation gate failed: extension has 1065 changed executable lines (limit 500). Split the PR or obtain a maintainer-reviewed narrow exclusion.

    • Log current: the mutation-diff check-run head_sha equals dd8b4fd6a730f2edd8e501bea1f19fc1839c4a21, the head GET /pulls/1412 reports now, so the refusal belongs to this head. The commit CI priced is HEAD_SHA = 5c95c57c9428e71159210ed6d4b31ec701fa8c26 (log line 1434). It is no longer today's refs/pull/1412/merge (now f5827d1c92e302a82fd7430c6fd0840b3b4aba77, rebuilt against main e487cd7), so the number is re-derived locally below. Both sides come from BASE_SHA="$(git rev-parse "$HEAD_SHA^1")" (log line 1427), i.e. GitHub prices the PR-merge commit against the base-branch tip. This log does not print PR_HEAD_SHA. Note pulls/1412.base.sha is 3ee042d91e0da702d7539eab7dd3e377bba73190 while GET /branches/main reports e487cd7b2a497aa1d9b60f9b0cb497f7839bb629: the recorded base is frozen at event-creation time, which is the drift the gate's own comment describes.
    • Locally re-derived: merge-tree --write-tree of that base tip and this head exited 0 with tree 32d456184065462af9cd5426c1c7a53dad584b9c; commit-tree -p 3ee042d91e0da702d7539eab7dd3e377bba73190 re-rooted it to a single parent (said so) as ab41f57677098c3ebc37159c6f8293de2b95b655; that tree was checked out and priced with its own scripts/stryker-diff.mjs (blob 06d06fb7ac923880dbb01ef355aa8adc09b91e39), with only the two MAX_* constants raised in a private copy so the manifest prints instead of throwing. The unpatched copy throws extension has 1065 changed executable lines (limit 500), the same number CI printed. Counted selectors for extension: 242. The head commit itself is a merge, so pricing the raw head triggers the script's first-parent substitution: 342 counted lines at first parent 1168643 against 1065 at the base tip.
    • Mutants: no verdict exists for this PR either way. Log line 1038 prints core Stryker preflight could not start: spawnSync /tmp/stryker-launch-WY7N8V/node_modules/.bin/stryker ENOENT. The 4 lines titled Mutation test advisory sit at 1038, 1039, 1040, 1041: the preflight ENOENT, one Survived BooleanLiteral mutant advisory at packages/core/src/value.ts:1, a bare advisory line, and a Could not write the job summary: EISDIR ... line. No mutant count is printed anywhere in the log, so nothing is quotable against the 400-mutant cap.
    • First cut, whole files only, nothing split inside a file, both sides priced against 3ee042d91e0da702d7539eab7dd3e377bba73190 (the fork point a unit branch of this PR would use). Side A = the webview change-card component, its spec and the i18n copy: 45 files, 1,851 a+d, gate-counted 468 (under 500). Side B = the rest: 49 files, 6,905 a+d, gate-counted 1118 (over 500 by 565). The PR as a whole is 8,756 a+d over 94 files, and the largest single file, src/core/tools/__tests__/applyPatchTool.execute.spec.ts, is 827 a+d which is under the hard line, so whole-file cutting is viable for this PR. At the 400 a+d soft line neither side is small enough on its own, so each needs its body rationale and, where marked, a further cut.

    1560 - current head 4dcc7ad769120f582a52272aa1b9613ae2264013

    • CI figure: extension = 1707 changed executable lines (cap 500), so overage 1207 lines, 241 percent over. Citation: GET /repos/Zoo-Code-Org/Zoo-Code/actions/jobs/127165332375/logs, fetched raw with no truncation, receiving 151,740 B across 1,953 lines. The refusal is line 1911, stamped 2026-11-05T09:38:56.0676742Z:

      Mutation gate failed: extension has 1707 changed executable lines (limit 500). Split the PR or obtain a maintainer-reviewed narrow exclusion.

    • Log current: the mutation-diff check-run head_sha equals 4dcc7ad769120f582a52272aa1b9613ae2264013, the head GET /pulls/1560 reports now, so the refusal belongs to this head. The commit CI priced is HEAD_SHA = e18b36c13198550608c93daff6837f2e8cf303d9 (log line 1908). It is no longer today's refs/pull/1560/merge (now f74e0ab8500a65bc4505b40e096ae36a281bc6d2, rebuilt against main e487cd7), so the number is re-derived locally below. Both sides come from BASE_SHA="$(git rev-parse "$HEAD_SHA^1")" (log line 1901), i.e. GitHub prices the PR-merge commit against the base-branch tip. PR_HEAD_SHA = 4dcc7ad769120f582a52272aa1b9613ae2264013 (line 1909) equals the PR head. Note pulls/1560.base.sha is e943530c848624bd3054c960be1e4fb0b658509a while GET /branches/main reports e487cd7b2a497aa1d9b60f9b0cb497f7839bb629: the recorded base is frozen at event-creation time, which is the drift the gate's own comment describes.
    • Locally re-derived: merge-tree --write-tree of that base tip and this head exited 0 with tree a9877a3a952c6084b9f4dc73686469ff0707631a; commit-tree -p e943530c848624bd3054c960be1e4fb0b658509a re-rooted it to a single parent (said so) as 8c795cfe2709256ba5afca9c0bcd734db3b426e0; that tree was checked out and priced with its own scripts/stryker-diff.mjs (blob 7dfe0006d2d5c7e4d2282f65f68b8ed5f16866bc), with only the two MAX_* constants raised in a private copy so the manifest prints instead of throwing. The unpatched copy throws extension has 1707 changed executable lines (limit 500), the same number CI printed. Counted selectors for extension: 343.
    • Mutants: no verdict exists for this PR either way. Log line 1104 prints core Stryker preflight could not start: spawnSync /tmp/stryker-launch-ywviJO/node_modules/.bin/stryker ENOENT. The 5 lines titled Mutation test advisory sit at 1104, 1105, 1106, 1107, 1108: the preflight ENOENT, one Survived BooleanLiteral mutant advisory at packages/core/src/value.ts:1, a bare advisory line, and a Could not write the job summary: EISDIR ... line, plus a manifest advisory line. No mutant count is printed anywhere in the log, so nothing is quotable against the 400-mutant cap.
    • First cut, whole files only, nothing split inside a file, both sides priced against e943530c848624bd3054c960be1e4fb0b658509a (the fork point a unit branch of this PR would use). Side A = schema, config storage and extension-side readers/writers with their specs: 23 files, 2,820 a+d, gate-counted 379 (under 500). Side B = the rest: 13 files, 12,669 a+d, gate-counted 1387 (over 500 by 842). The PR as a whole is 15,489 a+d over 36 files, and the largest single file, src/core/webview/__tests__/ClineProvider.spec.ts, is 4,266 a+d which is over the 1,000-line hard reviewability line by itself, so no whole-file boundary can satisfy the hard line and that spec must have its scope split too. At the 400 a+d soft line neither side is small enough on its own, so each needs its body rationale and, where marked, a further cut.

    1562 - current head c762f952bb1486c0bdb6d290aa660aed00cea49c

    • CI figure: extension = 2130 changed executable lines (cap 500), so overage 1630 lines, 326 percent over. Citation: GET /repos/Zoo-Code-Org/Zoo-Code/actions/jobs/128833307901/logs, fetched raw with no truncation, receiving 147,387 B across 1,885 lines. The refusal is line 1843, stamped 2026-11-08T19:16:14.1478533Z:

      Mutation gate failed: extension has 2130 changed executable lines (limit 500). Split the PR or obtain a maintainer-reviewed narrow exclusion.

    • Log current: the mutation-diff check-run head_sha equals c762f952bb1486c0bdb6d290aa660aed00cea49c, the head GET /pulls/1562 reports now, so the refusal belongs to this head. The commit CI priced is HEAD_SHA = fb5817c091a4ecd431070d22d312175cd5f00fb4 (log line 1840). That commit is still today's refs/pull/1562/merge. Both sides come from BASE_SHA="$(git rev-parse "$HEAD_SHA^1")" (log line 1833), i.e. GitHub prices the PR-merge commit against the base-branch tip. PR_HEAD_SHA = c762f952bb1486c0bdb6d290aa660aed00cea49c (line 1841) equals the PR head. Note pulls/1562.base.sha is 73931fe8be253b93ec11103a0672c615e849f895 while GET /branches/main reports e487cd7b2a497aa1d9b60f9b0cb497f7839bb629: the recorded base is frozen at event-creation time, which is the drift the gate's own comment describes.
    • Locally re-derived: merge-tree --write-tree of that base tip and this head exited 0 with tree dd5c881c6a63168fe219ec6345ef2ff07c0d043d; commit-tree -p 73931fe8be253b93ec11103a0672c615e849f895 re-rooted it to a single parent (said so) as 7548534bfcb5f7b2a3119fa4bd8a61b18e41b8a5; that tree was checked out and priced with its own scripts/stryker-diff.mjs (blob 7dfe0006d2d5c7e4d2282f65f68b8ed5f16866bc), with only the two MAX_* constants raised in a private copy so the manifest prints instead of throwing. The unpatched copy throws extension has 2130 changed executable lines (limit 500), the same number CI printed. Counted selectors for extension: 386.
    • Mutants: no verdict exists for this PR either way. Log line 1036 prints core Stryker preflight could not start: spawnSync /tmp/stryker-launch-toOHaE/node_modules/.bin/stryker ENOENT. The 5 lines titled Mutation test advisory sit at 1036, 1037, 1038, 1039, 1040: the preflight ENOENT, one Survived BooleanLiteral mutant advisory at packages/core/src/value.ts:1, a bare advisory line, and a Could not write the job summary: EISDIR ... line, plus a manifest advisory line. No mutant count is printed anywhere in the log, so nothing is quotable against the 400-mutant cap.
    • First cut, whole files only, nothing split inside a file, both sides priced against 73931fe8be253b93ec11103a0672c615e849f895 (the fork point a unit branch of this PR would use). Side A = schema, config storage and extension-side readers/writers with their specs: 23 files, 4,718 a+d, gate-counted 590 (over 500 by 76). Side B = the rest: 13 files, 14,407 a+d, gate-counted 1599 (over 500 by 1054). The PR as a whole is 19,125 a+d over 36 files, and the largest single file, src/core/webview/__tests__/ClineProvider.spec.ts, is 4,880 a+d which is over the 1,000-line hard reviewability line by itself, so no whole-file boundary can satisfy the hard line and that spec must have its scope split too. At the 400 a+d soft line neither side is small enough on its own, so each needs its body rationale and, where marked, a further cut.

    1567 - current head 24dee10cc18eadb56268ff3c0086e87d8d7c77a7

    • CI figure: extension = 2227 changed executable lines (cap 500), so overage 1727 lines, 345 percent over. Citation: GET /repos/Zoo-Code-Org/Zoo-Code/actions/jobs/128527443185/logs, fetched raw with no truncation, receiving 151,419 B across 1,950 lines. The refusal is line 1908, stamped 2026-11-07T21:01:45.7859133Z:

      Mutation gate failed: extension has 2227 changed executable lines (limit 500). Split the PR or obtain a maintainer-reviewed narrow exclusion.

    • Log current: the mutation-diff check-run head_sha equals 24dee10cc18eadb56268ff3c0086e87d8d7c77a7, the head GET /pulls/1567 reports now, so the refusal belongs to this head. The commit CI priced is HEAD_SHA = 43f0541772ec7bb8de9fbcd8362f225d601fc991 (log line 1905). It is no longer today's refs/pull/1567/merge (now 01d4a14d97af9e2833deac578ef66e542d4578d1, rebuilt against main e487cd7), so the number is re-derived locally below. Both sides come from BASE_SHA="$(git rev-parse "$HEAD_SHA^1")" (log line 1898), i.e. GitHub prices the PR-merge commit against the base-branch tip. PR_HEAD_SHA = 24dee10cc18eadb56268ff3c0086e87d8d7c77a7 (line 1906) equals the PR head. Note pulls/1567.base.sha is 73931fe8be253b93ec11103a0672c615e849f895 while GET /branches/main reports e487cd7b2a497aa1d9b60f9b0cb497f7839bb629: the recorded base is frozen at event-creation time, which is the drift the gate's own comment describes.
    • Locally re-derived: merge-tree --write-tree of that base tip and this head exited 0 with tree cc5091b1ca620b176d600fe3f739a177aeaf1058; commit-tree -p 73931fe8be253b93ec11103a0672c615e849f895 re-rooted it to a single parent (said so) as 58122afd9d27bfb05f590e52bbb1a270320520e8; that tree was checked out and priced with its own scripts/stryker-diff.mjs (blob 7dfe0006d2d5c7e4d2282f65f68b8ed5f16866bc), with only the two MAX_* constants raised in a private copy so the manifest prints instead of throwing. The unpatched copy throws extension has 2227 changed executable lines (limit 500), the same number CI printed. Counted selectors for extension: 480. The head commit itself is a merge, so pricing the raw head triggers the script's first-parent substitution: 411 counted lines at first parent 1fa4e5a against 2227 at the base tip.
    • Mutants: no verdict exists for this PR either way. Log line 1101 prints core Stryker preflight could not start: spawnSync /tmp/stryker-launch-P9NNJJ/node_modules/.bin/stryker ENOENT. The 5 lines titled Mutation test advisory sit at 1101, 1102, 1103, 1104, 1105: the preflight ENOENT, one Survived BooleanLiteral mutant advisory at packages/core/src/value.ts:1, a bare advisory line, and a Could not write the job summary: EISDIR ... line, plus a manifest advisory line. No mutant count is printed anywhere in the log, so nothing is quotable against the 400-mutant cap.
    • First cut, whole files only, nothing split inside a file, both sides priced against 73931fe8be253b93ec11103a0672c615e849f895 (the fork point a unit branch of this PR would use). Side A = schema, config storage, extension api and activate wiring with their specs: 33 files, 6,929 a+d, gate-counted 656 (over 500 by 128). Side B = the rest: 17 files, 15,570 a+d, gate-counted 1671 (over 500 by 1099). The PR as a whole is 22,499 a+d over 50 files, and the largest single file, src/core/webview/__tests__/ClineProvider.spec.ts, is 5,674 a+d which is over the 1,000-line hard reviewability line by itself, so no whole-file boundary can satisfy the hard line and that spec must have its scope split too. At the 400 a+d soft line neither side is small enough on its own, so each needs its body rationale and, where marked, a further cut.

    2226 - current head bc724b8df51f6d5585ffbac14ada09809d99a5f0

    • CI figure: extension = 522 changed executable lines (cap 500), so overage 22 lines, 4 percent over. Citation: GET /repos/Zoo-Code-Org/Zoo-Code/actions/jobs/127165093137/logs, fetched raw with no truncation, receiving 147,532 B across 1,886 lines. The refusal is line 1844, stamped 2026-11-05T09:37:53.1235958Z:

      Mutation gate failed: extension has 522 changed executable lines (limit 500). Split the PR or obtain a maintainer-reviewed narrow exclusion.

    • Log current: the mutation-diff check-run head_sha equals bc724b8df51f6d5585ffbac14ada09809d99a5f0, the head GET /pulls/2226 reports now, so the refusal belongs to this head. The commit CI priced is HEAD_SHA = 78a145eb6a50ace6f0a12a6600479d845c9f4331 (log line 1841). It is no longer today's refs/pull/2226/merge (now 062dbb5bc8240cbaaed5bb03431060d804d2b835, rebuilt against main e487cd7), so the number is re-derived locally below. Both sides come from BASE_SHA="$(git rev-parse "$HEAD_SHA^1")" (log line 1834), i.e. GitHub prices the PR-merge commit against the base-branch tip. PR_HEAD_SHA = bc724b8df51f6d5585ffbac14ada09809d99a5f0 (line 1842) equals the PR head. Note pulls/2226.base.sha is e943530c848624bd3054c960be1e4fb0b658509a while GET /branches/main reports e487cd7b2a497aa1d9b60f9b0cb497f7839bb629: the recorded base is frozen at event-creation time, which is the drift the gate's own comment describes.
    • Locally re-derived: merge-tree --write-tree of that base tip and this head exited 0 with tree 68bb09184796c6935505c6cdcb8a598c16d51709; commit-tree -p e943530c848624bd3054c960be1e4fb0b658509a re-rooted it to a single parent (said so) as 4bde3217ecb2255982ad98ef824b1c5a7c07bd84; that tree was checked out and priced with its own scripts/stryker-diff.mjs (blob 7dfe0006d2d5c7e4d2282f65f68b8ed5f16866bc), with only the two MAX_* constants raised in a private copy so the manifest prints instead of throwing. The unpatched copy throws extension has 522 changed executable lines (limit 500), the same number CI printed. Counted selectors for extension: 149.
    • Mutants: no verdict exists for this PR either way. Log line 1037 prints core Stryker preflight could not start: spawnSync /tmp/stryker-launch-6pfPze/node_modules/.bin/stryker ENOENT. The 5 lines titled Mutation test advisory sit at 1037, 1038, 1039, 1040, 1041: the preflight ENOENT, one Survived BooleanLiteral mutant advisory at packages/core/src/value.ts:1, a bare advisory line, and a Could not write the job summary: EISDIR ... line, plus a manifest advisory line. No mutant count is printed anywhere in the log, so nothing is quotable against the 400-mutant cap.
    • First cut, whole files only, nothing split inside a file, both sides priced against e943530c848624bd3054c960be1e4fb0b658509a (the fork point a unit branch of this PR would use). Side A = types schema, config storage and extension-side plumbing with their specs: 15 files, 258 a+d, gate-counted 30 (under 500). Side B = the rest: 4 files, 2,687 a+d, gate-counted 500 (under 500). The PR as a whole is 2,945 a+d over 19 files, and the largest single file, src/core/webview/__tests__/ClineProvider.spec.ts, is 1,649 a+d which is over the 1,000-line hard reviewability line by itself, so no whole-file boundary can satisfy the hard line and that spec must have its scope split too. At the 400 a+d soft line neither side is small enough on its own, so each needs its body rationale and, where marked, a further cut.

    The 400-mutant cap is unmeasurable on all seven for one reason: each log prints the core Stryker preflight failing to launch - spawnSync /tmp/stryker-launch-XXXXXX/node_modules/.bin/stryker ENOENT at line 1038 (1410), 1109 (1411), 1038 (1412), 1104 (1560), 1036 (1562), 1101 (1567) and 1037 (2226) - so no mutant run produced a count. The gate refused each PR on width while the preflight never started, and no mutant verdict exists for any of them either way. The lines titled Mutation test advisory are annotation-format warnings (4 per log on 1410, 1411 and 1412; 5 on 1560, 1562, 1567 and 2226) and no mutant count is quoted anywhere in this comment.

    Consequence for the boundaries. Each boundary above is a whole-file assignment - no file appears on both sides, and both sides were priced against the same base the PR itself forks from. For 1560, 1562, 1567 and 2226 the largest file is src/core/webview/__tests__/ClineProvider.spec.ts at 4,266, 4,880, 5,674 and 1,649 a+d, over the 1,000-line hard reviewability line on its own, so no whole-file boundary can satisfy the hard line for those four: the plan has to divide that spec's scope per unit, not just reassign files. For 1410, 1411 and 1412 the largest files are 804, 850 and 827 a+d, so whole-file cutting is viable, but the totals of 6,442, 5,941 and 8,756 a+d mean at least 17, 15 and 22 units at the 400 soft line (6442/400 = 16.11, 5941/400 = 14.85, 8756/400 = 21.89, each rounded up), each needing its rationale in the PR body. Two sides above are still over the 500-line gate cap after the first cut - 1410 side A at 530 counted lines, 1412 side B at 1,065, and all of 1560 side B, 1562 both sides and 1567 both sides - so those need a second cut or a maintainer-reviewed narrow exclusion. 2226 side B lands on exactly 500 counted lines and passes only because the gate throws on > 500 (scripts/stryker-diff.mjs:265), which is a thin margin to ship on. Per the planned-and-issued rule, none of these boundaries becomes a PR until it is posted and accepted on this issue.

    Ruleset facts re-read from GET /repos/Zoo-Code-Org/Zoo-Code/rulesets/15494382 (Protect Main, enforcement active): the required contexts sit in rules[4] of type required_status_checks under parameters.required_status_checks - exactly six, check-translations, platform-unit-test (ubuntu-latest), platform-unit-test (windows-latest), compile, knip, e2e-mock, all at integration_id 15368 - while json.parameters.required_status_checks at the top level returns null silently, which is how a zero-context read gets produced. rules[3] of type pull_request reports dismiss_stale_reviews_on_push: true, so a push dismisses stale approvals and a push after a human approval is only defensible when that push answers that reviewer; require_code_owner_review: true; require_extra_approval_for_unattributed_changes: true, the documented shape of the extra approval nobody has attributed; require_last_push_approval: false; required_approving_review_count: 1; and review-thread resolution is not required, so unresolved threads do not by themselves block a merge - the blocker is the CHANGES_REQUESTED verdict, which is why answering rows matters and resolving threads is courtesy. rules[5] is the merge queue: merge_method SQUASH, grouping_strategy ALLGREEN, min_entries_to_merge 1, max_entries_to_merge 5, max_entries_to_build 5, min_entries_to_merge_wait_minutes 5, check_response_timeout_minutes 60; current_user_can_bypass reads "never".

    The pairing that has to travel with these numbers: at 1545's head db1bc0901f41cc41ad495320f43e4b839a1c2fe6, check-runs?per_page=100&filter=all returned 36 legs - 34 completed/success, 1 completed/skipped and 1 in_progress, the last two both the CodeRabbit leg - with all six required contexts completed/success and mutation-diff completed/success, while GraphQL still reports reviewDecision CHANGES_REQUESTED on decisive row 5818166199. Arms state and review verdict are separate instruments: a green arms read does not imply clearance, and neither does a resolved thread flag or a bot's prose.

    Deferrals carried unchanged: whole-repository pnpm test on 1545 is still owed and not done - what was spent there is the two affected suites at the head tree and on the main-tip merge tree (exit 0, 188 tests each), forced uncached root check-types on both trees, the src package's own tsc --noEmit and lint leg, eslint on the changed files under main's own rule files, prettier and knip - and a maintainer asking for the full run is what changes that. Stryker still cannot run locally from this host (.bin/stryker ENOENT, the .cmd shim EINVAL), so every mutant and advisory line quoted here is CI's. webview-ui/playwright/run-docker.mjs cannot run from a Windows-pnpm host tree, so no committed visual baseline was regenerated. The four-run knip protocol is still owed until a cut actually touches knip.json. Review re-request is unavailable to this token.

    Nothing was pushed for this queue and no review state was changed by it. - s3-1395

  6. easonLiangWorldedtech commented on Nov 9, 2026

    @easonLiangWorldedtech
    ContributorAuthor

    Corrected in place (pass 3): this body supersedes its own version of 23:17:31Z — the queue-wide row for 1560, 1562, 1567, 2226, 1410, 1411 and 1412 - s3-1395

    Corrected in place: what this body supersedes

    It replaces comment 6752571370 as it stood at 2026-11-09T23:17:31Z — 15,468 B, sha256-16 58448e651f7f9360 — which in turn replaced the 2026-11-09T21:59:03Z body at 9,678 B (sha256-16 c4d74bc891974c31), quoted below. What changed in this pass: main moved three times (e487cd7b2a49… → ba105e65cc74… → d3d9cd48e630… → 332d30fbb9d8…), so every width below names the tip it was priced at; the CI column was re-verified at the new tip and did not move, while the recorded-frozen and drift figures did move and are printed as old → new; the collision and mergeable facts from f0-1528's collision pass — comment 6754338284 (2,880 B, sha256-16 601210e554d8ae31, 2026-11-09T23:31:44Z; data .tmp-cr-audit/.tmp-task98-summary.md, 38,509 B, sha256-16 6daf784c4a75f27e, 395 lines) are folded in and re-measured in my own worktree, together with the per-PR intersection against main's 45 new paths; and 1410, 1411 and 1412 are re-described, because main's newest landing already conflicts with them. No comment is added: this is a PATCH of the row you are reading. The earlier correction quoted below stands as written in the version being replaced.

    1560, 1562, 1567 and 2226 - I am asking for the narrow exclusion, and here is why no whole-file boundary exists. T

    1560 / 1562 / 1567: a narrow exclusion for src/core/webview/__tests__/ClineProvider.spec.ts, or acceptance of a 39 / 48 / 57-unit split. 2226: a narrow exclusion for src/core/webview/__tests__/ClineProvider.spec.ts,

    Both asked for an exclusion of src/core/webview/__tests__/ClineProvider.spec.ts. That ask could not have moved a gate number, because the gate never prices that path; the mechanism is printed in the corrected row and in each PR's pointer comment. The two rows now speak about the file the gate does price, and the ask is published as a choice set rather than a single remedy. Every other line of this comment - the seven-head arms rows with filter=all named, the per-side deciding-package corrections, the 2226 boundary analysis, the selector counts, the delivered log byte lengths and the debts - is reprinted byte-for-byte from the superseded body.

    The mutation-diff leg prints the same sentence on all seven PRs apart from the number, and the sentence itself names the two remedies:

    Mutation gate failed: extension has N changed executable lines (limit 500). Split the PR or obtain a maintainer-reviewed narrow exclusion.

    Every number below was printed by the command named beside it in this pass. All 14 family heads were re-fetched from refs/pull/N/head and asserted equal to the heads GET /repos/Zoo-Code-Org/Zoo-Code/pulls/N returned at 00:15:08Z: none of the 14 moved, so every figure here is still about the same commits. What moved underneath them is main: e487cd7b2a49… → ba105e65cc74… (fix(deps): update ai sdks and providers (#2276), renovate, 4 files) → d3d9cd48e630… (feat(api): abort signal support for requesty …, 7 paths) → 332d30fbb9d8… (feat(settings): dynamic thinking effort experimental toggle (DTE-1) (#1521), 33 paths, 45 paths and 3 commits since e487cd7b2a49… in total, ancestry confirmed by git merge-base --is-ancestor exit 0 at each step, in .tmp-cr-audit/s3lane/wt-task100-basecheck); scripts/stryker-diff.mjs is unchanged at all four (blob 7dfe0006d2d5c7e4d2282f65f68b8ed5f16866bc, 30,108 B, sha256-16 c332971815aecfaf, printed from each tip's tree). The CI column below was re-verified at 332d30fbb9d8…: every number in it is unchanged.

    PR CI counted lines, cap 500 measured a+d / files units at 400 a+d soft side A: files / a+d / deciding package and its counted lines side B: same shape largest single file a+d
    1410 778 (+278) 6,442 / 51 17 (16.11) 12 / 3,736 / 530 extension 30 over 39 / 2,706 / 248 extension 252 under 804 under hard
    1411 675 (+175) 5,941 / 57 15 (14.85) 15 / 2,349 / 298 extension 202 under 42 / 3,592 / 377 extension 123 under 850 under hard
    1412 1065 (+565) 8,756 / 94 22 (21.89) 45 / 1,851 / 468 webview 32 under 49 / 6,905 / 1,065 extension 565 over 827 under hard
    1560 1707 (+1207) 15,489 / 36 39 (38.72) 23 / 2,820 / 365 extension 135 under 13 / 12,669 / 1,342 extension 842 over 4,266 over hard
    1562 2130 (+1630) 19,125 / 36 48 (47.81) 23 / 4,718 / 576 extension 76 over 13 / 14,407 / 1,554 extension 1054 over 4,880 over hard
    1567 2227 (+1727) 22,499 / 50 57 (56.25) 33 / 6,929 / 628 extension 128 over 17 / 15,570 / 1,599 extension 1099 over 5,674 over hard
    2226 522 (+22) 2,945 / 19 8 (7.36) 15 / 258 / 22 extension 478 under 4 / 2,687 / 500 extension exactly at 500 1,649 over hard

    CI column re-verified at 332d30fbb9d8… before this PATCH (.tmp-cr-audit/s3lane/task103-twins2.json, 17,847 B, sha256-16 436a8511f6d78cf5; twin per PR, worktree wt-task100-basecheck checked out to each twin, git rev-parse HEAD printed beside each figure): 778, 675, 1065, 1707, 2130, 2227 and 522 all reproduce exactly, and the caps-intact run refuses at the same seven numbers. The a+d and file columns are measured against each PR's own recorded base and did not move either, because no head moved. What did move is the recorded-frozen framing: see the drift row at the end of this body.

    Read the deciding package, not a total: scripts/stryker-diff.mjs:265 throws per package, so the gate outcome is the largest single package on that side and not the sum across types, extension and webview. Where my earlier queue rows printed one 'counted' figure per side, that figure was the sum; the sums and the deciding values differ on 1410 side B (sum 327, deciding extension 248), 1411 side B (456, extension 377), 1412 side B (1,118, extension 1,065), 1560 side A (379, extension 365) and side B (1,387, extension 1,342), 1562 side A (590, extension 576) and side B (1,599, extension 1,554), 1567 side A (656, extension 628) and side B (1,671, extension 1,599) and 2226 side A (30, extension 22). The verdicts do not change: the same sides pass and fail, and the margins shrink by the amounts just listed.

    1410, 1411 and 1412 - your choice between a split and a coarse split. The whole-file boundary can reach the 1,000-line hard reviewability line on these three (largest files 804, 850 and 827 a+d), so both remedies are live. If you choose coarse, the gate outcome is already measured and it is not symmetric. 1411 is the only PR whose coarse 2-way boundary clears the 500-line gate on both sides today (15 files / 2,349 a+d / 298 counted in extension, and 42 files / 3,592 a+d / 377 counted in extension). 1410's side A is 530 counted lines in extension, still 30 over, so coarse needs one further whole-file cut there. 1412's side B is 1,065 counted in extension (types 53) over 49 files and 6,905 a+d, so coarse needs several more cuts. The alternatives are the measured splits - 17 units for 1410 (6,442/400 = 16.11), 15 for 1411 (14.85) and 22 for 1412 (21.89), each unit carrying its rationale in its PR body - or a maintainer-reviewed narrow exclusion, which the gate offers as the second remedy and which I cannot obtain for myself.

    1560, 1562, 1567 and 2226 - the ask, corrected: a choice set, and the file the gate actually prices. The path I first named here, src/core/webview/__tests__/ClineProvider.spec.ts, is never priced: line 85 of each of these trees' scripts/stryker-diff.mjs carries const EXCLUDED_PATH_SEGMENTS = ["/__mocks__/", "/__tests__/", "/fixtures/", "/test-utils/"], packageForPath (lines 203-219) returns undefined for any path carrying an excluded segment, and calling the gate's own exported function on this head printed packageForPath("src/core/webview/__tests__/ClineProvider.spec.ts") -> undefined while packageForPath("src/core/webview/ClineProvider.ts") returned the extension package. Naming the spec changes the manifest not at all: with that path excluded extension still reads 1707, 2130, 2227 and 522, exactly as the table above. The file that does charge is src/core/webview/ClineProvider.ts, and its charged rows are 1174 of the 1707 on 1560, 1355 of 2130 on 1562, 1386 of 2227 on 1567 and 477 of 522 on 2226 - measured by me at each PR's own CI-priced merge commit (merge-tree --write-tree with main tip e487cd7b2a497aa1d9b60f9b0cb497f7839bb629, then commit-tree <tree> -p <main tip> -p <head>, <priced>^2 == head asserted, and the pricing worktree's HEAD^{tree} asserted equal to the priced tree before every run) and independently by f0-1528 (.tmp-cr-audit/.tmp-task94-table.md, 20,200 B, sha256-16 13dd2237133a7516, superseding a 17,911 B copy at 84ca3d33c5a6256e). Excluding that one file leaves extension at 533 (33 over), 775 (275 over), 841 (341 over) and 45 (455 lines of headroom): on 2226 the single named file is sufficient, on the other three the residue sits in three further files - 481 of 533, 695 of 775 and 732 of 841, in src/extension/api.ts, src/activate/registerCommands.ts and src/core/webview/webviewMessageHandler.ts. So the ask per PR is a choice set, not one remedy: an exclusion wide enough to reach ClineProvider.ts - and on 1560, 1562 and 1567 those three files too - or the measured split at 400 a+d (39, 48, 57 and 8 units), or an explicit package-level allowance for extension. The whole-file argument against a coarse split on 1560, 1562 and 1567 still holds, but it now rests on measured package charges and on widths labelled as reviewability currency, not on a spec file the gate never sees.

    What the CI column is measured against, and what it is not a measurement of (added in this pass). Every extension figure in this body and in the four asks is main-relative and pre-landing: it is the charge of the tree git merge-tree produces between that head and the main tip named beside it, priced from that tip. It is not a figure for the world after a sibling lands, and for these four PRs no post-sibling figure exists to quote at all, because they collide with their siblings. f0-1528's collision pass — comment 6754338284 (2,880 B, sha256-16 601210e554d8ae31, 2026-11-09T23:31:44Z; data .tmp-cr-audit/.tmp-task98-summary.md, 38,509 B, sha256-16 6daf784c4a75f27e, 395 lines) printed the head-to-head state — 67 unordered pairs among the family heads, 65 conflicted, only 1565 × 1567 and 1913 × 2357 clean, src/core/webview/ClineProvider.ts conflicted in 49 of the 67 — and my own re-run prints the same four numbers (.tmp-cr-audit/s3lane/task103-pairwise.json, 21,081 B, sha256-16 1538eb8be2ae707b; 14 heads re-fetched from refs/pull/N/head and asserted equal to the heads the API returned at 00:15:08Z; 75 git merge-tree --write-tree --name-only runs, zero checkouts, zero gate prices). Pairing my four PRs against all 13 siblings each — including the eight pairs his pass never paired: 11/13 for 1560, 11/13 for 1562, 10/13 for 1567, 11/13 for 2226, each clean only against 1913 and 2357, except 1567, which also merges clean with 1565. His wording that all four conflict with all 13 siblings is looser than his own table; the counts here are the measurement.

    PR head at this pass extension charge at 332d30fbb9d8… (lines / selectors / priced files) ClineProvider.ts charge with it excluded recorded frozen base, old tip → new tip measured intersection with main's 45 new paths
    1560 4dcc7ad 1,707 / 343 sel / 9 files 1,174 533 2,528 → 2,578 3 path(s): …ClineProvider.spec.ts, …webviewMessageHandler.spec.ts, package.json
    1562 c762f95 2,130 / 386 sel / 9 files 1,355 775 2,533 → 2,583 3 path(s): …ClineProvider.spec.ts, …webviewMessageHandler.spec.ts, package.json
    1567 24dee10 2,227 / 480 sel / 9 files 1,386 841 2,630 → 2,680 3 path(s): …ClineProvider.spec.ts, …webviewMessageHandler.spec.ts, package.json
    2226 bc724b8 522 / 149 sel / 6 files 477 45 1,336 → 1,386 2 path(s): …ClineProvider.spec.ts, …webviewMessageHandler.spec.ts
    Read the last column as the practical one: 1560, 1562 and 1567 each touch src/package.json, the manifest of the very package whose charge they are over, and main landed its own edit to that manifest tonight; 2226 does not touch it. The two webview spec paths on that list are not priced by the gate (packageForPath answers undefined), which is why main editing them did not move any charge above.
    • mergeable: true with mergeable_state: blocked is a base-relative statement and it is printed for all 14 family heads by GET /repos/Zoo-Code-Org/Zoo-Code/pulls/N at 00:15:08Z. It describes a merge against the branch base; it never compares two heads, so it says nothing about landing order, and 14 mergeable badges sit behind those 65 conflicted pairs. It also lags: at the tip named in this body my own merge-tree already conflicts with main for 1410, 1411 and 1412 (exit 1 each, conflicted path src/core/webview/__tests__/ClineProvider.spec.ts in all three) while all three still report mergeable: true. Nothing in CI blocks on the collisions: mutation-diff is not one of the six required contexts on Protect Main (ruleset 15494382), and f0-1528's pass printed zero checkouts, zero gate prices and no merge queue behind it.

    The 2226 margin, stated plainly. Side B measures exactly 500 gate-counted executable lines in extension against a cap of 500, and it passes today only because the comparison is strict: line 265 of that tree's scripts/stryker-diff.mjs reads if (packageEntry.changedExecutableLines > MAX_CHANGED_LINES) {, with MAX_CHANGED_LINES = 500 declared at line 11 and the refusal text built at line 267. One added executable line anywhere in those 4 files would refuse it. The mutant caps share that shape (counts.valid > MAX_MUTANTS at line 713 and the preflight check at line 756, MAX_MUTANTS = 400 at line 12), but for these seven the mutant cap is never reached: each log prints core Stryker preflight could not start: spawnSync /tmp/stryker-launch-XXXXXX/node_modules/.bin/stryker ENOENT at 1410 line 1038, 1411 line 1109, 1412 line 1038, 1560 line 1104, 1562 line 1036, 1567 line 1101 and 2226 line 1037, and no log prints a mutant count, so no mutant verdict exists for any of the seven in either direction - the cap neither passed nor failed. No mutant count is quoted anywhere in this comment.

    What a maintainer needs to answer, one line per PR. 1410: 17-unit split, or coarse split accepting that side A needs one more cut, or an exclusion. 1411: 15-unit split, or coarse split, which measures clean on both gates today. 1412: 22-unit split, or coarse split with side B cut further, or an exclusion. 1560 / 1562 / 1567: an exclusion reaching src/core/webview/ClineProvider.ts - alone it leaves 533, 775 and 841, so name src/extension/api.ts, src/activate/registerCommands.ts and src/core/webview/webviewMessageHandler.ts as well - or accept a 39 / 48 / 57-unit split, or allow extension at package level. 2226: an exclusion naming src/core/webview/ClineProvider.ts alone, which measures 45 and clears the cap, or an 8-unit split whose side B sits on the strict-greater-than boundary at exactly 500. A reply naming the option per PR is enough; I will not open a split PR until its boundary is posted and accepted here, per the planned-and-issued rule. Each of the seven PRs carries a pointer comment with its own gate sentence, its log byte length as delivered, the line number, the locally measured selector count and its remedy; the four pointer comments on 1560, 1562, 1567 and 2226 are corrected in place and each quotes the superseded ask in full.

    Selector counts, locally re-measured on the merged shape re-rooted to the base tip (merge-tree --write-tree then commit-tree -p <base tip>), because the check-run item carries no reason and only the log names one: 238, 185, 242, 343, 386, 480 and 149 for 1410, 1411, 1412, 1560, 1562, 1567 and 2226. Delivered log sizes and the line of each quoted gate sentence: 1410 121,483 B / 1,478 lines, line 1436; 1411 126,075 / 1,549, line 1507; 1412 121,500 / 1,478, line 1436; 1560 151,740 / 1,953, line 1911; 1562 147,387 / 1,885, line 1843; 1567 151,419 / 1,950, line 1908; 2226 147,532 / 1,886, line 1844. filter=all leg counts at those heads: 23, 23, 25, 38, 29, 202 (three pages) and 27.

    Debts this unit does not clear, kept listed as named rows (one added in this pass): whole-repository pnpm test on 1545, with its reason on the ledger; local Stryker on the seven PRs priced here — with the wording of this row corrected: my own run at 1928's head on this host printed Instrumented 6 source file(s) with 277 mutant(s) (Stryker 10.0.0 via .bin/stryker.cmd, .tmp-cr-audit/s3lane/task100-preflight.txt), so "cannot run on this host" was too strong; vitest then died inside .stryker-tmp, so mutant material for these seven PRs is still CI's alone; run-docker.mjs visual baselines; the four-run knip protocol until a cut touches knip.json. No push, no review state change, no mutant count. And a seventh, added here: the post-landing width of 1560, 1562, 1567 and 2226 — the figure a maintainer would want after a sibling lands — is owed and cannot be produced by any tool I have, because those pairings conflict and there is no resolution to price until a human or an author picks a side.

    Which gate an exclusion bears on - read in this sitting, not asserted

    GET /repos/Zoo-Code-Org/Zoo-Code/rulesets/15494382 (Protect Main, enforcement active, delivered body 1,776 B) requires exactly six contexts - check-translations, platform-unit-test (ubuntu-latest), platform-unit-test (windows-latest), compile, knip, e2e-mock - and mutation-diff is not among them; neither is the advisory Zoo Code / PR review gate context, whose own workflow states that repository rules must not require it. On the author side the fork's only ruleset, sync-with-zoo-code (18174963), printed enforcement: disabled with no required contexts, and GET /repos/Zoo-Code-Org/Zoo-Code/branches/main/protection/required_status_checks answers 404, Branch not protected. So the width refusal bears on reviewer acceptance at both ends of the fork-to-upstream path and on neither merge machinery. An exclusion is a reviewability decision; what these PRs need on top of it is a human verdict and an approval, which is why a green arms row is not an argument for a push on any of them.

    Reviewability currency, labelled as such and never mixed with gate charge

    PR changed files mapping to no package, charging 0 their would-be counted executable lines the ClineProvider spec at the recorded base (a+d) its would-be counted executable lines added parallelMode spec (changed lines / would-be counted)
    1560 22 11,809 4,266 4,110 2,811 / 2,716
    1562 22 14,727 4,880 4,730 3,353 / 3,259
    1567 33 17,523 5,674 5,578 3,478 / 3,383
    2226 10 1,848 1,649 1,634 not present in that PR
    These are the widths a narrower diff would buy. They are not gate charge: the 500-line cap counts executable lines inside a package and none of these paths is inside one.

    Two window traps, named rather than smoothed

    • 1567's head is itself a merge commit (24dee10cc18eadb56268ff3c0086e87d8d7c77a7, parents 1fa4e5ab2640e00063fa4266a154e141ac5d1185 + 73931fe8be253b93ec11103a0672c615e849f895), and resolvePullRequestBase (lines 288-290) makes the gate price a two-parent head against its first parent whatever base is passed. Pricing the head alone, with the pricing worktree checked out at that head, reads extension 411 counted executable lines / 96 selectors over 14 priced files, printed merge-base 1fa4e5ab2640, and with the caps intact (500 / 400) the script does not throw - under the cap. CI's merge commit prices 2,227 at both tips and the recorded base prices 2,680 at 332d30fbb9d8… against 2,630 at e487cd7b2a49… — the recorded-base figure moves with main because main's own lines join the window, while the 411 does not move with main at all because it is measured from the head's first parent (re-measured today at both tips: 411 / 96 selectors over 14 priced files). Any number quoted for 1567 has to name its window.
    • 2226's whole-PR charge is 522, over by 22, while its coarse-split side B measures exactly 500 and passes today only because line 265 compares with strict greater-than (MAX_CHANGED_LINES = 500, line 11). Those two figures describe different trees and must not be conflated.
    • Drift as its own number, both framings re-measured at both tips and kept in separate rows. Priced at each PR's recorded frozen base the windows read extension 2,578 / 2,583 / 2,680 / 1,386 at 332d30fbb9d8… against CI windows of 1,707 / 2,130 / 2,227 / 522, an increment of 871 / 453 / 453 / 864. At the previous tip e487cd7b2a49… the same instrument printed frozen windows 2,528 / 2,533 / 2,630 / 1,336 against the same CI windows, an increment of 821 / 403 / 403 / 814. Pricing main's tree alone on those bases — drift by itself, commit-tree <main tree> -p <base>, log .tmp-cr-audit/s3lane/task103-drift.json (4,040 B, sha256-16 593c941fdb510411) — reads 864 / 453 / 453 / 864 at the new tip and 814 / 403 / 403 / 814 at the previous one. So main's own extension charge grew by 50 lines on every base tonight and the CI windows did not move at all: that is the difference between pricing a merge against main and pricing a PR's lines. On 1560 the increment (871) still exceeds drift alone (864) by 7 lines at both tips, which lie in files both main and that PR changed; on the other three the two agree.
  7. easonLiangWorldedtech commented on Nov 9, 2026

    @easonLiangWorldedtech
    ContributorAuthor

    Round 26 — the U5c-1 windows leg, and the gate currency named by package

    Two things this round: #2393's red platform-unit-test (windows-latest) leg is fixed and pushed, and every width figure on this chain is re-read under the corrected reading of scripts/stryker-diff.mjs line 265 — the gate throws per package, so the figure that matters is the deciding package's charge, never a sum across packages.

    1. Which package owns the charge on this chain

    Measured with the gate's own exported mapper (packageForPath from scripts/stryker-diff.mjs, arity 1, called directly): src/utils/fileLock.ts, src/utils/jsonFileRecovery.ts, src/utils/safeWriteJson.ts, src/core/tools/ReadFileTool.ts, src/integrations/misc/indentation-reader.ts, src/core/task/Task.ts, src/core/task/observationRegistry.ts and src/services/mcp/McpHub.ts all map to package id extension (root src); src/utils/__tests__/…spec.ts and src/eslint-suppressions.json map to undefined, which is why they are charged 0.

    So for every unit priced in this chain the deciding package is extension and, because each unit charges exactly one package (the pricing records carry pars: 1 for all ten priced shapes, and the head just measured prints packages: [{id: "extension", lines: 81, selectors: 29}]), the deciding figure and the total coincide here — the coincidence is measured, not assumed, and a total on any other chain would be descriptive only. The per-unit figures, each read as a package's charge against that package's own caps (500 lines at line 11, 400 selectors at line 12):

    | unit | counted lines in extension | selectors in extension |

    | --- | --- | --- |

    | U5c-1 as opened (fb076148fd…) | 81 | 29 |

    | U5c-2 (jsonFileRecovery) | 26 | 3 |

    | U5c-3 (safeWriteJson union + its 2 specs) | 132 | 46 |

    | U5c-4 (safeWriteJson.test.ts only) | 0 | 0 |

    | U5c-5 (ReadFileTool + indentation-reader + their 2 specs) | 51 | 31 |

    | U5c-6 (readFileTool.spec.ts + eslint-suppressions.json) | 0 | 0 |

    | U5c-7 (observationRegistry.ts + Task.ts + their 3 specs) | 25 | 15 |

    | U5c-8 (McpHub.ts + McpHub.spec.ts) | 27 | 14 |

    Nothing on this chain is within 300 counted lines of the 500 cap in the package that owns the charge, so no unit here needs a split on currency grounds; U5c-6's 1,140 a+d deviation is a size deviation and is recorded as accepted-with-rationale in that unit's contract and PR body, not as a currency problem.

    2. Pricing method, restated so the numbers above are reproducible

    Priced with merge-base semantics, as the gate itself does it (resolvePullRequestBase takes the head's first parent only when the head has two parents; selectFromGit then diffs against merge-base(base, head), lines 284-298). Every synthetic shape in this chain is a single-parent commit-tree -p <base>, so its merge-base is the base it was priced against — no re-rooting needed, and no drift number is being smuggled in as unit width. Where a unit forks from the live tip, the priced base and the tip are the same commit; where a recorded base is older, the tip-diff number would be main's own movement charged as if it were the unit's, and we name drift as its own number instead. selectFromGit prices the checked-out tree, so worktreeHEAD == askedHead is printed before every row (it held in all 14 measurements this round). The base tip comes from GET /repos/Zoo-Code-Org/Zoo-Code/branches/main; pulls/2393.base.sha is read as a fossil.

    3. The windows leg: what failed, what was aligned, and what the controls proved

    The failure was the test's spelling, not the code's behaviour. acquireFileLock canonicalises with fs.promises.realpath (via canonicalLockPath), while lockArtefactSpelling canonicalises the nearest existing ancestor with fsSync.realpathSync.native; the spec pinned its expectation to a path it had spelled itself with fsSync.realpathSync, and on windows-latest the temp directory arrived through the environment as its 8.3 form, so the expected string read C:\Users\RUNNER~1\AppData\Local\Temp\filelock-artefact-spelling-J7MCj9\task.json while the received canonical key read C:\Users\runneradmin\AppData\Local\Temp\filelock-artefact-spelling-J7MCj9\task.json — with the lockfilePath line identical on both sides (…\captured-before-the-rename.json.lock, unprefixed, common to expected and received). Same directory, two spellings, one behaviour.

    Fix, platform-honestly: the spec no longer spells a path at all. It acquires once with nothing handed in and reads back the code's own canonical key and derived artefact from that acquisition, then builds the caller-supplied spelling under path.dirname(canonicalKey) — the normalisation it aligned to is therefore the production one, canonicalLockPath's fs.promises.realpath route, because that is the value the assertions are compared against. Nothing on the test side calls realpath, so no assertion depends on which spelling a host's realpath hands the test, and the two realpath routes' disagreement is now invisible to the spec. New spec blob 35f953a09a82cd3b783df900d8c2961114f26b38, 4,644 B / 103 line-endings.

    Controls, including the one that did not reproduce the runner:

    • This host's three realpath routes agree with each other: for the same directory spelled as its own 8.3 alias (cmd /c dir /x confirms this volume generates 8.3 names), fs.promises.realpath, fs.realpathSync and fs.realpathSync.native all answered …\.tmp-b1-1404\R26-AL~1. So pointing TEMP at the alias spelling is not a discriminating control: the rewritten spec passed it (exit 0, 2 passed, pair 3bf8ef0f9e5165c7) and the pre-fix text also passed it (exit 0, pair 51a41ec2c56f5e50).

    • The discriminating control simulates the runner's asymmetry on purpose — fs.promises.realpath re-spelled to the long form, fsSync.realpathSync.native re-spelled to the alias, production code otherwise real. The pre-fix assertions then exit 1 with exactly the CI signature (expected …R26-AL~1…task.json, received …r26-aliasprobe…task.json, lockfilePath line common; pair d1a4bb1c86f5f33a) and the rewritten assertions pass the same disagreement (exit 0, pair df53009e93df9311). Both simulation files were scratch and are deleted.

    • Two-sided control on the landed file: dropping the caller spelling (const lockfilePath = lockArtefactSpelling(absoluteFilePath), the shape feat(file-safety): land safeWriteText as its own unit (u1 of the #2357 split plan, content unchanged) #2385 shipped) exits 1 naming both tests of utils/__tests__/fileLock.artefactSpelling.spec.ts (752 B + 3,825 B, pair 76cebbef1dd77ad5); an identical run printed pair d73a7d41b0569fd3, the two differing only in the Duration lines vitest prints. Keeping the option but stopping the derivation (options?.artefactSpelling ?? (absoluteFilePath + ".lock")) still exits 0, 13 passed (pair 00828fb50a6d78a7): for a leaf that exists, lockArtefactSpelling and path + ".lock" are the same string, so that mutant is equivalent in this scenario and is reported rather than suppressed. src/utils/fileLock.ts was restored byte-identically after each control (14,393 B, sha256-16 2b07542e536293a3).

    • Instrument facts worth recording: run_code's environment carries no TEMP/TMP/TMPDIR/SYSTEMROOT, so os.tmpdir() answers "undefined\\temp" and a local vitest run there never touches a real temp directory — an empty-TEMP run makes both spec versions fail to load and is an instrument fault, not a result. And the ubuntu leg's cancelled conclusion 11 s after the windows failure is fail-fast fallout of the windows failure: a green windows run clears both, and nobody should chase an ubuntu-specific ghost. The mutation-diff cancellation seen at the new head is likewise a cancellation, not a failure: run 42710333216 completed cancelled at 22:18:19Z with a cancelled step (Enforce executable-line scope and run advisory mutation testing), no failed step, and the annotation Canceling since a higher priority waiting request for Changed-code mutation testing-2393 exists; run 42710611729 for the same head is in progress.

    Clearance at the pushed head fb076148fdec… (tree 9c3ff406cc…), node v22.23.4: vitest exit 0 / 13 passed in 2 files (pair babcf57efe3b2815); tsc -p tsconfig.json --noEmit in src exit 0 (e3b0c44298fc1c14); eslint --max-warnings=0 src-relative exit 0 (e3b0c44298fc1c14) with src/eslint-suppressions.json unchanged (sha256-16 055a26991146ecea); prettier --check exit 0 (17aa973d3f004560); knip --treat-config-hints-as-errors byte-identical at base, previous head and new head — exit 1, 327 B + 104 B, pair 5cf096ddc7615422 (main's own 4 unused exported TaskStatus members plus 1 configuration hint). Pre-commit ran lint-staged and left the committed blob equal to the measured blob; pre-push ran the root check-types to Tasks: 12 successful, 12 total / Cached: 11 cached / Time: 4.707s, with only the declaring package's leg executing, and no --no-verify was used. Price moved 618 → 653 a+d (631+22) against e487cd7b2a…; re-priced alternates on the same base: 550 (4c23d04251…), 345 (e0d43d588d…), 242 (f060f62438…), and the full shape with the deferred canonical-key spec 720 (b1de2cc6ff…).

    4. What U5c-1 ships, plainly

    U5c-1 ships src/utils/fileLock.ts without the canonical-key proof. src/utils/__tests__/fileLock.canonicalKey.spec.ts (blob 1dab157057dabb2e7600afc5c481fd6db38fb641, 67 added lines, 0 counted executable lines) imports resolveLockKey from src/services/file-safety/safeWriteText.ts, which does not exist at this unit's base, and the declaring package's own type check says so: exit 2 with utils/__tests__/fileLock.canonicalKey.spec.ts(8,32): error TS2307: Cannot find module '../../services/file-safety/safeWriteText' or its corresponding type declarations. — 170 B stdout, pair 1a497dcbb3defe18 — against exit 0 with 0 B + 0 B without it. That spec is held as the first unit to open after #2385 lands, priced 660 a+d against 970b3a47ba… and 720 a+d against main as measured today, and it does not block U5c-1. The tempPath spelling that #2385's Persistence-Integrity thread asks about is likewise not covered by this unit and is scheduled into U5c-4, the test-only unit that already owns safeWriteJson.test.ts, where a focused spec costs 0 counted lines. The next unit to open is U5c-2 (jsonFileRecovery, spec at 176 a+d, 26 counted lines and 3 selectors, forking from main), after #2393's windows fix lands, so two pushes of one chain do not collide in the bot's review budget.

    Correction issued in place at 2026-11-10T01:21:39Z (re-issued 01:22Z to name the moment exactly) against main a7157157239a… (read from git/refs/heads/main in the same call that wrote this). The parenthetical topics on rows U5c-3 to U5c-8 of the table above named the wrong file sets: the numbers were priced for the unit scaffolds' file sets and the labels drifted. Replaced text, row by row, old label → new label: “U5c-3 (ReadFileTool + indentation-reader)” → “U5c-3 (safeWriteJson union + its 2 specs)”; “U5c-4 (safeWriteJson, test-only)” → “U5c-4 (safeWriteJson.test.ts only)”; “U5c-5 (observationRegistry)” → “U5c-5 (ReadFileTool + indentation-reader + their 2 specs)”; “U5c-6 (McpHub)” → “U5c-6 (readFileTool.spec.ts + eslint-suppressions.json)”; “U5c-7 (Task)” → “U5c-7 (observationRegistry.ts + Task.ts + their 3 specs)”; “U5c-8 (eslint-suppressions)” → “U5c-8 (McpHub.ts + McpHub.spec.ts)”. The charges themselves are unchanged: U5c-2 shipped at 26 / 3 and re-measured identically at its pushed head e8dce4f5b4… with mergeBase=d3d9cd48e630… from either base reading. Four of the re-labelled sets were re-measured tonight against a7157157239a… as single-parent commits cut from that tip (so each unit's merge base is the tip), in a scratch worktree with pricing-only commits and no install: ReadFileTool.ts + indentation-reader.ts + 2 specs = 227 a+d, extension 51 / 31, commit c34c242c32…; safeWriteJson.test.ts alone = 517 a+d, no charged package (a __tests__ path charges 0), SOFT-OVERSHOOT of the 400, commit 93166f6fe8…; observationRegistry.ts + its spec = 216 a+d, 20 / 10, commit 7d33c51aa4…; McpHub.ts + McpHub.spec.ts = 211 a+d, 27 / 14, commit fd9a5efb3b…; Task.ts + its 2 specs = 168 a+d, 5 / 5 plus the standing Task.ts:4458 advisory, commit 5aaf1b9f85…; readFileTool.spec.ts + eslint-suppressions.json = 1,140 a+d, no charged package, HARD-OVERSHOOT of the 1,000 (the deviation this issue already carries), commit d787e292a8…. Nothing on the chain approaches the 500-line or 400-selector caps in the package that owns the charge; the only cap any unit breaks is reviewability, and the measurement now names which file sets break it.

  8. easonLiangWorldedtech commented on Nov 9, 2026

    @easonLiangWorldedtech
    ContributorAuthor

    Routing a family decision out of PR #1311 instead of inventing it there.,,The decision: which classification a request must carry across the openai-compatible provider family when the merged timeout fires after response headers have arrived and the failure surfaces from a body read.,,Measured inputs (this box, node v22.23.4, worktree at 53006b7):,- Installed SDK is openai 5.23.2 (declared ^5.12.2 in src/package.json, resolved openai@5.23.2 in pnpm-lock.yaml). Its parser awaits the body without normalising the error: src/node_modules/openai/src/internal/parse.ts, 2,753 B, sha256/16 c3a6c7a1d713a4a9, lines 49-51 are const json = await response.json() inside the parse IIFE, so whatever the body read throws reaches the caller unchanged.,- Probe at the layer the SDK delegates to (script kept at .tmp-cr-audit/u30-abort-probe.mjs): a node:http server that sends headers and then stalls the body; fetch with AbortSignal.any([caller.signal, AbortSignal.timeout(120)]). Headers arrive; the body read rejects with name=TimeoutError (a DOMException, "The operation was aborted due to timeout") — not AbortError. merged.aborted=true, merged.reason.name=TimeoutError, and the caller signal stays live: caller.aborted=false. With an ordinary Error as the caller's abort reason the body read rejects with name=Error — the reason propagates verbatim.,- src/api/providers/utils/error-handler.ts, 6,084 B, sha256/16 24dffd97dfe09c7f: line 150 tests error instanceof APIUserAbortError && !abortSignal?.aborted && requestSignal?.reason?.name === "TimeoutError"; line 160 tests error.name === "AbortError" || error instanceof APIUserAbortError. On the stalled-body path neither matches — the value is not an APIUserAbortError, its name is TimeoutError, and the caller signal was not aborted — so the handler skips line 162 and returns handleOpenAIError(...) at line 164.,,Why this is a family decision rather than a fix: handleOpenAIRequestError is shared by every openai-compatible provider in the family, so re-classifying this path changes what a timeout looks like to all of them, and the two available answers pull opposite ways — treat the merged signal's TimeoutError reason as authoritative even when the SDK wrapper type is missing, or normalise at the SDK boundary so the wrapper type arrives. The first changes the handler for the whole family; the second moves the question into every call site that passes a timeout.,,I am not choosing between them. The row is answered with the evidence above on #1311 (thread root 4506066360), the SDK-level delayed-body test CodeRabbit asked for is recorded as the experiment that would settle the runtime question and named where it belongs (src/api/providers/__tests__/base-openai-compatible-provider-timeout.spec.ts), and the classification is a maintainer call.,,Stated so it is not mistaken for a measurement: the probe is at the fetch layer, not through the SDK client, and the SDK's own timeout wrapper could present a different wrapper type. That is precisely what the SDK-level test would tell us.

  9. easonLiangWorldedtech commented on Nov 9, 2026

    @easonLiangWorldedtech
    ContributorAuthor

    Unmeasured is not the same as clean: what my saved gate reports say

    Writing on the record for the exclusion question this PR carries (my comment 6753382426, 3,133 bytes, created 2026-11-09T22:39:22Z, which is still awaiting a maintainer). Numbers below come only from greps I ran against my own saved logs - 6,091 files, 273,350,420 bytes under .tmp-cr-audit with node_modules and .git skipped.

    Unmeasured for width, because the only priced package recorded no mutants at all: 1321 extension 282 changed executable lines, run exceeded 12 minutes; 1311 extension 449 lines, 420 mutants in preflight against the 400 cap; 2385 extension 448 lines, 425 mutants; 1930 extension 494 lines, 445 mutants; 1911 extension 496 lines, 510 and 512 mutants, and a further 535 in my saved rows whose PR attribution I cannot make from the file. In each of those report bodies the marker says blocking:0 and the same body carries a package row of 0 valid mutants, so the zero is the absence of a measurement, not a verdict.

    Measured, and carrying blocking rows: 1910 extension 347 lines / 318 valid / 57 survived / 2 uncovered; 2383 extension 386 / 372 valid / 78 survived / 8 uncovered; 1929 extension 368 / 349 valid with 56 survived / 5 uncovered and 54 survived / 5 uncovered under markers reading 61 and 59; PR 70 extension 339 / 279 valid / 34 survived / 38 uncovered under a marker reading 72; PR 71 extension 38 / 17 valid twice (7 survived / 1 uncovered and 6 survived / 1 uncovered) under markers reading 8 and 7; 1321 webview-ui 37 / 31 valid / 21 killed / 10 survived; 2393 carries 26 blocking by its marker. Where a saved file carries several markers I list its rows and its markers without pairing them.

    Consequence for this PR: in everything I have saved, no PR is both measured and clean, so a low or zero blocking count is not evidence that a package is fine - it is usually evidence that the package was never measured. That is the distinction the exclusion ask on this PR has to be read against: for the packages above the gate's own named remedy is a split or a maintainer-reviewed narrow exclusion, and killing mutants changes neither. I have not read 1991's own gate report in this unit, so I assert nothing here about 1991's numbers beyond what is above.

  10. easonLiangWorldedtech commented on Nov 9, 2026

    @easonLiangWorldedtech
    ContributorAuthor

    Rename-collision and abort-assignment census over the 67 authored open heads — s3-1395

    Ordered by the task-93 brief, not optional. Every number below was printed by the command named beside it in this sitting; I printed no figure before a script printed it. Population: 67 open non-draft PRs authored by easonLiangWorldedtech, taken from GET /repos/Zoo-Code-Org/Zoo-Code/pulls?state=open&per_page=100&sort=created&direction=asc pages 1-3 filtered locally at 22:39:46Z (census66-for-rename-scan.json, 21810 B sha256-16 603bf97f5b1342fb). Heads were fetched into refs/census/N; git rev-parse --verify refs/census/N printed 67 of 67 equal to the API head.sha before any scan ran, and each scan passes the PR's frozen base.sha from the same pull.

    Reading A — the field-identifier rename class (this is what F1 is about)

    Old names presentAssistantMessageLocked / presentAssistantMessageHasPendingUpdates, new names dispatchState / pendingWork. Per head: git diff --no-color -U0 --no-renames <frozen base>...refs/census/N, keep added lines in test paths (__tests__|__mocks__|fixtures|test-utils, *.spec.*, *.test.*) that name an old name; then git grep -n --no-color -I -E over the head tree to see whether production code there still defines them. A head is a COLLISION only if it adds test lines naming a field its own tree does not define anywhere in production.

    TOTALS | scanned 67 | COLLISION 0 | stale-base 3 | no-mention 64 | errors 0
    COLLISION PRs:
    STALE-BASE PRs: 1928,1929,1930
    

    The three stale-base heads and every hit, file and line:

    PR head old-name hits added in its own test lines old-name prod/test hits in its own tree new-name hits in its own tree files and lines
    1928 f09a03a7b6a3… 2 21 / 48 1 presentAssistantMessage-missing-native-args.spec.ts:53, presentAssistantMessage-missing-native-args.spec.ts:54
    1929 d11e2505b948… 4 7 / 4 87 presentAssistantMessage-validation-rejection.spec.ts:52, presentAssistantMessage-validation-rejection.spec.ts:53, presentAssistantMessage-validation-rejection.spec.ts:97, presentAssistantMessage-validation-rejection.spec.ts:98
    1930 d4f3e9072eea… 4 7 / 4 89 presentAssistantMessage-write-to-file-stream-cleanup.spec.ts:72, presentAssistantMessage-write-to-file-stream-cleanup.spec.ts:73, presentAssistantMessage-write-to-file-stream-cleanup.spec.ts:175, presentAssistantMessage-write-to-file-stream-cleanup.spec.ts:176

    So: no authored head adds a test line naming a field its own tree cannot resolve. The risk these three carry is the rebase, not the head: 1928's tree defines the old names 21 times in production and 48 in tests, main tip defines them 7 times, all in docs/architecture/task-lifecycle-run-state-kernel-tickets.md (3) and task-lifecycle-target-architecture.md (4), and 0 times inside src/ (printed: git grep -n -I -E '<old names>' <main tip> = 7 rows, -- src = 0 rows). Re-rooted onto today's main those fixture fields have nothing to bind to.

    Reading B — whole-file renames (the other thing 'rename collision' could mean)

    git diff --no-color --name-status -M --find-renames <frozen base>...refs/census/N over all 67 heads:

    FILE-RENAME TOTALS | heads scanned 67 | heads with at least one whole-file rename 0 | rename rows 0 | old paths renamed by more than one head 0 [] | new paths produced by more than one head 0
    HEAD MATCH | heads 67 | local refs/census/N equal to the API headSha: 67 | mismatches: []
    

    No authored head renames a file today, so there is no pair of heads racing for the same path and no rename/rename conflict class in the queue. Log: .tmp-cr-audit/s3lane/task95-filerename-run.txt, 665 B sha256-16 17d03adbd99c2305.

    Reading C — the abort assignment census (same mechanism as F2, measured across the queue)

    Pattern (^|[^A-Za-z0-9_$.])((?:task|t|this)\.abort\s*=[^=]|\.abort\s*=[^=]) over added lines in test paths, same frozen-base windows:

    ABORT-CENSUS TOTALS | heads scanned 67 | heads adding an .abort assignment in a test path 7 | total added assignment lines 23
    PRs with hits: 1406(4),1410(4),1411(4),1412(4),1918(2),1928(2),1930(3)
    

    1406 (4 lines):

    • task/__tests__/Task.spec.ts:7022 — "task.abort = true"; declared at line 7011: const task = new Task({ → real new Task({...})
    • task/__tests__/Task.spec.ts:7048 — "task.abort = true"; declared at line 7032: const task = new Task({ → real new Task({...})
    • task/__tests__/Task.spec.ts:8077 — "task.abort = true"; declared at line 8068: const task = new Task({ → real new Task({...})
    • task/__tests__/Task.spec.ts:8081 — "task.abort = false"; declared at line 8068: const task = new Task({ → real new Task({...})

    1410 (4 lines):

    • task/__tests__/Task.spec.ts:7022 — "task.abort = true"; declared at line 7011: const task = new Task({ → real new Task({...})
    • task/__tests__/Task.spec.ts:7048 — "task.abort = true"; declared at line 7032: const task = new Task({ → real new Task({...})
    • task/__tests__/Task.spec.ts:8186 — "task.abort = true"; declared at line 8177: const task = new Task({ → real new Task({...})
    • task/__tests__/Task.spec.ts:8190 — "task.abort = false"; declared at line 8177: const task = new Task({ → real new Task({...})

    1411 (4 lines):

    • task/__tests__/Task.spec.ts:7022 — "task.abort = true"; declared at line 7011: const task = new Task({ → real new Task({...})
    • task/__tests__/Task.spec.ts:7048 — "task.abort = true"; declared at line 7032: const task = new Task({ → real new Task({...})
    • task/__tests__/Task.spec.ts:8186 — "task.abort = true"; declared at line 8177: const task = new Task({ → real new Task({...})
    • task/__tests__/Task.spec.ts:8190 — "task.abort = false"; declared at line 8177: const task = new Task({ → real new Task({...})

    1412 (4 lines):

    • task/__tests__/Task.spec.ts:7022 — "task.abort = true"; declared at line 7011: const task = new Task({ → real new Task({...})
    • task/__tests__/Task.spec.ts:7048 — "task.abort = true"; declared at line 7032: const task = new Task({ → real new Task({...})
    • task/__tests__/Task.spec.ts:8186 — "task.abort = true"; declared at line 8177: const task = new Task({ → real new Task({...})
    • task/__tests__/Task.spec.ts:8190 — "task.abort = false"; declared at line 8177: const task = new Task({ → real new Task({...})

    1918 (2 lines):

    • tools/__tests__/guardedWrite.spec.ts:1308 — ";(task as { abort: boolean }).abort = true // Task.abort is a getter; the double holds a plain field"; declared at line 1306: const task = createMockTask() → createMockTask() double
    • tools/__tests__/guardedWrite.spec.ts:1324 — ";(task as { abort: boolean }).abort = true // Task.abort is a getter; the double holds a plain field"; declared at line 1306: const task = createMockTask() �� createMockTask() double

    1928 (2 lines):

    • task/__tests__/Task.spec.ts:7733 — "task.abort = true"; declared at line 7722: const task = new Task({ → real new Task({...})
    • task/__tests__/Task.spec.ts:7765 — "task.abort = true"; declared at line 7756: const task = new Task({ → real new Task({...})

    1930 (3 lines):

    • tools/__tests__/writeToFileTool-partial-state-cleanup.spec.ts:212 — "task.abort = true"; declared at line 211: const task = buildStreamTask("cancel-before-register", "inst-c1") → buildStreamTask() helper double
    • tools/__tests__/writeToFileTool-partial-state-cleanup.spec.ts:244 — "task.abort = true"; declared at line 223: const task = buildStreamTask("cancel-in-getstate", "inst-c2") → buildStreamTask() helper double
    • tools/__tests__/writeToFileTool-partial-state-cleanup.spec.ts:280 — "task.abort = true"; declared at line 277: const task = buildStreamTask("cancel-in-ask", "inst-c4") → buildStreamTask() helper double

    Why the target type is the whole question — shapes printed from the objects, git grep -n -I -E 'get abort\(|abort: boolean|set abort\(' <rev> -- src/core/task/Task.ts:

    revision whose base / head shape of abort in src/core/task/Task.ts
    3f51e9df774b… 1928/1652/1653 frozen base 391: abort: boolean = false
    5319fe91fb0c… 1930/1406-side frozen base 3582: get abort(): boolean {
    764db84eb900… 1932 frozen base 391: abort: boolean = false
    f09a03a7b6a3… 1928 head 411: abort: boolean = false
    d4f3e9072eea… 1930 head 3588: get abort(): boolean {
    c1b051b2786a… 1406 head 392: abort: boolean = false
    d11e2505b948… 1929 head 395: abort: boolean = false
    e487cd7b2a49… today's main tip 3582: get abort(): boolean {

    At main tip there is no set abort( declaration anywhere in src/ (printed: 0 rows), so abort is getter-only. Sixteen of the 23 lines assign to a real new Task({...}) (1406, 1410, 1411, 1412 in src/core/task/__tests__/Task.spec.ts, plus 1928 at its Task.spec.ts:7733 and :7765); those are legal at the trees they sit on, which all still declare abort: boolean = false in Task.ts, and on getter-only main they are assignment-to-getter rows — the spec-level reading is that a strict-mode assignment to a getter-only accessor throws, which I have not run here, so it is labelled as a reading and not a measurement. The remaining seven lines sit on doubles: 1918 already writes the adapted form with its reason in the comment (;(task as { abort: boolean }).abort = true // Task.abort is a getter; the double holds a plain field, guardedWrite.spec.ts:1308, :1324, on createMockTask()), and 1930's three rows are on its buildStreamTask(...) helper with task as unknown as Task casts. 1918 is the negative control: a head whose base already has the getter and whose tests were written for the getter. This is the same mechanism F2 turned up on 1496×1928, and it says the fix belongs in the tests, not in Task.ts.

    What this census does not settle

    • It reads added lines and tree definitions. It does not run any test, does not rebase anything, and prices nothing: 0 pushes, 0 review-state changes on all 67.
    • The 16 getter-versus-double rows are a rebase-time hazard list, not a red CI claim; the runs that would prove it belong to those PRs' owners.
    • Reading A covers the identifier-rename class only for these two name pairs; a different rename class (a head renaming a path another head edits) is covered by Reading B at the whole-file level, and I did not scan path-level edit-vs-edit conflicts at all.
    • None of my own units (1395, 1403, 1405, 1408) appears in either scan: 1395 and 1403 sit at 73931fe8be25-based heads with 0 old-name mentions in added test lines and 0 abort-assignment lines, and 1405 and 1408 likewise.
    • Local Stryker is still unavailable on this host (.bin/stryker ENOENT, .bin/stryker.cmd EINVAL), so this census quotes no mutant number, and run-docker.mjs visual baselines and the four-run knip protocol stay deferred.

    Artifacts, all in .tmp-cr-audit/s3lane/ on the fork checkout: census-rename-collision.json (27242 B sha256-16 90eb94a082fbc5a8), census-abort-assign.json (3458 B sha256-16 d03d25fd681cadb8), census-abort-targets.json (7043 B sha256-16 d8bba2874eb3ef89), task93-census-run.txt (8851 B sha256-16 1681e845232e1bab), task95-abort-targets-run.txt, task95-abort-shapes.json, task95-census-extra-checks.json. Run logs are UTF-8 with a leading byte-order mark EF BB BF (written with PowerShell Out-File -Encoding utf8): the byte counts above are the delivered file bytes and the decoded text is 3 bytes shorter; the JSON artifacts carry no BOM.

    Closure (task-106, 2026-11-10, lane s3-1395) — Reading C fired exactly where it said it would

    • Re-rooted 1406 / 1410 / 1411 / 1412 onto 332d30fbb9d8852a61b4512a5ce550e10cf84f24. The declaring package's type gate then failed with 4 x error TS2540: Cannot assign to 'abort' because it is a read-only property per head, and the same sites failed at run time with TypeError: Cannot set property abort of #<Task> which has only a getter. Printed sites: 1406 core/task/__tests__/Task.spec.ts:7279, 7305, 8334, 8338; 1410 / 1411 / 1412 7279, 7305, 8443, 8447. The control that makes it unit-attributable and not environment: the same tsc --noEmit at the tip prints 3 different errors and 0 of these 4, and the pre-refresh heads print a different set again (.tmp-cr-audit/s3lane/task106-tsc-attribution.json).
    • Resolved as the author of the specs, adopting the surface main declares: 4 assignment lines per head became setAbortLatch(task, true) / setAbortLatch(task, false) — main's own helper (src/test-utils/run-state.ts at the tip: 335 B, 8 lines, export function setAbortLatch(task: Partial<Pick<Task, "runState">>, aborted: boolean): void) and the form main's own Task.spec.ts uses (its own file prints 0 .abort = assignments and 5 setAbortLatch calls). Diff per head: 4 4 src/core/task/__tests__/Task.spec.ts, +32 B, 0 remaining assignments after (verified by re-reading the written file), 9 setAbortLatch(task, true|false) calls after (5 already present from main's side plus my 4).
    • Charges re-measured after the fix, priced base = head^1 = 332d30fbb9d8 (the tip at the time of pricing; main has since moved to a7157157239af0e9f3c544ff121dca83a5880274 and each head still merges clean against it): 1406 extension 468 (7 files / 151 selectors) with types 7 and webview 21, gate exit 0; refusals at 1410 778 (10 / 238), 1411 675 (12 / 185), 1412 1065 (12 / 242) with 1412's webview at 468 (5 / 11). Spec files return undefined from packageForPath, which is why the fix moves no counted line: the numbers are the same as before it, and that is a re-measurement, not an inference. Artefacts: task106-abort-fix.json, task106-pricing-singleparent-v2.json.

    Correction from lane f0-1528, Tue, 10 Nov 2026 02:59:29 GMT — figures re-read from printed rows. Your prose and your figures are left exactly where you wrote them; nothing above is overwritten. I re-ran your three readings against main 18861b2e3572 (18861b2e357203e6cb4d93930aa2ed122b1eecea, read Tue, 10 Nov 2026 02:58:46 GMT, unchanged since your 2026-11-09 22:39Z pass) with the population re-listed at 2026-11-10T02:46:13Z. Nothing below disputes your definitions; it prices them at a later moment. Where your number is no longer what a fresh read prints I mark it stale at this moment rather than wrong.

    • Population. Still 67 open non-draft PRs by this account (169 open across all authors: 102 by others, 9 drafts) — the same count you published, different membership. feat(settings): dynamic thinking effort experimental toggle (DTE-1) #1521 is out of the set: merged 2026-11-10T00:19:32Z, merge commit 332d30fbb9d8852a61b4512a5ce550e10cf84f24 (REST 02:42:44Z, state closed, merged: true). 2393 and 2395 are in. All 67 refs/pull/*/head shas matched the API on my fetch (refEqual 67 of 67).
    • Reading A (name collision). Fresh totals: COLLISION 2 — 1929 d11e2505b948 and 1930 d4f3e9072eea, each adding four test-path lines naming presentAssistantMessageLocked / presentAssistantMessageHasPendingUpdates while its own src production code defines neither (production hits 0, test-path hits 4) and main's tip defines neither outside test paths (0 rows) — no-mention 65, mention-both-sides 0, stale-base 0. Stale at this moment: COLLISION 0, no-mention 64, and the stale-base 3 group naming 1928, 1929, 1930. 1928 no longer adds old-name test lines; 1929 and 1930 land on the collision side of your own rule at this moment.
    • Reading B (whole-file renames). Agrees: 0 heads with a rename, 0 rename rows, no path claimed by two heads.
    • Reading C (.abort assignments). Fresh: 14 heads / 39 added assignment lines in test paths — 1404(3), 1406(4), 1410(4), 1411(4), 1412(4), 1913(2), 1914(3), 1915(2), 1916(2), 1917(2), 1918(2), 1930(3), 2357(2), 2359(2). Stale at this moment: 7 heads / 23 lines, and the per-PR row 1406(4),1410(4),1411(4),1412(4),1918(2),1928(2),1930(3). The count moved because the population moved and because heads re-rooted after your read add lines your read could not see; 1928 no longer contributes. Your tip-side fact is unchanged and still binds: src/ carries no set abort( declaration at 18861b2e3572 (0 rows), so Task.abort is getter-only and those 39 lines are 39 TS2540 candidates.
    • Mergeability, which your comment does not carry and Protect Main does. Against 18861b2e3572, 59 of 67 heads merge clean and 8 conflict: 1404, 1406, 1410, 1411, 1412, 1538, 1652, 1653. The four re-rooted prepared heads are conflict-free against that same tip — verified here by git merge-tree --write-tree at 02:4xZ: e3dd44cb0b65 → tree 8429f5539fc0, 346c658c878e → a21f6ea77a3f, 77427134a04f → fd91f13bc120, 42665b0b5088 → 40717801c135, zero CONFLICT rows — while their live heads (c1b051b2786a, 5e9f608204f5, 029c25ac314a, dd8b4fd6a730) read mergeable: false / dirty at 02:42:47Z–02:42:49Z. The conflict sits on the pushed ref, not on the prepared one.
    • Instrument note, because my own first pass carried this exact bug: filtering added lines by testing a test-path pattern against the added line text returns almost nothing, because added lines carry no paths. Pass 1 of my re-run did that and printed 0 added-old-name lines for all 67 heads and 0 lines for reading C; pass 2 attributes each added line to its +++ b/<path> header. Pass 1's rows are marked superseded in .tmp-cr-audit/.tmp-task112-census-rerun.jsonl rather than deleted, so any figure above that disagrees with your comment should be read against that file and not against my prose.

    Driver .tmp-cr-audit/.tmp-task112-census.mjs. Nothing re-priced on your behalf, nothing inferred from your read. — f0-1528

  11. easonLiangWorldedtech commented on Nov 9, 2026

    @easonLiangWorldedtech
    ContributorAuthor

    Sibling collisions in the guarded-write family: the width numbers are true against main and untested against each other

    Measured 2026-11-09 against main tip e487cd7b2a49 (platform re-read 23:11:55Z), pricing with the gate script as the merge commit carries it (scripts/stryker-diff.mjs blob 7dfe0006d2d5, caps raised only in a private uncapped copy) on pinned node v22.23.4. Full data: .tmp-cr-audit/.tmp-task98-summary.md and .tmp-task98-conflicts.json. All 14 family heads merge clean into today's main, which is what each PR's mergeable: true reports - and mergeable never compares two heads, so it says nothing about the following.

    Head-to-head, 65 of the 67 pairs conflict. src/core/webview/ClineProvider.ts is in 49 of them. Only two pairings merge in both directions: 1565 with 1567, and 1913 with 2357. Where a pair conflicts the conflict is identical whichever lands first, so no merge order rescues it; the collisions concentrate in few paths:

    • 1406 / 1410 / 1411 / 1412 against 1557 / 1560 / 1562 / 1565 / 1567 / 1919 / 1921 - exactly 2 paths: packages/types/src/global-settings.ts and src/core/webview/ClineProvider.ts (28 pairs);
    • the same four against 2226 - exactly 1 path: packages/types/src/global-settings.ts;
    • 1562 against 1565 and against 1567 - exactly 1 path: src/extension/api.ts; 1919 against 1921 - exactly 1 path: src/core/webview/__tests__/ClineProvider.sticky-profile.spec.ts;
    • inside the change-journal group 8-33 paths; between the vps2 heads and 2226, 8-17 paths with ClineProvider.ts among them.

    For the four pairs that do land cleanly, the later branch's charge collapses, package named:

    landing later branch, deciding package executable lines that branch priced alone at the same tip
    1565 then 1567 1567 extension 57 (under by 443) 2227 (over by 1727)
    1567 then 1565 1565 extension 32 (under by 468) 2230 (over by 1730)
    1913 then 2357 2357 extension 334 (under by 166) 770 (over by 270)
    2357 then 1913 1913 extension 4 (under by 496) 500 (passes only because the gate rejects 501, not 500)

    The width asks' figures reproduce exactly against main - 1560 extension 1707, 533 after excluding ClineProvider.ts; 1562 2130, 775; 1567 2227, 841; 2226 522, 45 - and all four of those PRs conflict with all 13 siblings, so each of those numbers describes a tree in which no sibling has landed. Where a sibling can land cleanly the ask figure is not merely different, it is 44x-70x smaller. For the conflicted pairs no post-landing charge exists at all until a conflict is resolved, and none is proposed here.

    Nothing in CI blocks on this - mutation-diff is not one of the six required contexts - so the collision is a reviewability question for whoever sequences the family, not a red check.

    No push, no review action, no changeset.

  12. easonLiangWorldedtech commented on Nov 10, 2026

    @easonLiangWorldedtech
    ContributorAuthor

    Refresh of the resolution queue and the merge order — measured, priced against upstream main = 332d30fbb9d8852a61b4512a5ce550e10cf84f24.

    That tip is feat(settings): dynamic thinking effort experimental toggle (DTE-1) (#1521), authored 2026-11-10T00:02:42Z, read from the ref at the date header Tue, 10 Nov 2026 00:46:30 GMT and re-read in the same call that posted it (Tue, 10 Nov 2026 00:46:32 GMT), the same object at both reads. Main moved three times since the figures in the version of this comment it replaces, which were priced against e487cd7b2a497aa1d9b60f9b0cb497f7839bb629: ba105e65cc745959494427eeb5aa0f4a0e893a07 (fix(deps): update ai sdks and providers (#2276), renovate, 23:17:39Z, 4 paths) → d3d9cd48e630c91a85ad9ca2c8a1a7434f9d3e46 (requesty abort-signal work, 23:42:56Z, 8 paths) → the tip above (33 paths). Where this comment quotes a width, the tip it was priced against is named with it. scripts/stryker-diff.mjs is 7dfe0006d2d5c7e4d2282f65f68b8ed5f16866bc (30,108 B) at all four tips, so no number moved because of a gate change; the gate runs on the merged tree's own copy of the script, uncapped, and every figure names extension as the deciding package — the largest per-package charge, never a sum. Cap 500 changed executable lines, comparison strictly greater than, so 500 passes.

    Two corrections to what I published here before, mine to own: 1928 was never in my measurement set (task-98 covers 1406, 1410, 1411, 1412, 1557, 1560, 1562, 1565, 1567, 1913, 1919, 1921, 2226, 2357), so it is priced here from scratch as a 15th head at its post-move head 94f0925066a4d27c9c594ad5d1064fbceb632cea; and the earlier comment's numbers belong to the e487cd7b2a49 world, which is history now. The 14 heads themselves did not move — byte-identical between the 23:11:55Z snapshot and a 00:13:42Z re-read — so the changes below are main's, not the heads'.

    The queue: how little stands between each PR and main

    All 105 sibling pairs were merged head to head (merge-tree --write-tree; 35 clean, 70 conflicted; 3,312 hunks over 68 paths; 460 content, 20 add/add). The head-vs-head structure is invariant to where main is — 105/105 identical to the d3d9cd48e630 pass, 91/91 to the anchor pass, 67/67 to task-98. Sorted by how few paths a merger must resolve for that PR, with the deciding collision spelled out:

    PR paths deciding collision — the paths, their kind and hunk count its own width against 332d30fbb9d8 (extension) cap verdict
    1913 0 no sibling collision at all — clean with all 14 others 500 under by 0, exactly at the line, passes
    1406 1 packages/types/src/global-settings.ts content 1h (with 2226) — (conflicts with main) not priceable at this tip
    1410 1 packages/types/src/global-settings.ts content 1h (with 2226) — (conflicts with main) not priceable at this tip
    1411 1 packages/types/src/global-settings.ts content 1h (with 2226) — (conflicts with main) not priceable at this tip
    1412 1 packages/types/src/global-settings.ts content 1h (with 2226) — (conflicts with main) not priceable at this tip
    1928 1 src/core/tools/WriteToFileTool.ts content 2h (with 1406) 334 under by 166
    2226 1 packages/types/src/global-settings.ts content 1h (with 1406) 522 over by 22
    2357 1 knip.json content 1h (with 1410) 770 over by 270
    1562 1 src/extension/api.ts content 2h (with 1565) 2130 over by 1630
    1919 1 src/core/webview/__tests__/ClineProvider.sticky-profile.spec.ts content 1h (with 1921) 2216 over by 1716
    1567 1 src/extension/api.ts content 2h (with 1562) 2227 over by 1727
    1565 1 src/extension/api.ts content 2h (with 1562) 2230 over by 1730
    1921 1 src/core/webview/__tests__/ClineProvider.sticky-profile.spec.ts content 1h (with 1919) 2328 over by 1828
    1560 2 packages/types/src/global-settings.ts content 1h, src/core/webview/ClineProvider.ts content 1h (with 1406) 1707 over by 1207
    1557 2 packages/types/src/global-settings.ts content 1h, src/core/webview/ClineProvider.ts content 1h (with 1406) 1852 over by 1352

    Four heads now conflict with main itself, which no sibling order fixes: 1406 on src/core/webview/__tests__/ClineProvider.spec.ts + webview-ui/src/components/settings/__tests__/SettingsView.spec.tsx (2 paths, 3 hunks), and 1410, 1411, 1412 each on ClineProvider.spec.ts (1 path, 1 hunk). They overlap 20 of main's 33 new paths — the i18n settings locales plus those two specs — and conflict on only 2 or 1 of them; overlap is where a conflict is possible, merge-tree decides it. The nine one-path collisions are 1406×1928 (WriteToFileTool.ts 2h), 1406×2226, 1410×2226, 1411×2226, 1412×2226 (all global-settings.ts 1h), 1410×2357 (knip.json 1h), 1562×1565, 1562×1567 (both src/extension/api.ts 2h) and 1919×1921 (ClineProvider.sticky-profile.spec.ts 1h).

    The declared orders, simulated into a running tree

    Started from 332d30fbb9d8, merged sequentially, stopped at the first conflict, nothing past it merged or priced:

    order what happens step by step (charge per merge, extension) lands where it stops and who carries it
    A lead-preference-pairs-first 2357 (770) → 1913 (4) → 1565 (2230) → 1567 (57) → 1406 conflicts 4 (2357, 1913, 1565, 1567) 1406 at 4 paths / 5 hunks against 56a409aeecd6 — easonLiangWorldedtech/Zoo-Code:feat/change-journal-b2 carries it; rebase: merge or rebase easonLiangWorldedtech/Zoo-Code:feat/change-journal-b2 onto the running tip 56a409aeecd662388229e94b870a8cf0b5d742af and resolve 4 path(s) before that PR can land
    B reversed-pair-control 1913 (500) → 2357 (334) → 1567 (2227) → 1565 (32) → 1406 conflicts 4 (1913, 2357, 1567, 1565) 1406 at 4 paths / 5 hunks against 1d67f2ee4084 — easonLiangWorldedtech/Zoo-Code:feat/change-journal-b2 carries it; rebase: merge or rebase easonLiangWorldedtech/Zoo-Code:feat/change-journal-b2 onto the running tip 1d67f2ee40840fc44805c9321dd8e56437709fcb and resolve 4 path(s) before that PR can land
    C charge-ascending-overall 1406 conflicts 0 1406 at 2 paths / 3 hunks against 332d30fbb9d8 — easonLiangWorldedtech/Zoo-Code:feat/change-journal-b2 carries it; rebase: merge or rebase easonLiangWorldedtech/Zoo-Code:feat/change-journal-b2 onto the running tip 332d30f and resolve 2 path(s) before that PR can land
    D queue-head-first 1406 conflicts 0 1406 at 2 paths / 3 hunks against 332d30fbb9d8 — easonLiangWorldedtech/Zoo-Code:feat/change-journal-b2 carries it; rebase: merge or rebase easonLiangWorldedtech/Zoo-Code:feat/change-journal-b2 onto the running tip 332d30f and resolve 2 path(s) before that PR can land
    E measured-charge-ascending-15 1928 (334) → 1406 conflicts 1 (1928) 1406 at 3 paths / 5 hunks against 67e17653a631 — easonLiangWorldedtech/Zoo-Code:feat/change-journal-b2 carries it; rebase: merge or rebase easonLiangWorldedtech/Zoo-Code:feat/change-journal-b2 onto the running tip 67e17653a631993f2a16452166a5097a27f32352 and resolve 3 path(s) before that PR can land

    Both greedy searches (fewest paths first; tie-break by standalone width, then by the priced charge at the running tip) land four heads and stop: 1928 → 1913 → 2226 → 2357 (tip 922eaa93d47d) and 1928 → 1913 → 2357 → 2226 (tip bbe859feeeeb); 11 heads remain and none merges clean into either tip, the cheapest near-misses being 1406 at 4 paths / 6 hunks and 1411/1412 at 5 paths / 7–8 hunks. Taking 2226 early is what shuts 1565 and 1567 out.

    Whether any order avoids a conflict: checked exhaustively

    Sequencability requires every pair in a set to merge clean, so the ceiling is a clique over the 105 measured edges; all 2¹⁵ = 32,768 subsets were enumerated. The largest landable set is 5 PRs and it is unique: {1565, 1567, 1913, 1928, 2357}. All 120 orders of that set were then merged and priced merge by merge:

    • All 120 land all five (600 sequential merges, 0 conflicts) and all 120 produce the same tree dc7cf4f38e30c16d05c5c0c50603658202b6868d. Among these five, order never decides cleanliness.
    • Order decides who is charged, because each merge commit is priced against its own first parent: land 1913 last and the final merge charges 4 executable lines (headroom 496); 1565 last 32; 1567 last 57; 1928 or 2357 last 334 (headroom 166). Each of those endings occurs in 24 of the 120 orders.
    • No order keeps every merge under the cap: 0 of 120. The worst merge in a chain is 1567 (2227) or 1565 (2230) in every order — 60 each — because neither payload is in the tree until its own merge. Totals over the five merges: 3367 (the 60 best), 3395, 3427, 3455 lines; cheapest chain 1928 → 2357 → 1913 → 1567 → 1565 = 334 + 770 + 4 + 2227 + 32.
    • Onto the landed five (77f582450241d25bb02d136e40de54e351340e30, the 1928 → 2357 → 1565 → 1567 → 1913 chain, final merge 4 lines): 0 of the remaining 10 heads merges clean — 1562 at 1 path / 2 hunks, 1560 at 3 / 5, 1406 at 5 / 7, 1411 and 1412 at 5 / 8, 1410 at 6 / 9, 2226 at 10 / 39, 1557 at 15 / 65, 1919 at 16 / 251, 1921 at 17 / 289. So the measurement says no order lands this family at this tip, and I am not proposing one anyway.

    Advice a maintainer can act on (recommendation, not measurement)

    1. Land the five, and let 1913 close it. Every order of {1928, 2357, 1565, 1567, 1913} merges clean; 1913 last makes the closing merge cost 4 lines instead of 334, and 1913 is the head with nothing standing between it and main. If the concern is the total charged across the five merges rather than the last one, 1928 → 2357 → 1913 → 1567 → 1565 is the cheapest at 3367 lines.
    2. Rebase 1406 onto 332d30fbb9d8 before sequencing anything. It stops four of the five declared orders (C and D at their very first step, against main itself), sits in nine of the family's conflicts, and is the cheapest head to land after a five-head tip (5 paths / 7 hunks).
    3. 1410, 1411, 1412 need the same rebase — one path each (ClineProvider.spec.ts) — before their write-tool and global-settings.ts collisions are worth ordering.
    4. 1928 is cheap to land but unblocks nothing: 334 lines with 166 of headroom, clean with 10 of 14 heads, and landing it first still stops at 1406 (order E, 3 paths). Its conflicts are the same WriteToFileTool.ts / writeToFileTool.spec.ts lines 1406/1410/1411/1412 carry, so it belongs after those heads are rebased, not before.
    5. 1562 is the only other head worth riding along later — 1 path / 2 hunks (src/extension/api.ts) against the landed five; 1919 and 1921 are at 251 and 289 hunks and want their own landing.
    6. Do not read mergeable or a chain's printed width as evidence about sibling merges. All fourteen heads in the 23:11:55Z platform snapshot read mergeable: true with state blocked; the same fourteen read mergeable: null / state unknown at 23:46Z and were still null at a 00:13:42Z re-read, while their shas were byte-identical across those reads — the field flipped because main moved, not because anything about these branches' coexistence changed (1928's mergeable is not quoted here because this lane never read it) — that field is about main, and it is a queue for GitHub to compute, not a verdict on coexistence. Equally: 14 heads merging clean against main while 70 of 105 pairs conflict head to head is a disagreement class, not a contradiction.

    Nothing here has been given a synthesised resolution — conflicts are reported and the chain stops. Counts for this pass: 656 merge-tree attempts, 294 conflicted trees, 244 commit-tree writes, 76 merge commits checked out and priced, plus the 120-order sweep (600 merges, 600 commits, 600 gate runs) on pinned node v22.23.4 with git 2.54.0. Deliverable with the per-head tables: .tmp-cr-audit/.tmp-task101-refresh.md. No push, no review action, no changeset, nothing added to the tracked tree by this pass.

    In-place correction, third publication of this comment: the opening sentence first named one ref read twice and then named a third read that never happened. Both corrections touched that clause only — no figure, no path, no verdict in this comment moved. The exact date headers of all three publications and of every main read (00:46:30, 00:46:32, 00:48:55, 00:48:56, and this call's two) are recorded in .tmp-cr-audit/.tmp-task101d-1991-receipt.json; main was 332d30fbb9d8852a61b4512a5ce550e10cf84f24 at all of them.

    The #1521 mutation denominator, read Tue, 10 Nov 2026 01:26:59 GMT: the leg is closed, and the 31-file gap it leaves is main's debt now

    Main is a7157157239af0e9f3c544ff121dca83a5880274 at the ref read that timed this section ("feat(openai): add GPT-6.1 Terra support (#2379)"). Everything below is priced against 332d30fbb9d8852a61b4512a5ce550e10cf84f24 and its first parent d3d9cd48e630c91a85ad9ca2c8a1a7434f9d3e46 — the window #1521's content entered main through — so it does not move when main moves.

    The leg. mutation-diff, the Stryker advisory workflow on PR #1521: advisory, not one of the six ruleset-required contexts.

    The runs, from the check-run API at 01:17:01Z and 01:17:20Z. At the head that merged, b3ccfa4558df7c458bd6cc2765a2aacd77a4b71a (recorded base a15ba8c8ace93a5bba85da6d6883963e85f7614e, 25 commits, +428/−2, 33 files), run 129434237619 is completed / success, 2026-11-09T20:57:12Z to 21:04:14Z. At the earlier head dadac92706d333018b5fbbe0d14235c7bb65a508, run 126818992793 ended cancelled (2026-11-04T19:44:23Z to 19:47:06Z) and was replaced by run 126820531241, completed / success (19:47:56Z to 19:55:56Z). #1521 is not a blocked PR and its gate is not missing: it ran and it passed. The debt is about what "passed" covers.

    What the passing run priced, out of its own log and its own artifact — job log fetched directly (149,703 B, HTTP 200), artifact 13709124880 (1,113,856 B, sha256-16 6380a0787397f515, the same size the log reported):

    Mutation-testing 2 package(s) from merge base e487cd7b2a49: types (2 lines), extension (2 lines)
    

    and the table the gate built and never got to post (marker head:b3ccfa4558df7c458bd6cc2765a2aacd77a4b71a blocking:0 advisories:0, body 7,908 B, sha256-16 02024eb48890cea2, still sitting in the artifact as pr-comment.md):

    Package Changed executable lines Valid Killed Timeout Survived No coverage Result
    types 2 0 0 0 0 0 No mutants generated
    extension 2 2 2 0 0 0 Passed

    The artifact carries extension/mutation.json (3,270,852 B) with 2 mutants in shared/experiments.ts — replacements true and {}, both Killed, matched by replacement text rather than by the printed line number — and no types/ report at all.

    Cancelled files against priced files: 31 against 2. Reproduced in this unit with the copy of scripts/stryker-diff.mjs main itself carries (blob 7dfe0006d2d5…, 30,108 B, sha256-16 c332971815aecfaf; pinned node v22.23.4): in worktree scratch-task107a-head3 (HEAD b3ccfa4558df…, porcelain-clean) against a15ba8c8a…, and in worktree scratch-task107a-main3 (HEAD 332d30fbb9d8…, porcelain-clean) against main's own first parent d3d9cd48e630…. Both print 33 changed files, 2 priced — packages/types/src/experiment.ts (types, 2 changed executable lines) and src/shared/experiments.ts (extension, 2) — and 31 cancelled by the gate's own selection rules: 12 under __tests__ / fixture segments, and 19 under webview-ui/, which is where the feature's visible half lives: 17 settings-locale JSONs plus ExperimentalSettings.visual.tsx, its fixture and four screenshot baselines. Zero advisories, neither cap touched, the types half generating no mutants at all.

    This is a debt on main, not a blocker on a closed PR. #1521 landed as the squash 332d30fbb9d8…, and that commit does carry CI legs — I first published the opposite, and the correction is below. Paginated to the endpoint's own total_count at 02:04:44Z, 332d30fbb9d8 lists 784 check runs under 199 distinct names, and eight of those names are gate contexts: mutation-diff 129529165620, compile 129536540748 and 129529166430, knip 129536540694 and 129529166497, check-translations 129536540331 and 129529166319, platform-unit-test (ubuntu-latest) and platform-unit-test (windows-latest), e2e-mock 129529169375 and webview-visual 129529170716. All 784 rows carry head_sha equal to the commit asked about, and every one of those gate runs reads completed/success.

    The mutation leg's success is a skip, not a price. Job 129529165620 (run 42725268293, started 00:10:12Z, completed 00:10:16Z — four seconds) reports Set up job=success and Record merge-queue enforcement=success, then Checkout pull request merge result=skipped, Setup Node.js and pnpm=skipped, Test mutation gate logic=skipped, Enforce executable-line scope and run advisory mutation testing=skipped, Upload mutation reports=skipped, Link mutation report artifact=skipped, Complete job=success. The run carries zero artifacts, and its job-log endpoint returns 404 (106 B, sha256-16 edbd0367cec893df) at 02:05:27Z. The current tip a7157157239af0e9f3c544ff121dca83a5880274 reads the same way at 02:04:55Z: 439 check runs under 196 distinct names, the same eight gate names present, and mutation-diff 129557076910 (run 42733516473, 01:07:55Z to 01:07:58Z) with the identical step-by-step skip and zero artifacts. Main's ruleset 15494382 Protect Main requires six contexts — check-translations, platform-unit-test (ubuntu-latest), platform-unit-test (windows-latest), compile, knip, e2e-mock — and mutation-diff is not among them, read at 02:03:12Z, so the advisory framing of this leg stands.

    So the debt is narrower and sharper than what I first wrote down. Main does run CI over its copy of this content, including a mutation-diff leg that reports success; what never runs on a push is that leg's scope step, which is the only place the gate decides which changed files get priced. Nothing in main's copy of DTE-1 has been through scripts/stryker-diff.mjs and nothing will until a PR carries it. The only mutation denominator the campaign can quote for DTE-1 remains the one above: 2 valid mutants over 4 changed executable lines in 2 of the 33 files that landed, measured against merge base e487cd7b2a49.

    And the mistake, named: my first version of this paragraph counted 181 distinct check-run names on the squash, described all of them as reconcile / dispatch / resolve / comment mutation survivors automation, listed none of the gate contexts, and reported the current tip as reading 100 check-runs with 0 non-automation lanes at 01:27:00Z. That read stopped short of the endpoint's own total_count, and I then stated its result as a standing property of squash commits. The re-read above walks every page to the reported total (784 and 439 rows), keeps only rows whose head_sha equals the commit asked about, and prints step-level conclusions from the job records. The corrected statement is that main's squash carries the full set of gate legs and they skip themselves rather than never appearing. Everything else in this comment — the cancelled-file counts, the two priced files, the artifact contents, the two-mutant tally and both split plans — is measured in this unit and unchanged by this correction. A reader handed mutation-diff: success on a closed PR reads it as a pass over the merged content; it is a pass over two mutants, with 31 of the 33 landed files structurally outside the gate's selection. The job's own summary never landed either: Could not write the job summary: EISDIR: illegal operation on a directory, open '/tmp/stryker-summary-7LtvCu', which is why the table above has to be read out of the artifact rather than off the PR, and why the gate's prepared comment is invisible to anyone who did not download the artifact.

    What closing it would take — advice a maintainer can act on, not a measurement: either bring the webview half inside a source root the gate mutates, or record the gap as accepted, since the two priced files are already covered and re-running the leg on the current tip prints the same 2 lines and the same 2 mutants. Shape matters, and that finding is s3-1395's measurement, quoted to them: the workflow derives the gate's base from the head's first parent, so a two-parent merge head makes the gate price main's payload — they caught extension has 1325 changed executable lines (limit 500) from one — while a single-parent head parented on the tip prices the unit.

    Measured by f0-1528 in this unit, 01:17Z–01:26Z, at heads b3ccfa4558df… and 332d30fbb9d8…; run ids, the artifact id, the mutant replacements and the 31-file cancelled list are reproducible from .tmp-cr-audit/.tmp-task107a-denominator.json and .tmp-task107a-mutation-tally.json.

    Split plans for #1919 and #1921, priced against the current tip

    Both plans are priced at the tip a7157157239af0e9f3c544ff121dca83a5880274 with scripts/stryker-diff.mjs as it exists there — blob 7dfe0006d2d5c7e4d2282f65f68b8ed5f16866bc, 30,108 B, sha256-16 c332971815aecfaf, MAX_CHANGED_LINES = 500, MAX_MUTANTS = 400 — caps raised only in a private pricing copy. Units are single-parent commits, so the base the gate resolves for each is the parent sha named in the run record. Pricing instrument: 24 priced synthetic commits across the two heads plus 28 single-file probes; every figure below is printed by those runs, summed from printed rows, not recalled.

    #1919, head 05aa4e103858c9456c6a69b23e1ce0b45be6db97, 32 files, +15,611/-761, merge base e943530c848624bd3054c960be1e4fb0b658509a. CI's own refusal at that head is extension 2216 lines. Per-file extension charges at the tip: ClineProvider.ts 1,634, webviewMessageHandler.ts 289, registerCommands.ts 266, ProviderSettingsManager.ts 19, profileDeletionError.ts 5, ContextProxy.ts 3, ClineProviderFactory.ts 3, importExport.ts 2 (summed 2,221); types 14, webview 45. Declared unit order, each unit priced against the unit before it:

    • U1 types + webview contract — 9 files, 703 a+d, deciding webview 45 (types 14), clears 500
    • U2 settings persistence — 7 files, 471 a+d, extension 24, clears
    • U3 registerCommands + profileDeletionError + its spec + e2e restart test — 4 files, 1,966 a+d, extension 271, clears
    • U4 webviewMessageHandler + its spec — 2 files, 1,403 a+d, extension 289, clears
    • U5 ClineProviderFactory + shipped ClineProvider specs + src/package.json + eslint-suppressions.json — 9 files, 8,948 a+d, extension 3, clears
    • U6…U10 = ClineProvider.ts hunks 1-22 / 23-30 / 31-35 / 36-42 / 43-45 of 45 — 929, 662, 659, 823, 65 a+d, charging extension 450, 319, 338, 438, 26, each clearing 500 on its own.

    Ten units, all clearing the line cap. The whole chain priced against the tip prints types 14 / extension 2,216 / webview 45 �� CI's own number, so the units carry the whole of the head's charge. Main forces one thing: 37 of ClineProvider.ts's 45 forward patch applications against the tip fail, so its units were built by replaying the head-side hunks onto the merge base and three-way merging against main; after that merge the file lands as blob 9e7b218d81, not the head's d70299fba0. Any unit shipping that file must be rebased against main before it opens.

    #1921, head e4b102b2f129d07923eb5b8cf825074909ff4c4a, 55 files, +18,257/-791, merge base 73931fe8be253b93ec11103a0672c615e849f895. CI's refusal is extension 2,328 lines. Per-file extension charges at the tip: ClineProvider.ts 1,671, registerCommands.ts 329, webviewMessageHandler.ts 293, ProviderSettingsManager.ts 19, profileDeletionError.ts 5, ClineProviderFactory.ts 4, ContextProxy.ts 3, SkillsManager.ts 2, importExport.ts 2 — summed 2,328, CI's number file by file; types 14, webview 45. Declared order:

    • U1 types + webview contract — 9 files, 784 a+d, webview 45 (types 14), clears
    • U2 settings persistence — 7 files, 556 a+d, extension 24, clears
    • U3 registerCommands + profileDeletionError + spec + e2e test + test-utils/vscode.ts — 5 files, 2,752 a+d, extension 334, clears
    • U4 webviewMessageHandler + spec + ClineProvider.parallelMode.spec.ts — 3 files, 2,549 a+d, extension 293, clears
    • U5 SkillsManager + spec + locale-bundles.spec + src/package.json + eslint-suppressions.json — 5 files, 152 a+d, extension 2, clears
    • U6 ClineProviderFactory + shipped ClineProvider specs — 7 files, 8,933 a+d, extension 4, clears
    • U7 the 18 locale common.json files — 18 files, 18 a+d, nothing priced, clears
    • U8…U12 = ClineProvider.ts hunks 1-20 / 21-29 / 30-34 / 35-40 / 41-45 of 45 — 891, 763, 659, 625, 295 a+d, charging extension 444, 353, 338, 353, 133, each clearing 500.

    Twelve units, all clearing the line cap, and this head replays onto the tip byte-exactly: every hunk applied with no conflict and no fuzzy application, and the chain (tree 8924d8d9ad55) reproduces the head's blob for all 55 paths and prints types 14 / extension 2,328 / webview 45.

    The exclusion each of these two PRs would need instead, measured. The gate already cancels 20 of #1919's 32 files and 42 of #1921's 55 — every spec and test file, the 18 locale JSONs, src/test-utils/vscode.ts, packages/types/src/vscode-extension-host.ts, src/package.json, src/eslint-suppressions.json — so an exclusion aimed at the test or locale churn removes nothing from the capped count. Dropping ClineProvider.ts alone is not enough either: 587 remains on #1919 and 657 on #1921, both over 500. The minimal measured exclusion sets are ClineProvider.ts + webviewMessageHandler.ts (298 left) or ClineProvider.ts + registerCommands.ts (321 left) on #1919, and ClineProvider.ts + registerCommands.ts (328 left) or ClineProvider.ts + webviewMessageHandler.ts (364 left) on #1921. Each PR carries the same choice as its own comment.

    What is not priced here. No Stryker ran on these units, so the 400-mutant cap is unmeasured for all 22 of them; neither head ever reached a mutant in CI. The only printed density figure the campaign owns is u7-1918's 1910 unit — 355 changed executable lines, 336 valid mutants — and at that ratio 400 mutants corresponds to roughly 423 charge-lines, which would put #1919's U6 (450) and U9 (438) and #1921's U8 (444) on the wrong side. That is a prediction from another lane's printed row, not a measurement of these units, and cutting those three below 400 lines needs a re-price. Fidelity for both plans is content-level: every changed path lands in exactly one unit and the chains reproduce the head (exactly for #1921; merged against main for #1919's ClineProvider.ts), so no unit is a place where content is quietly dropped.

    Main moved during publication, and the plans were re-priced rather than re-asserted. At Tue, 10 Nov 2026 02:07:53 GMT main reads 18861b2e357203e6cb4d93930aa2ed122b1eecea (feat(friendli): fetch models from /v1/models (#2331), 23 paths, a+d 1,141) — main's sixth move, and the only capped file inside it is src/core/webview/webviewMessageHandler.ts. Re-priced against that tip with the same gate copy: #1919's U4 still charges extension 289 and #1921's U4 still charges 293, the files alone still charge 289 and 293, and the whole heads still price types 14 / extension 2,216 / webview 45 and types 14 / extension 2,328 / webview 45; ClineProvider.ts is the same blob (6cec3bdbe7) at both tips, so neither head's hunk-group boundaries are touched. The only figures that move are those two units' a+d — 1,403 to 1,434 on #1919 and 2,549 to 2,580 on #1921 — because main's own edit to that file now belongs to the unit's parent rather than to the unit's diff. Both plans above are widths measured against a7157157239af0e9f3c544ff121dca83a5880274, and nothing in either plan needed re-cutting because of the move.


    Fresh read from lane f0-1528 at Tue, 10 Nov 2026 02:59:31 GMT, against main 18861b2e3572 (18861b2e357203e6cb4d93930aa2ed122b1eecea, read Tue, 10 Nov 2026 02:58:46 GMT; unchanged since every figure above this block was priced, so none of those widths moves). Population re-listed 02:46:13Z: still 67 open non-draft PRs by this account, different membership — #1521 merged at 2026-11-10T00:19:32Z as 332d30fbb9d8852a61b4512a5ce550e10cf84f24, and 2393 and 2395 joined.

    PR state mergeable head at this moment declared base updated read moment
    1910 open true / blocked a6ce54e00bfb 73931fe8be25 2026-11-10T01:55:22Z 02:42:44Z
    1321 open true / blocked 504611564b61 (pushed 02:41:41Z) 5319fe91fb0c 2026-11-10T02:41:41Z 02:42:45Z
    2393 open true / blocked 9c08f7bafdd6 e487cd7b2a49 2026-11-10T00:54:07Z 02:42:46Z
    2395 open true / blocked e8dce4f5b452 332d30fbb9d8 2026-11-10T01:34:26Z 02:42:47Z
    1406 open false / dirty c1b051b2786a 3ee042d91e0d 2026-11-10T00:31:28Z 02:42:47Z
    1410 open false / dirty 5e9f608204f5 3ee042d91e0d 2026-11-10T00:30:39Z 02:42:48Z
    1411 open false / dirty 029c25ac314a 3ee042d91e0d 2026-11-10T00:30:18Z 02:42:49Z
    1412 open false / dirty dd8b4fd6a730 3ee042d91e0d 2026-11-10T00:31:47Z 02:42:49Z
    • What this changes about the line above reading "Four heads now conflict with main itself": it was true when I wrote it and it is still true of the pushed heads — 1406, 1410, 1411 and 1412 read mergeable: false / dirty against 18861b2e3572 in the table above — and it is no longer true of the prepared heads, which merge clean against the same tip with zero conflict rows, verified here by git merge-tree --write-tree at 02:4xZ: e3dd44cb0b65 → tree 8429f5539fc0, 346c658c878e → a21f6ea77a3f, 77427134a04f → fd91f13bc120, 42665b0b5088 → 40717801c135. The blocker is pushing a head that already merges, not re-planning the order.
    • No verdict is inferred here from an older read. 1910, 1321, 2393 and 2395 were not rows in any table above, so they get fresh rows and no carried figures; their mergeable_state: blocked is the branch-protection state and not a content judgment. 1321's head moved 89 seconds before this read, so every 1321 width quoted above this block is priced at 9b6dc1a639db and is stale until re-priced at 504611564b61.
    • Census re-run at the same moment, for the record: reading A COLLISION 2 (1929 d11e2505b948, 1930 d4f3e9072eea), no-mention 65; reading B 0 renames; reading C 14 heads / 39 .abort assignment lines in test paths; mergeability against 18861b2e3572 59 clean / 8 conflicting (1404, 1406, 1410, 1411, 1412, 1538, 1652, 1653). Rows in .tmp-cr-audit/.tmp-task112-census-rerun.jsonl.

    — f0-1528
    (Addendum from lane f0-1528, stamped at the read this write was built on: Tue, 10 Nov 2026 03:07:46 GMT.) The stale marker above on 1321 does not leave a measurement owed by me, and I am correcting my own sentence rather than letting it imply a job: u7-1918 has already priced at the delivered head 504611564b612b7869ca93e8f529700243031588, in comment 6755230244 on #1321 — not on this issue — authored 2026-11-10T00:23:47Z and updated_at 2026-11-10T02:41:41Z, 19267 B, sha256-16 c621560eb657ecae…, which is where its own Delta at head 504611564b61 section reads, in its words: Surviving/uncovered went 10 → 2 (webview). Killed went 21 → 29 (webview) … Valid stayed 31 and changed executable lines stayed 37 (webview), and for the other arm The extension row still prints 282 changed executable lines against the gate's 500-line cap, unchanged from the 9b6dc1a639db report, with the leg recorded incomplete (extension row 282 with 0 valid, CI log core Stryker preflight could not start: … ENOENT). Two things a reader must carry with that pointer: the price is for a merge candidate — its log prints Merge 504611564b612b7869ca93e8f529700243031588 into 18861b2e357203e6cb4d93930aa2ed122b1eecea at checkout e225aa8c4ea6b11ff9731d6208e6075abec7c41a, so the second parent is what CI had for main, not necessarily today's tip — and the extension arm is still 0 valid, so it prices the width and not the mutation outcome.

    So the correct use of my stale marker is the pointer, not a duplicate measurement: the widths I called stale are stale in favour of 6755230244 at 504611564b612b7869ca93e8f529700243031588, and producing a second number for the same head from my own run would be a vote rather than a measurement. I took no figure of my own at that head and opened no install-matched worktree for it, so there is no number of mine sitting beside his, and the residual I recorded in my own ledger as owed — 1321's re-price — is discharged as a cross-reference, with the cross-referenced comment read from its own body at the moment stamped above rather than from anyone's summary of it.


    task-117 control result — f0-1528, tip read 2026-11-10T03:21:16Z, passes run 2026-11-10T03:23:29Z and 03:23:31Z

    A control run, not a review action: 1652's abort-versus-timeout assertion shape applied to openai.ts at main 18861b2e357203e6cb4d93930aa2ed122b1eecea. Nothing pushed, nothing opened, no verdict cast on 1652.

    Assertion shape from opencode-go.spec.ts at 1652's head 04c981b39c716b60cdc618415108d67f159415a3; harness from the tip's own openai.spec.ts (mocked openai constructor exposing chat.completions.create, mocked ../utils/timeout-config). Arriving-error shapes measured, not remembered: openai@5.23.2 core/error.mjs:67–85 plus a live construction print APIUserAbortError → name "Error", message "Request was aborted.", no status and APIConnectionTimeoutError → name "Error", message "Request timed out.". Instrument: worktree scratch-task112-inst at the tip on that head's own frozen install (13 node_modules dirs), pinned node 22.23.4, pnpm exec vitest run api/providers/__tests__/openai-abort-timeout-control.spec.ts from src; scratch spec 6,771 B f344a9877e269258, untracked and removed after the run, worktree porcelain 0 rows after removal; every GIT_* key stripped on both rows — gitKeysStripped=GIT_PAGER, gitKeysRemaining= empty, 0 environment-guard lines. Both passes: exit 1, Test Files 1 failed (1), Tests 3 failed | 2 passed (5); the two logs are identical apart from millisecond timings.

    cell 1652's assertion form, applied to openai.ts printed result
    1 · abort arriving at the request call surfaced error has name === "AbortError" FAIL — received name "Error", message "OpenAI completion error: Request was aborted."
    2 · abort and timeout arriving at the call the two surfaced errors differ by name FAIL — both names "Error"; the messages do differ: …Request was aborted. against …Request timed out.
    3 · abort arriving mid-stream surfaced error has name === "AbortError" FAIL — received name "Error"; the thrown object is otherwise surfaced unchanged (asserted, passing)
    4 · non-abort error mid-stream rethrown unchanged, toBe the thrown object PASS
    5 · signal already aborted before the call the request still begins, caller's signal forwarded PASS — 1 call, capturedSignal === controller.signal

    What this proves, and where it stops. The failing half is 1652's abort-identity point, and it fails at the twin in both timings. Mid-stream, because the SDK's own abort error arrives with name "Error" and nothing normalizes it — openai.ts prints 0 AbortError rows at the tip and its for await loop at line 222 sits outside any catch. At the call, because line 206 routes the error through handleOpenAIError into handleProviderError (2d32ff62e294cccc864a761abcfbdbb432f77367, 4,748 B, 0 rows naming abort or timeout), which returns a fresh new Error("OpenAI completion error: …") and preserves only status, errorDetails, code and $metadata — class gone, name gone, and the abort-versus-timeout distinction surviving only as text inside the rewritten message. On that half the family is in scope, base-openai-compatible-provider.ts shares the wrapper (3 mentions at the tip), openai-codex.ts is named as a partial counter-example because it skips the wrapper and prints 9 AbortError rows, and openai-native.ts carries the SDK-level gap without the wrapper and was read rather than run.

    The passing half is 1652's fast-fail point, and it does not carry over: openai.ts does no awaitable work before the request — 0 rows for getModels, resolveModel or fetchModels, and the only await in the preamble window is the SDK call itself — so a caller whose signal is already aborted reaches create() without paying a model-catalog fetch, which is the hazard 1652's pre-flight guard exists to close in a provider that does resolve a catalog first. That arm is provider-local. If a lane later widens 1652 across the OpenAI family, this row is the one that should hold the widening to abort identity and stop it at fast-fail.

    Two instrument faults recorded rather than hidden: the first run failed 5 of 5 on my own mock (() => ({…}) is not a constructor at openai.ts:89) until I copied the tip spec's function () { return {…} } shape, and in the second version cell 2 threw the abort error in both arms, so its name comparison was comparing abort against abort until it was fixed before this publication. Every figure above is a printed row; the runs are .tmp-cr-audit/.tmp-task117-run1.log and -run2.log, the spec is .tmp-cr-audit/.tmp-task117-control-spec.ts.

  13. easonLiangWorldedtech commented on Nov 10, 2026

    @easonLiangWorldedtech
    ContributorAuthor

    U5c-2 on the record before it opens: boundary, fidelity contract, and the price at the tip it was cut from

    This is the planned-and-issued entry for unit U5c-2 so the plan exists here before the PR does. It cites rather than repeats the two earlier entries: the split plan of record (#issuecomment-6751466344, 18,330 B, 2026-11-09T21:06:35Z) and the per-unit currency table (#issuecomment-6753021473, 10,543 B, 22:21:08Z, which carries U5c-2 at 26 counted lines / 3 selectors in extension). Nothing below re-derives those; what is new here is the declared boundary, the declared fidelity contract, and a re-price against a named tip, because main has moved four times since that table was written: e487cd7b2a49 → ba105e65cc74 → d3d9cd48e630 → 332d30fbb9d8 (ancestry confirmed, git merge-base --is-ancestor exit 0 for each pair, printed 2026-11-10T00:37:55.842ZZ).

    1. Unit boundary — exactly two paths, both owned by no other unit

    path 2357 blob a/d vs the base what the unit does to it
    src/utils/jsonFileRecovery.ts 1ad95dbdd8a98b76df3371250faeb61542171f5a +45 / −2 refuses an exact mtime tie between two leftover backups; adds AmbiguousBackupRecencyError
    src/utils/__tests__/jsonFileRecovery.test.ts 57b2c33e9af5446bcec00ebdf3938105032e617b +128 / −1 five rows, named in §3

    Total 176 a+d (173 insertions, 3 deletions). No third path. src/utils/fileLock.ts, src/utils/safeWriteJson.ts, src/eslint-suppressions.json, knip.json and every turbo.json stay at their base blobs — they belong to U5c-1, U5c-4 and U5c-8. No changeset, no CHANGELOG row (AGENTS.md), no new Stryker disable directive anywhere in the diff.

    2. Fidelity contract

    Port 2357’s rows verbatim; add nothing, widen nothing, rename nothing. The behavioural content is one decision: recency between two leftover backups is decided by mtime and nothing else, so an exact tie carries no evidence, and recovery must refuse rather than pick by readdir order — it restores nothing and deletes nothing, so both copies and their evidence survive for a human. That is the same fail-closed rule recovery already applies to a backup it cannot stat. The error carries code: "AMBIGUOUS_BACKUP_RECENCY" plus targetPath, mtimeMs, firstBackupPath, secondBackupPath, and its message names basenames only, because error telemetry captures message and stack and a workspace path there would carry a local username into that capture. If a reviewer asks for a different tie policy (oldest wins, mtime+size, refuse-then-require-human), that is a design change to the recovery contract and belongs on this issue, not smuggled into a unit PR.

    3. The five proof rows, quoted by name from the ported spec

    1. refuses an exact mtime tie between two backups instead of picking by directory order — tie set with fs.utimes at a fixed 1,700,000,000,000 rather than raced against the clock; asserts the rejection, the code, the reported mtimeMs, that the message states the magnitude it refused to guess about, and that both backups are still on disk with the canonical path still absent.
    2. restores the newer backup when two backups have different mtimes — the control that stops the tie rows from being a test which merely asserts “two backups means reject”; the loser of a decided race is still garbage-collected.
    3. restores the newest backup when two OLDER backups share a tie — 200 / 100 / 100 mtimes: the tie sits below the winner, so recovery still restores and removes both losers. This pins the guard so it cannot be widened into “any duplicate mtime means refuse”.
    4. refuses a tie through the reader entry point and keeps both copies — the same state driven through recoverMissingJsonFile, asserting the rejection and the persistence-trace rows.
    5. keeps the tied paths out of the tie error message and on the error properties — expect(tie.message).not.toContain(path.sep) with the full paths asserted on targetPath / firstBackupPath / secondBackupPath.

    4. Independence from the unit that merges before it, measured

    The file imports fs/promises, path, ./errorCode, ../core/task-persistence/persistenceTrace and withFileLock from ./fileLock — all present at the base tip — and no part of it uses U5c-1’s canonical-key or artefact-spelling API. pnpm --dir src run check-types (tsc --noEmit, tsconfig at src/tsconfig.json, node v22.23.4, matched pnpm 10.8.1 install of the tip) exits 0 on this tree, so the unit is not waiting on U5c-1 to compile. Merge order stays as planned: U5c-1 (#2393) lands, then U5c-2; the branch is cut from main rather than from U5c-1’s head, so no reviewer or bot ever sees a cumulative diff.

    5. Price, with the tip named beside every width

    Priced with the gate’s own exported selector (blob 7dfe0006d2d5c7e4d2282f65f68b8ed5f16866bc at all four tips; MAX_CHANGED_LINES = 500 line 11, MAX_MUTANTS = 400 line 12):

    • Priced against d3d9cd48e630c91a85ad9ca2c8a1a7434f9d3e46 — the commit 7f4a40a58e2f88be65c60531919f87c53e154961 was cut from and the merge base the gate reports (selectFromGit printed mergeBase == baseSha == d3d9cd48e630…, advisories: []).
    • Deciding package: extension (packageForPath maps src/utils/jsonFileRecovery.ts there). 26 counted executable lines of 500, 3 selectors of 400 (utils/jsonFileRecovery.ts:110-128, :215-215, :220-225). Headroom 474 lines and 400 mutants.
    • src/utils/__tests__/jsonFileRecovery.test.ts charges 0 counted lines because packageForPath (arity 1) returns undefined for __tests__/ paths — named, not averaged.
    • Current main is 332d30fbb9d8852a61b4512a5ce550e10cf84f24 (feat(settings): dynamic thinking effort experimental toggle (DTE-1), 33 paths, read 2026-11-10T00:37:55.843ZZ). It touches neither path in this boundary — both of the unit’s base blobs are byte-identical at all four tips — and git merge-base upstream/main 7f4a40a58e… still answers d3d9cd48e630…, so the widths above are the widths the gate will compute against today’s main as well, and they are unchanged from the 26 / 3 in 6753021473. Any figure in this comment was priced at d3d9cd48e630… and says so.

    Pushed state and local mutation band for the pushed tree are recorded on the PR body when it opens, each with its exit code and printed hash pair. Nothing on this issue changed as a result of this comment; it is a record, not a request.

  14. easonLiangWorldedtech commented on Nov 10, 2026

    @easonLiangWorldedtech
    ContributorAuthor

    Unit boundary issued before the branch exists: scaffold U5c-7 cut in two, observation first. Main read from git/refs/heads/main at 2026-11-10T01:35:28.814ZZ = a7157157239af0e9f3c544ff121dca83a5880274 in the same call that posts this; the branch will be rooted on the tip read in the same call that pushes it, as a single-parent commit, because s3-1395 measured that CI sets the gate's base from the head's first parent, so a two-parent merge head would make the gate price main's payload rather than the unit.

    The cut. src/core/task/observationRegistry.ts (blob b3b7a719871d45ac9bb7d08159e017a5b58a3f76, 3,010 B / 85 lines, export interface FileObservation at line 13 and export class ObservationRegistry at line 27, header: “pure in-memory — zero I/O, no dependencies”) with its own src/core/task/__tests__/observationRegistry.spec.ts (blob a7813fc2bb7b36a4a439f326f831c1636a58a9c5, 4,084 B / 133 lines, 3 describe blocks and 13 it blocks; its only imports are vitest and ../observationRegistry). Neither path exists at a7157157239a…, so both are new files and the unit is 216 additions / 0 deletions over 2 paths. Content is ported verbatim from 2357's d6fa7117e07b70455da24e4722b47eb0b202fa6e: fidelity = byte-identical blobs, no lane-authored lines in this unit.

    Currency, measured rather than inherited. Priced tonight against 332d30fbb9d8… as a single-parent commit (7d33c51aa4…) cut from that tip, so its merge base was the tip: extension 20 counted executable lines of 500, 10 selectors of 400, advisories: []. Re-measured against a7157157239a… in the priced tail (r28e-unit-prices.json): same 216 a+d, same 20 / 10, because main has not touched either path. Under the 400 soft bound, well clear of both hard caps. Charges per 6753021473 as corrected in place at 01:21:39Z against this same tip (its numbers follow the scaffold file sets; the topics on rows U5c-3 to U5c-8 were relabelled there, which is why this cut is named as half of scaffold U5c-7 and not as “U5c-5”).

    Why it is a unit and what it is a prerequisite of. The registry is a leaf: nothing at a7157157239a… imports it, and it imports nothing, so this cut changes no existing behaviour and answers no reviewer row on any open PR — what it carries is the FileObservation version/completeness record that 2357's Task.ts consumes (+33 lines, priced separately at 168 a+d, 5 / 5, commit 5aaf1b9f85…) and that the guarded-write work described in 6751466344 sits on top of. Ordering consequence, stated plainly: the Task cut has to be based on this one, because Task.ts imports ./observationRegistry and the tip does not have that file. If a reviewer would rather see the two cuts as one unit (384 a+d, 25 / 15 measured for the pair), that is a cheaper review event but a larger diff; I am opening the smaller cut and will fold if a maintainer asks.

    What will be proved before the push, and what I will not claim yet. The mutation band will run on this unit's own selectors with the mutant count printed against the 400 limit rather than left to be inferred; the gates (the declaring package's own check-types, eslint with prune-revert proof on the touched files, prettier, knip, the misc and unit suites) run in the install-matching worktree on pinned node v22.23.4; and the control keeps the spec and reverts the production change, which must fail with named rows and assertion texts — for a brand-new file that control has two shapes, deleting the file (the suite cannot load it) and neutralising its behaviours (named assertion failures), and I will report whichever the legs print. One thing I am deliberately not claiming before the gate runs: whether knip is content with a file whose only consumer is its own spec. If it objects, that is a finding about this cut, and I will bring it back rather than export something artificial to satisfy it.


    Boundary changed, and this paragraph is the planned-and-issued record for the change, posted before the push. The smaller cut ran into a required context and the fold is now the unit.

    What CI printed. knip is one of the six contexts ruleset 15494382 requires, and at head 0975cb445afd… it failed (job 129579712187): after pnpm test:knip-policy passes every subtest — including ok 4 - a production file imported only by tests is still reported as unused — knip --production --treat-config-hints-as-errors printed Unused files (1) → src/core/task/observationRegistry.ts → ELIFECYCLE Command failed with exit code 1.. Main at 18861b2e3572… is completed/success on that same context (job 129577571680), so the row belongs to this cut, not to the tree it sits on. The reason in words: this unit shipped a production file whose only non-test importer was its own spec, and the repo asserts by policy that a test-only importer is not an importer. Stacking the consumer behind #2396 was the other option I offered, and it fails on its own terms — it would leave #2396 permanently red on a required context while the stack could not help it.

    The third path, named. The unit now also carries the scaffold U5c-7 consumer half: src/core/task/Task.ts (+33), src/core/task/__tests__/Task.dispose.test.ts (+50) and src/core/task/__tests__/Task.spec.ts (+85), so all five scaffold paths are in one commit. The consumer is not decorative — at the source head Task.ts line 109 is import { ObservationRegistry } from "./observationRegistry", line 302 is readonly observationRegistry = new ObservationRegistry(), and line 3788 is this.observationRegistry.close() on disposal, which is the semantics this unit's close() rows assert.

    New currency, re-measured at the base being pushed onto. Base and merge base are 18861b2e357203e6cb4d93930aa2ed122b1eecea (upstream/main read in the same session; it has not moved since #2396 was opened), the fold commit is b9b85debe3… with that single parent, and the gate prints extension 25 counted executable lines of 500 and 15 selectors of 400 for 384 additions / 0 deletions over 5 paths — Task.ts contributes 5 executable lines (302, 406, 3617, 3631, 3788) and the registry its 20. zdt split verify on the re-derived five-path contract (r30-contracts/U5c7fold.json, base 18861b2e3572…, head b9b85debe3…) exits 0 with standalone: 384 a+d / 5 files UNDER-SOFT and all five rows OK (content subset of source) … byte-identical; all five head blobs equal their source blobs by git hash. The band on all 15 union selectors printed 11 mutants instrumented from the 15 union selectors — Task.ts's five selectors contribute three (UpdateOperator at 3617 and 3631, CallExpression at 3788; the field initialiser at 302 and the statement at 406 instrument to nothing) — and the real run killed all 11, exit 0, tally {"Killed": 11}, 0 survived / 0 timed out / 0 no-coverage — against the gate's 400-mutant limit, so Task.ts's five selectors did move the mutant count and I am pricing it rather than letting anyone discover it. The gate's one advisory at this head is src/core/task/Task.ts:4458: mutation exclusions must use "// Stryker disable next-line MutatorName: concrete reason"…, and it is not content this unit introduces: the very same directive text sits on main at line 4425, this unit's 33 added lines shift it to 4458, scripts/stryker-diff.mjs:238 validates directives against every line of a changed file rather than the changed lines, and :86-87's regex rejects a mutator list written with a space after the comma (ConditionalExpression, EqualityOperator), so a pre-existing, well-reasoned exclusion is reported the moment its file enters the diff. It is advisory, not blocking, and editing someone else's directive is a maintainer's call, not a split author's.

    Two corrections to my own earlier record, made here rather than quietly. First, the line counts in the paragraphs above said 3,010 B / 85 lines and 4,084 B / 133 lines; the LF counts are 84 and 132 — I counted a phantom line after the final newline (84 + 132 is exactly the 216 additions). Second, I told you the gate declares vitestConfig: "vitest.stryker.config.ts"; that is line 53, which belongs to the webview package entry rooted at webview-ui, and webview-ui/vitest.stryker.config.ts does exist. The entry that governs this unit is line 62 — vitestConfig: "vitest.config.ts" for the extension package — and src/vitest.config.ts exists, so this round's band runs with the config the gate actually passes, and last round's vitest.core.config.ts deviation was my misreading of which entry applied, not a gap in the repo.

    What the next push does and why it is cheap. One push, replacing #2396's head with the folded commit: knip failure → success, 216 → 384 a+d, 20 → 25 counted lines, 10 → 15 selectors, and the mutation row staying blocking: 0 if CI reproduces the band. It costs no review: the PR has zero review rows, CodeRabbit's row is skipped on its label gate (coderabbit-review-active, workflow-managed, so I added none), and there is no approval to dismiss — while the alternative is leaving a merge-blocking red standing on a required context. Hooks run with no --no-verify, and the head comes back from GET.

    Gates run against this head, and the numbers behind the claim. CI's exact knip sequence, run in the install-matching worktree wt-r304-u5c2-matched on pinned node v22.23.4 with the three script spellings taken from git show b9b85debe3:package.json (the ref under test, not the checkout on disk — all thirteen workspace node_modules trees are present there and @roo-code/* links realpath inside the worktree): at the fold head pnpm test:knip-policy → # tests 8 / # pass 8 / # fail 0 exit 0, pnpm knip exit 0, pnpm knip:production exit 0; and in the same worktree after git checkout --detach 18861b2e3572… the same three blocks exit 0/0/0 as the control, with HEAD restored afterwards to b9b85debe3… and git status clean. So the fold is knip-clean in the form CI actually runs, and the main control is quoted rather than assumed. The mutation band on the union's selectors instrumented 11 mutants and killed all 11, exit 0. The control keeps the specs and the consumer and deletes the production file: Test Files 2 failed (2) with Serialized Error: { code: 'ERR_MODULE_NOT_FOUND' }, and pnpm --dir src run check-types exit 1 printing core/task/Task.ts(109,37): error TS2307: Cannot find module './observationRegistry', core/task/__tests__/Task.dispose.test.ts(6,37) and core/task/__tests__/observationRegistry.spec.ts(3,37); restoring the file returns 29/29 green, and the restored blob pair 6d057ff9c1b150e3 was re-printed after every mutant. Three named mutants were run by hand too: removing the if (this.retired) { return } guard gives Tests 2 failed | 27 passed (29); dropping this.retired = true from close() gives Tests 3 failed | 26 passed (29) — close drops every entry and reports the registry closed (expected false to be true), observe is a no-op after close… (expected 1 to be +0) and Task dispose method > clears the per-Task observation registry on disposal (expected false to be true); flipping complete: boolean = true to false gives Tests 2 failed | 27 passed (29) on expect(obs.complete).toBe(true).
    dalone at the tip, and the confinement question closed by measurement.** Paths src/services/mcp/McpHub.ts and src/services/mcp/__tests__/McpHub.spec.ts, no third path. Against main at 18861b2e357203e6cb4d93930aa2ed122b1eecea the unit is 201 additions and 10 deletions over 2 files (211 a+d): McpHub.ts 68 lines of change (59 added, 9 deleted, 12 hunks, 88,603 B / 2,553 lines at main becoming 91,403 B / 2,603) and McpHub.spec.ts 143 (142 added, 1 deleted, 5 hunks, 110,947 B / 3,468 becoming 117,809 B / 3,609). The content is the scaffold's McpHub rows from #2357, source pair 5319fe91fb0caf...d6fa7117e07b..., and it ports byte-identically because main's blobs for both paths are the same blobs the source pair starts from (c973f35a03bac9d790adbf2b6a39fb6a6b4ff847 and 35ff10d64b124c97cccd26021504caa551050688), so the unit's head blobs are the source head blobs 50ae26ebb32029c1a1099197868ea3f66ca63d12 and b6aacddc94128dd24ff5f1f8ef6cdec5cb948adc. Priced earlier at fd9a5efb3b by scripts/stryker-diff.mjs at 27 counted executable lines and 14 selectors, inside both caps; the contract will be re-derived and re-priced against the candidate head before the push, and the mutation band run before the push too.

    Why this unit does not share code with #2385, measured rather than assumed. canonicalizePathUnderRoot and canonicalKey print 0 rows at main's tip, 0 rows at #2357's base 5319fe91fb0c, and 0 rows at #2385's head 970b3a47ba69; they print exactly 1 row at #2357's head d6fa7117e07b, and that row is line 1 - the // npx vitest run ... header comment - of its new src/utils/__tests__/fileLock.canonicalKey.spec.ts. McpHub.ts and McpHub.spec.ts contain 0 rows for fileLock, file-safety, canonical or lockKey at all four of those refs. Main has no src/services/file-safety directory at all (0 paths); #2357's head and #2385's head each carry three (safeWriteText.ts and its two specs). #2385's own tip commit is 13 additions and 1 deletion on src/services/file-safety/safeWriteText.ts plus a new 100-line src/services/file-safety/__tests__/safeWriteText.spec.ts. So U5c-6 and U5c-2/the canonicalKey unit touch disjoint files, the fileLock.canonicalKey.spec.ts rows ride with the canonicalKey unit rather than with McpHub, and this cut needs no dependency on #2385: it roots on a single parent at the tip read from git/refs/heads/main in the same call that reads the remote ref before the push.

    Correction to the paragraph above, made here rather than quietly: the confinement type, not the lock key, is what ties this cut to the chain, and the plan's own unit number for it is U5c-8. Textually the two McpHub paths still share no lock-key code with #2385 - the four-ref row above holds. The dependency is the option they pass. confineTo prints 0 rows anywhere at main's tip 18861b2e3572 and 51 rows at #2357's head d6fa7117e07b; the tip's src/utils/safeWriteJson.ts is blob e54b8c51d3c34a7f1b3c7eb4b21f45c4bb9b9e90 (11,635 B / 286 lines) and its SafeWriteJsonOptions at line 24 carries prettyPrint, merge and trace only, while #2357's version (blob f4edc7ee9c8c168da844730742a54f682bd00a70, 28,601 B / 621 lines) declares confineTo?: string at line 78. That rewrite in turn imports resolveLockKey and resolvePublishTarget from ../services/file-safety/safeWriteText, and main has 0 paths under src/services/file-safety at all; in this campaign that module exists only through #2385, whose head 970b3a47ba69 exports resolvePublishTarget, resolveLockKey and safeWriteText. Measured with the instrument CI uses for its compile context (src/package.json declares check-types as tsc --noEmit; run by typescript 5.9.3 under node v22.23.4 in the install-matched worktree wt-r316-u5c6): the candidate at commit abffb182d405a1ee2de52ad1501c4ea02661f930 exits 1 with 6 error lines - McpHub.ts(2142,5), (2232,6), (2445,5) TS2353 and McpHub.spec.ts(1263,22), (1292,22), (1346,22) TS2339, every one of them 'confineTo' does not exist on type 'SafeWriteJsonOptions' - and the tip control in the same worktree and interpreter exits 0 with 0 error lines. Fidelity and budget are clean on this cut: zdt split verify against that head passes with both rows byte-identical to the source pair, and split measure prints 201 additions and 10 deletions = 211 a+d over 2 files, UNDER-SOFT; the gate prices it at 27 of 500 counted executable lines and 14 of 400 selectors against the tip, all 14 inside services/mcp/McpHub.ts.

    So the record now reads: U5c-8 (McpHub write confinement), not U5c-6, and it is planned to land last, not first. This tracking issue's own plan table assigns src/services/mcp/McpHub.ts plus src/services/mcp/__tests__/McpHub.spec.ts at 27 of 500 and 14 of 400 (211 a+d) to U5c-8, and U5c-6 to src/core/tools/__tests__/readFileTool.spec.ts with the suppressions row at 1,140 a+d; the dependency paragraph in this issue says U5c-8 lands after U5c-3 because 8 of McpHub.ts's 67 changed rows name safeWriteJson and the change relies on safeWriteJson resolving the publish target before staging beside it, and the declared order is U5c-1, U5c-2, U5c-3, U5c-4, then U5c-8. The cut is built and priced (branch vps2f-1914-u5c6-mcphub, candidate abffb182d4, hooks proved, blob proofs held: 50ae26ebb32029c1a1099197868ea3f66ca63d12 and b6aacddc94128dd24ff5f1f8ef6cdec5cb948adc, equal to #2357's head blobs) and it is being held off the fork rather than pushed to a red required compile: it needs U5c-3 (safeWriteJson plus its 2 specs, priced 614 a+d at 131 of 500 and 46 of 400, not yet cut), which needs U5c-2 (#2395) and U5c-1 (#2393), and U5c-3's own dependency src/services/file-safety/safeWriteText.ts comes from #2385, all four of which are still open. Nothing here changes the numbers above; it moves where this unit sits in the chain.

  15. easonLiangWorldedtech commented on Nov 10, 2026

    @easonLiangWorldedtech
    ContributorAuthor

    Deferral rows for lane s3-1395, task-106 (four dirty PRs re-rooted onto 332d30fbb9d8) — 2026-11-10T01:43:18Z

    Five things this lane is not doing on 1406 / 1410 / 1411 / 1412, each named with its reason, so "not verified" is on the record instead of being mistaken for "verified clean". Figures below were printed by the commands named beside them in this sitting.

    DEFERRAL 1 — four-run knip: the original protocol wording is gone; the lead-issued definition runs in its place

    • What is owed: "knip's four-run protocol" appears in my ledger as an owed item (43 rows reference it) and in the Lead's records as an owed item, and neither side still holds the original definition text. Recorded as lost, not reconstructed from memory.
    • What runs instead, re-issued by the Lead at 01:19Z and implemented verbatim in .tmp-cr-audit/s3lane/task106-gates3.mjs: (a) a run at the head, (b) a run at the head's merge base, (c) a second identical head run, (d) a third head run after git checkout --force <head> + git clean -fd on the same tree, with per-run sha256-16 of the captured output compared pairwise. A pass 2 run at the four pre-fix heads already printed a_eq_c=true, a_eq_d=true and a_ne_b=false — four identical outputs, 339 B, exit 0, at f9da7e5b6def07b2 — which is why I say "no run-to-run variance and no head-versus-base difference in knip's eyes" rather than "knip is deterministic", and why (b) is captured once at 332d30fbb9d8 for all four: all four merge bases are that same commit.
    • Cost of the deferral: none for the heads (they run); the deferral is only about the wording, so if a maintainer meant a different four (e.g. per-package --config variants) the runs must be re-issued.

    DEFERRAL 2 — whole-repo pnpm test and the coverage lanes are not run on these heads

    • What runs: the declaring package's own CI lanes, whole — pnpm --dir src run test:core, test:services, test:misc (lane assignment derived from each src/vitest.<lane>.config.ts include globs, 0 unclaimed specs across the four heads) plus the full pnpm --dir webview-ui run test, and the packages' check-types through turbo (--filter=zoo-code, --filter=@roo-code/vscode-webview), with @roo-code/types#build as the graph demands.
    • What does not: the 13-package pnpm test fan-out, test:coverage:*, test:dist, verify:coverage-contract, verify:coverage-cache-inputs, verify:build-cache-inputs, and the @roo-code/core unit/integration lanes. Reason: those are CI's fan-out, they do not price the unit delta, and a full local fan-out on this host costs more than the focused lanes while proving less about my content; changed-line coverage is CI's binary item, not mine to fake locally.
    • Also deferred: pnpm lifecycle:model-check and the other model-check scripts on these heads (they gate task-lifecycle source, which these four units do not touch — 0 changed lines under src/core/task-persistence/ or src/core/task/run-state/ in each head's diff against the tip).

    DEFERRAL 3 — run-docker.mjs visual baselines

    • The *.visual.* specs and the Docker-backed baseline capture are not run on this host (no Docker daemon session in this lane, and the baselines are not in my worktrees). No visual claim is made for the two webview specs I merged (webview-ui/src/components/settings/__tests__/SettingsView.spec.tsx and friends).

    DEFERRAL 4 — 1404 (B1, the prerequisite of this chain) stays dirty and untouched

    • GET /pulls/1404 reports head e406c0951f0b… and a dirty conflict state; task-106 names only 1406 / 1410 / 1411 / 1412, and I neither re-rooted nor pushed 1404, so the chain's declared order B1 -> B2 -> B3a -> B3c -> B3b still has B1 unblocked-by-nobody. Consequence stated plainly: my four heads are re-rooted onto 332d30fbb9d8 directly, not through B1, because none of the four branches contains another (measured: git merge-base --is-ancestor exit 1 for every pair in an earlier span), so B1's dirt does not propagate into these four, and it does not make these four mergeable ahead of it either.

    DEFERRAL 5 — no review re-request is possible from this token

    • POST /repos/Zoo-Code-Org/Zoo-Code/pulls/<n>/requested_reviewers returned 403 for this token in an earlier span (recorded in my ledger; not re-probed this sitting, because probing it on a live PR would add a reviewer and the Lead asked that no reviews or review requests be touched). So after the push this lane can post comments and nothing else on the review side; dismiss_stale_reviews_on_push: true means the push itself removes the bot approval (the Lead measured it on 2395 at 01:33:53Z), which is why the answers here ride with the content push rather than as a comment-only push.

    — lane s3-1395. Artefacts: .tmp-cr-audit/s3lane/task106-gates.mjs (pass 1, 6,721 B), task106-gates2.mjs (pass 2, 10,225 B, killed on purpose after the abort fix made its heads stale), task106-gates3.mjs (pass 3, the CI-faithful run whose log is task106-gates3.log), task106-abort-fix.json, task106-pricing-singleparent-v2.json.

  16. easonLiangWorldedtech commented on Nov 10, 2026

    @easonLiangWorldedtech
    ContributorAuthor

    Split-or-exclusion plan for the guarded-write chain PRs 1406, 1410, 1411 and 1412 - planned and issued before any split PR opens

    Written by lane s3-1395. Every figure below is a re-read, not a recollection: the priced base is named on every row, the instrument is quoted by blob, and the moment is printed. This comment adds no split PR, no label and no review action.

    1. The gate figures, with the priced base named

    Instrument: scripts/stryker-diff.mjs taken at the base tip - blob c332971815aecfaf, 30,108 B, 869 lines - whose cap MAX_CHANGED_LINES = 500 sits at line 11 and whose second remedy, quoted verbatim from the refusal it prints, is "Split the PR or obtain a maintainer-reviewed narrow exclusion." Both framings agree digit for digit with what CI printed in each head own mutation-diff leg.

    PR head (12) deciding package changed executable lines files / selectors verdict against 500
    1406 e3dd44c extension 468 7 files / 151 selectors inside the cap, 32 lines of headroom
    1411 7742713 extension 675 12 files / 185 selectors 175 lines over (35 percent)
    1410 346c658 extension 778 10 files / 238 selectors 278 lines over (56 percent)
    1412 42665b0 extension 1,065 12 files / 242 selectors 565 lines over (113 percent)

    Priced base for all four rows: 18861b2e357203e6cb4d93930aa2ed122b1eecea (main tip at the moment of the task-106 deliveries; the CI legs print the same merge base in their own words - Mutation-testing 3 package(s) from merge base 18861b2e3572: types (7 lines), extension (468 lines), webview (21 lines) for 1406). The other two packages stay inside the cap at every head: types 7 / 36 / 36 / 53 lines and webview 21 / 43 / 43 / 468 lines, with 1412 the case where webview is 468 and extension is still the deciding package.

    2. Which remedy this lane asks for, and on which boundaries

    The lane asks for the split, not the exclusion, for 1410, 1411 and 1412, and asks for nothing at all on 1406 (468 of 500). The boundary work is already measured and published in this issue and on the PRs: whole-file assignments priced with three-dot diffs against each PR own fork point - 1410 total 6,442 a+d over 51 files, side A 12 files / 3,736 a+d / 530 extension lines (30 over) against side B 39 files / 2,706 / 248 (252 under); 1411 total 5,941 over 57 files, side A 15 / 2,349 / 298 against side B 42 / 3,592 / 377, both under; 1412 total 8,756 over 94 files, side A 45 / 1,851 / 468 on webview against side B 49 / 6,905 / 1,065 on extension. An exclusion is still the cheaper option for 1412 if a maintainer prefers it, because a 49-file side B is itself a reviewability problem; that choice belongs to a maintainer and the lane is not making it here.

    3. Two units this round, numbered, with their hosts ruled out

    • Unit task-130 - stop the task-start baseline from painting the completion card. Measured cause, on all four merge results against the same tip: packages/types/src/global-settings.ts adds DEFAULT_PER_WRITE_CHECKPOINTS = true; src/core/webview/ClineProvider.ts returns perWriteCheckpoints ?? DEFAULT_PER_WRITE_CHECKPOINTS; src/core/task/Task.ts lines 3913-3945 record a task-start baseline gated at 3924 by if (baselineEnabled === false || this.abort) and issued at 3930 as const baseline = await this.checkpointSave(true, true) with force=true standing for allowEmpty (the tip ties force to allowEmpty in its own comment at Task.ts:2479; the signature is checkpointSave(task, force = false, suppressMessage = false, write?) at src/core/checkpoints/index.ts:250). The event then broadcasts at src/core/checkpoints/index.ts:183-206 with postMessageToWebview({ type: "currentCheckpointUpdated", ..., suppressMessage }) and say("checkpoint_saved", ..., { suppressMessage }); the suppress flag hides the chat row but not the completion card, which renders SeeNewChangesButtons at webview-ui/src/components/chat/ChatRow.tsx:1400 and :1759. The visible consequence, measured from the CI artifact: 5,061 pixels (ratio 0.03) differ in electron-chat-dark-sidebar.png, the same number in all four legs, one test in three attempts, run_attempt 1, environment excluded (all five logs print Validated version: 1.100.0 and the same 142.98 MB download) and the fixture unchanged (apps/vscode-e2e/src/visual/__screenshots__/electron-chat-dark-sidebar.png is blob dfc6fee680c8, 28,998 B at the tip, at 332d30f and at every merge result). Shape of the unit: an explicit flag at the call site Task.ts:3930 threaded into the handler at checkpoints/index.ts:183-191, carrying a comment that names the visual consequence, plus the focused checkpoint and task suites and pnpm lifecycle:model-check in the gate, with the two controls that separate it from a blanket suppression - the baseline commit must still be produced, and an armed mutant that suppresses every currentCheckpointUpdated broadcast must fail a test asserting the affordance appears after a real user checkpoint. Declaring package: extension. Host cannot be 1406: it prices 468 of 500 extension lines with 32 lines of headroom, and this fix is production code plus its tests, so putting it inside 1406 would manufacture a new cap refusal out of a fix. No PNG regeneration in this unit and no e2e port.
    • Unit task-133 - guard the unguarded error emits in the shadow checkpoint service. Measured on the 1410 head in C:/work/Zoo-Code-fork/Zoo-Code/.tmp-cr-audit/s3lane/wt-task106-1410: restoreCheckpoint already guards at src/services/checkpoints/ShadowCheckpointService.ts:425-428 with if (this.listenerCount("error") > 0), while restoreFile emits unguarded at line 586 and saveCheckpoint emits unguarded at line 375. Nothing in production subscribes to this service error event: git grep -n -F '.on("error"' HEAD -- src webview-ui/src packages exits 0 with 23 rows, none in the checkpoint production path; the only subscriptions are in the service own spec at lines 609, 782 and 1193, and git grep -n -F 'listenerCount("error")' HEAD -- src exits 0 with exactly the guard at 426 and an assertion at spec line 827. Measured consequence on node v22.23.4: with 0 listeners the thrown value carries code=ERR_UNHANDLED_ERROR and the message Unhandled error. ({ type: error, error: Error: ... }); with 1 listener code=undefined and the original error reaches the caller. Probe run counts: 1 failed | 1 passed (2), exit 1, 463 ms, --config vitest.services.config.ts from src; the delivered suite under the same config and target: 61 passed (61), exit 0, 102.39 s. The suite cannot see the defect because spec.ts:220 asserts rejects.toThrow(/excluded from the shadow repository/) with 0 listeners and Node embeds the payload in the message it throws, so the regex matches under both semantics. performRestore at src/core/checkpoints/rollback.ts:131-143 reports error.message on the per-file rollback outcome, which is where the garbled string reaches the user. Declaring package: extension. Host cannot be 1410: 778 of 500, already 278 lines over. Shape: the guard at 586 (and the matching site at 375, named here so the unit is not written half-done) plus a test that asserts error identity and code rather than a substring.

    Both units are ordered after the earliest split unit that carries their host files, and the chain order for the four delivered PRs is unchanged: 1406 lands first, then 1411, then 1410, then 1412. The B1 setting itself has its own tracking issue, #1397 (open, body 4,372 B, 0 comments as read at the moment below), which is where the persisted-setting round-trip rows belong; this issue carries the chain-level rows.

    Nothing was published here that changes state: no split PR opened, no label added or removed, no review requested, no branch touched. Numbers re-read at the moment printed in this comment header.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions