Skip to content

[ENHANCEMENT] File-write safety series (upstream epic #1375): version-guarded atomic writes + per-step visibility/rollback — PR series plan #33

Description

@easonLiangWorldedtech

| TRIAL | feat/fws-trial-all | open — #1413 (trial build of all 15; head 4fc14c4 = 11 merge commits + addenda 6a0feb1 + 376e013 (spec composition, apply_diff self-observation) + 178e6f4 (CodeRabbit round 3: apply_patch observation, mcp merge array guard + spec-mock parity, safeWriteJson doUnmock, webview writeDelayMs shared default, ChangeCard no-files step + grow, CheckpointSettings sibling, Catalan) + d2239ce (restore safety: rev-parse checkpoint verification + realpath symlink containment) + 4fc14c4 (CodeRabbit round 4: McpHub.spec unknown-safe error.code guard, exported createInitialExtensionState + pre-hydration defaults test); all CodeRabbit findings replied — cross-process TOCTOU scoped out and tracked as upstream follow-up Zoo-Code-Org#1414 with epic risk wording proposed; all 15 component PR heads verified mergeable into upstream/main; CI on 4fc14c4 GREEN (ubuntu unit + e2e-mock + codecov/patch + compile + VSIX build all success); PR Zoo-Code-Org#1413 body updated to the green run 33134141568 (artifact 9671394094, inner vsix sha256 1cbe1793...); all CodeRabbit findings confirmed incl. the round-4 pair (01:59-02:02 UTC); the 8 component PRs re-pushed with the propagated fixes (round-10 notes on each row) and all 8 new heads merge-checked clean against upstream/main; round 11: head 83d83a3 = previous + addendum 6 (re-merge of the updated Zoo-Code-Org#1411 94fea2f + Zoo-Code-Org#1412 c48522c after the trial-review user feedback: apply_diff per-write checkpoint + change card; change-card open-in-editor control incl. the CodeRabbit a11y native-button fix; CI GREEN); round 12: head d6d08e8 = 83d83a3 + addendum 7 (re-merge of Zoo-Code-Org#1411 341a9c2 + Zoo-Code-Org#1412 502f8ca after the CodeRabbit round-11 fixes; CI GREEN run 33161152644 — inner vsix sha256 016e930f…, PR body on that green run; no visible UI change) + addendum 8 (re-merge of Zoo-Code-Org#1411 67d8525 — the trial-review audit found the edit and search_replace tools wrote with no per-write checkpoint / journal entry / change card; both now wired with the same checkpointSave call site as the other four write tools + focused 8-test spec; CI running); fork PR #34 (→ trial) still open, CI 15/15 green, re-verified mergeable against the new trial tip d6d08e8; round 13 (2026-08-29): the CodeRabbit review round was fixed in the introducing PRs — Zoo-Code-Org#141039afe1b (a card whose file was re-written in a later step is now refused instead of overwriting the newer write, and an unreadable journal fails loudly instead of a false no-op), Zoo-Code-Org#1411c202745 + 8c5c53b (typed settings test doubles + nl changeCardDetail sentence), Zoo-Code-Org#1412a0693e9 + ea90ea8 (correlated webview failure results, localized no-task results, focusable change-card error states, openFile path labels, es/hi/it/ko locale corrections, schema-invalid spec coverage); all 18 CodeRabbit findings replied (15 original + 3 round-2 on Zoo-Code-Org#1412) and CR confirmed each fix; all four heads merge-checked clean vs upstream/main (efc30cf) and ubuntu CI green (Zoo-Code-Org#1410 6/6, Zoo-Code-Org#1411 6/6 — the round-1 mocked-E2E failure was a history-write/restart race flake, green on re-run, Zoo-Code-Org#1412 8/8); trial re-merged via addendum 9 (779cb4b, empty CI-retry commit for the flake) + addendum 10 (22a3f33, 46 files +496/-99) — CI GREEN run 33257633298 (inner vsix sha256 aaf90d5e…), PR Zoo-Code-Org#1413 body updated to that green run (artifact 9716305621); fork PR #34 re-verified MERGEABLE/CLEAN against the new trial tip 22a3f33; round 14 (2026-08-30): upstream/main moved efc30cf147147c (5 commits: Zoo-Code-Org#1434 the subtask-approval mocked-E2E timeout fix — the exact flake that failed Zoo-Code-Org#1411's round-1 run, Zoo-Code-Org#1426 Extension Host visual regression rework incl. new electron visual suite + playwright harness restructure, Zoo-Code-Org#1445 label clipping, Zoo-Code-Org#1446 typed host message capture, Zoo-Code-Org#1449 partial-ask e2e ignore); all 16 series heads re-verified merge-tree CLEAN against the new tip 147147c; required checks per the main ruleset = 6 (check-translations, platform-unit-test ubuntu + windows, compile, knip, e2e-mock) — all green on every head; the org is rolling out a new PR review gate (Zoo-Code-Org#1437: required CI → CodeRabbit current-head review → human maintainer approval, SQUASH merge queue ALLGREEN) — its 'PR review gate' status is advisory for fork PRs (hard enforcement = native required checks + review protections) and no code change is needed for it; Zoo-Code-Org#1412 carried a stale CodeRabbit CHANGES_REQUESTED (all findings fixed + individually confirmed by CR) so it was labeled awaiting-author — /approve requested on Zoo-Code-Org#1412 and /review requested on Zoo-Code-Org#1411 + Zoo-Code-Org#1413 at 03:49Z but CodeRabbit picked up no manual command within 30 min (the new gate flow owns CR triggering; the org CR config may restrict fork users), so empty commits were pushed to re-trigger the native synchronize flow: Zoo-Code-Org#1411 8c5c53b0d31ea0, Zoo-Code-Org#1412 ea90ea863dcd24 (the push also dismisses the two stale CodeRabbit CHANGES_REQUESTED reviews per the ruleset dismiss_stale_reviews_on_push), Zoo-Code-Org#1413 22a3f335cb1d96 (addendum 11); zero code change (empty commits), all three re-verified merge-tree CLEAN against main 147147c; NOTE: every one of the 16 series PRs is currently mergeable=true / mergeStateStatus=blocked — the block is the ruleset requiring 1 approving review + code-owner review, i.e. awaiting maintainer review, not CI; CI (6 required checks) is green on all 16 heads; the only remaining automation work is CodeRabbit current-head reviews on the three re-pushed PRs (watching), after which the series is fully at the human-approval stage; round 15 (2026-08-30, after inspecting Zoo-Code-Org#1440 which cleared the bot stage): the gate flow per Zoo-Code-Org#1440's process comment = (1) required CI green, (2) workflow starts CodeRabbit automatically, (3) CodeRabbit APPROVES the latest commit (native review state — COMMENTED/acked findings do NOT count), (4) human maintainer approval; Zoo-Code-Org#1440 cleared step 3 with a clean re-review (zero new comments → APPROVED in 2 min) and sits at awaiting-maintainer; our 16 PRs were all at the gate step "Wait for CodeRabbit to approve the latest commit" (CR states were COMMENTED), so all 16 got the same empty-commit re-trigger: 1380→046b78cad, 1381→309ffd644, 1382→b67e57312, 1383→2a42f886f, 1384→d03877438, 1394→a00eef8e0, 1395→7fd49bcea, 1403→cb1360613, 1404→abc10b43e, 1405→be894d9e0, 1406→7b83143cf, 1408→e96df628a, 1410→87471b558 (plus the earlier 1411→0d31ea062, 1412→63dcd24e2, 1413→5cb1d9674); zero code change in all 16 (empty commits), all 16 re-verified merge-tree CLEAN vs main 147147c, all 16 bodies carry a review-gate re-trigger note (code heads unchanged); the "PR review gate" status stays pending by design until a maintainer approves (advisory — the hard gates are the 6 required checks [all green] + 1 approving review + code-owner); watching CI + CR approvals on the 16 new heads; round 16 (2026-08-30): gate-mechanism forensics + @coderabbitai canary result + 1412 CI clear. (a) The review gate (guide comment + "Zoo Code / PR review gate" status + awaiting-coderabbit/awaiting-maintainer labels) is produced by the NEW label-pr-review-state.yml which exists ONLY on Zoo-Code-Org#1437 gate branch feature/human-pr-review-gate-0shw6cv6mcu (main still runs the old label-only reconciler, last touched 07-13); it executes when zoomote[bot] workflow_dispatches it from that branch — dispatch run 33280010451 (08-29 23:02Z) batch-posted Zoo-Code-Org#1440 plus 13 of our guide comments in a single run. Zoo-Code-Org#1440 bot pass, end to end: real-diff push (18:10Z) → CodeRabbit native APPROVED review on the exact head commit within ~2 min (zero findings) → workflow phase "maintainer" (awaiting-maintainer label + gate step "A human maintainer must now review and approve it"); the gate status is advisory — the hard gates are the 6 required CI checks + 1 approving review + code-owner review (re-verified against ruleset 15494382 "Protect Main", which also has a merge_queue rule with ALLGREEN grouping). (b) CANARY NEGATIVE: @coderabbitai review mention on Zoo-Code-Org#1381 (05:58Z) received a CodeRabbit reply within 7 seconds: "Action not completed — No files to review. CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused." → no review object can be created on an empty-commit head; consistent with the 90-min watcher showing zero fresh CR reviews on all 16 heads → the mention path cannot clear the CR stage; no rollout performed. (c) 1412 addendum 12 (empty commit ab281f5) cleared the flaky ubuntu unit run (the vitest EnvironmentTeardownError x9 was an infra false positive, no assertion failures) → 1412 now 6/6 required green and stable. ALL 16 PRs now: 6/6 required checks green, mergeable, zero mechanical blockers; the only remaining gates are the two human rules (1 approving review + code-owner review), plus — for the merge-queue path only — the advisory extension-host-visual on the 6 B1-stack PRs, whose root cause (B1 task-start baseline checkpoint → SeeNewChangesButtons rendering below the completion row against the pre-FWS Zoo-Code-Org#1426 baseline; deterministic 432-px delta per Playwright / 973 px in the raw PNG diff; pixel-identical across 1404/1406/1410/1411/1412/1413) is documented in those PR bodies for maintainer disposition. The CR stage clears automatically if Zoo-Code-Org#1437 merges (its .coderabbit.yaml gates auto-review on the coderabbit-review-active label all 16 carry); otherwise the path to merge is direct maintainer approval (the gate comment is advisory). |# [ENHANCEMENT] File-write safety series (upstream epic Zoo-Code-Org#1375): version-guarded atomic writes + per-step visibility/rollback — PR series plan

PR series plan + execution tracker (DTE-style: one independently mergeable PR per phase).
Story of record: upstream epic Zoo-Code-Org/Zoo-Code#1375.
Phase 0 PRs (already open): #1380, #1381, #1382 (closed 2026-09-03, superseded by #1496).

1. Goal

Make agent file writes fast, loud-failing, self-healing, atomic, and auditable — in that order.

  • Safety: no silent corruption (races, truncated tool calls, cross-instance state writes); stale writes fail loudly with a remediation the model can act on.
  • Speed: zero artificial latency on the default write path.
  • Visibility: every agent write is checkpointed, journaled, and shown as a per-step change card; rollback per file or per step. Explicit non-goals (per epic): shell-tool writes, workspace lockfiles, diff-editor approval UX replacement, worktrees as the task-start mechanism.

2. Series overview

PR Epic item Scope Deps Est. Status
Phase 0a A5 — Zoo-Code-Org#1371 Atomic mcp_settings.json stub creation (safeWriteJson + merge under advisory lock) done open (#1380)
Phase 0b Speed DEFAULT_WRITE_DELAY_MS 0 + remove delay(300) done open (#1381)
Phase 0c A5 — Zoo-Code-Org#1021 saveClineMessages abandoned guard done closed unmerged 2026-09-03 — superseded by #1496 (d48f46d; CodeRabbit APPROVED 2026-09-04; awaiting human approval)
S1 A1 Version token (pure function + unit tests) 0.5d OPEN (#1383)
S2 A2 Per-task observation registry (ReadFileTool populates) S1 1d OPEN (#1394)
S3 A4 Atomic publish: safeWriteText (staging + fsync + rename / Windows ReplaceFile+DACL) 2d OPEN (#1395)
S4a A3 Guarded write core: CAS (createIfAbsent / replaceIfVersion) + per-path FIFO chain S1+S2+S3 2d OPEN (#1405, tracking #1399); round 10: head 56ce4bf (CodeRabbit fixes from trial addendum: safeWriteJson cleanup now doUnmock + resetModules; CI green)
S4b A3 Guarded write tool wiring (WriteToFile / EditFile / SearchReplace / ApplyPatch / ApplyDiff) S4a 1d OPEN (#1408, tracking #1400); round 10: head 88c9352 (CodeRabbit fixes from trial addendum: apply_patch hunk read now records the S2 observation for the guarded publish; CI green)
S5 A5 — Zoo-Code-Org#920 Cross-instance updateTaskHistory regression test — (track Zoo-Code-Org#1319) 1d planned
S6 A6 — Zoo-Code-Org#1221 Truncated tool-call parser fix (no partial-args reuse) stacked on Zoo-Code-Org#1066 1d blocked
L1 Speed Async post-save diagnostics (follow-up event, not awaited) S3 1d OPEN (#1403, tracking #1396)
L2 Speed Chat-diff (PREVENT_FOCUS_DISRUPTION) as default approval path 1d OPEN (#1384)
B1 B1 Per-write checkpoint (extend shadow git; O(1) task-start baseline) S3 2d OPEN (#1404, tracking #1397); round 10: head b98f159 (CodeRabbit fixes from trial addendum: exported createInitialExtensionState + pre-hydration perWriteCheckpoints test; CI green)
B2 B2 Per-task JSONL change journal (torn-tail repair) B1 1d OPEN (#1406, tracking #1398)
B3a B3 Per-step change cards + changeCardDetail setting (extension host; split from the planned B3a by the 1000-line cap) B1+B2 2d OPEN (#1411, tracking #1401); round 10: head 9b0787a (CodeRabbit fixes from trial addendum: exported createInitialExtensionState + pre-hydration changeCardDetail test; CI green; round 11: head 94fea2f (user feedback from the trial review: apply_diff writes now record the per-write checkpoint and emit the per-step change card, parity with write_to_file / edit_file / apply_patch; new 4-test spec; CI GREEN); round 12: head 67d8525 (341a9c2 + trial-review audit fix: the edit and search_replace tools — exact string replacement, exposed by default — now also record the per-write checkpoint and emit the per-step change card, parity with all six write tools; focused 8-test spec; CI running); round 13: heads c202745 + 8c5c53b (CodeRabbit review fixes: Slider test double typed narrow instead of any + nl changeCardDetail.description full sentence; VSCodeCheckbox/VSCodeLink settings doubles fully typed; tsc 0, eslint 0, 5/5 spec; CI GREEN 6/6 — the round-1 mocked-E2E failure was a history-write/restart race flake, green on re-run; merge-tree clean)
B3c B3 Per-file/per-step rollback service (extension host; split from B3a) B3a 1d OPEN (#1410, tracking #1409); round 10: head d64389c (CodeRabbit fixes from trial addendum: checkpoint availability verification + symlinked-ancestor realpath containment; CI green); round 13: head 39afe1b (CodeRabbit review fixes on the Zoo-Code-Org#1435 semantic head: stale-card rollback refused via latest-entry checkpointId comparison instead of overwriting the newer write; journal read failures (EACCES/EISDIR/torn JSON) propagate instead of a false no-op — only ENOENT yields []; EISDIR + non-Error rejection regression tests; tsc 0, eslint 0, 100% changed-line coverage; CI GREEN 6/6; merge-tree clean)
B3b B3 Change cards UI + rollback buttons (webview) B3a+B3c 1d tracking (#1402); round 10: head 0e021ef (CodeRabbit fixes from trial addendum: ChangeCard no-files step resolution, Tailwind v4 grow, sibling settings, Catalan fix; CI green; round 11: head c48522c (user feedback from the trial review: per-file open-in-editor control on change cards - CodeAccordion jump icon on diff rows + native button on no-diff rows - via the existing openFile webview message; changeCard.openFile i18n key in all 18 locales; 3 new spec tests incl. the CodeRabbit a11y fix making the compact-row control a native button; CI re-running); round 12: head 502f8ca (typed VSCodeCheckbox change events + typed settings test doubles; CI GREEN); round 13: head ea90ea8 (CodeRabbit review fixes: the three rollback/restore handler cases post correlated failure results when the import/journal/git restore throws, no-active-task results post localized copy across 18 locales, the change-card error states are focusable status elements (role=status + tabIndex + aria-label = the error), the compact-row open-file label names the target file (openFile {{path}} slot, 18 webview locales), es/hi/it/ko locale corrections, and the spec covers the schema-invalid JSON path; tsc 0, eslint 0, 100% changed-line coverage; CI GREEN 8/8; merge-tree clean)

3. PR specs

S1 — A1 Version token

  • New file src/utils/versionToken.ts: computeVersionToken(filePath): Promise<string> + pure versionTokenOfStat(stats) for testability.
  • Token format: dev:ino:size:mtimeNs:ctimeNs from one fs.stat; ns fields as decimal strings from BigInt (no float precision loss).
  • No production callers in this PR — pure infrastructure. A disk fact: every process observing the same file state computes the same token.
  • Tests (unit): determinism on identical stat; distinct token on size/mtime change; BigInt large-size handling; absent file rejects with ENOENT.
  • Acceptance: unit suite green; zero behavior change (no imports from production code).

S2 — A2 Observation registry

  • New file src/core/task/observationRegistry.ts: per-task Map<absolutePath, { version, observedAt }>; owner = task, so parent and subtask observations are independent.
  • Task owns an instance; ReadFileTool records an observation after a successful read (one extra stat, same call the write guard will reuse).
  • No behavior change — observations are recorded but not yet consulted.
  • Tests: registry unit tests (observe/get/replace-on-reobserve); ReadFileTool spec-level test asserting a read of an existing file registers an observation with the on-disk version; subtask isolation test.
  • Acceptance: read path cost = +1 stat; no other observable change.

S3 — A4 Atomic publish

  • New file src/services/file-safety/safeWriteText.ts: temp file in a private per-write staging dir → write → fsync → close → atomic rename; on Windows: ReplaceFile with DACL copy, rename fallback. Generalizes the staging/backup/rollback logic currently inside safeWriteJson (which is refactored to call this).
  • saveDirectly (all five write tools + write_to_file) switches from raw fs.writeFile to safeWriteText.
  • Behavior-preserving (no version guard yet): same writes succeed, but now crash/power-loss safe (reader sees only old-or-new complete content).
  • Tests: staging dir created/cleaned; fsync called before rename (mock fs); crash-during-write leaves no torn target (simulate failure between write and rename); Windows ReplaceFile path + DACL preservation; safeWriteJson existing suite still 100% (no regression); patch coverage 100%.
  • Acceptance: saveDirectly no longer calls raw fs.writeFile; safeWriteJson behavior unchanged.

S4 — A3 Guarded write/edit (the behavior change)

  • New file src/core/tools/guardedWrite.ts: compare-and-swap on the write path:
  • unobserved target → createIfAbsent: new file succeeds; existing file fails (forces a read first — the model re-reads and retries with the observed version);
  • observed-absent → createIfAbsent;
  • observed-present → replaceIfVersion(version): mismatch fails with "stale version — re-read the file, then retry";
  • edit keeps its literal-match check plus the version guard; unobserved edit fails with "file not read yet — read the file, then retry".
  • Per-absolute-path tail-promise chain (in-process FIFO) wrapping read → guard → publish: concurrent subtask mutations to the same file are deterministically ordered — one wins, the rest fail as stale.
  • Wired into WriteToFile / EditFile / SearchReplace / ApplyPatch / ApplyDiff.
  • Diff budget note: core (guard + FIFO chain + registry hook) is S4a; tool wiring is S4b if the combined diff would exceed the 800-line target.
  • Cross-process stance: no lockfile (would block the user's own editor); two processes on the same file are detected via the token, loser fails as stale and re-reads.
  • Tests (unit + concurrency): each guard branch per tool; stale-version failure carries the remediation suffix; unobserved-edit failure; two concurrent writers on one path → exactly one succeeds; observed-absent then concurrent-create → second fails stale; regression: normal single-writer flow unchanged (no new failures on existing tool suites).
  • Acceptance: all five write tools fail loudly (tool-call error, step event in chat) on stale/unobserved writes; model self-heals via re-read+retry in the standard loop; no silent overwrite path remains.

S5 — A5 Zoo-Code-Org#920 cross-instance regression test

  • Regression test: two extension instances (parallel tabs) racing updateTaskHistory on the locked-merge write; asserts the second instance's merge preserves the first's fields (no clobbered history item).
  • Base material: local branch fix/920-concurrent-task-history-cross-instance (5 commits, prior work).
  • Track [Fix] Task history disappears when user reopens a task Zoo-Code-Org/Zoo-Code#1319 (task-history safe-write retry + advisory-lock merge) — if it lands first, adapt or close as covered; do not double-fix.
  • Acceptance: test reproduces the clobber on the pre-fix code path (or documents why it no longer reproduces) and passes post-fix.

S6 — A6 Zoo-Code-Org#1221 truncated tool-call parser

L1 — Async post-save diagnostics

  • saveDirectly tail: instead of awaiting LSP diagnostics, emit them as an asynchronous follow-up event once settled.
  • Tests: save resolves without waiting on diagnostics; follow-up event fires with the settled diagnostic payload; no event on clean save (or a no-op event, per implementation).
  • Acceptance: post-save latency drops by the LSP-settle time; diagnostic information is still delivered (just later).

L2 — Chat-diff default approval path

  • Make the chat-diff (PREVENT_FOCUS_DISRUPTION) path the default approval flow; diff-editor path remains available.
  • Setting: flips the existing PREVENT_FOCUS_DISRUPTION default falsetrue (src/shared/experiments.ts:22); toggle kept as escape hatch, storage key unchanged (§6).
  • Tests: default path resolves to chat-diff; focus not stolen from the active editor (behavioral test at the provider level).
  • Acceptance: default approval no longer opens/refocuses the diff editor.

B1 — Per-write checkpoint

  • Extend ShadowCheckpointService: every successful write_to_file / edit_file / apply-patch records a checkpoint (currently only user message sends do), keyed by step; checkpoint payload reuses the existing shadow git at <globalStorage>/tasks/<taskId>/checkpoints.
  • Task start becomes a real O(1) baseline (today: no-op). Not a worktree.
  • Tests: checkpoint created per successful write (mock service assertions); task-start baseline recorded once; rollback-to-checkpoint restores file content; checkpoint count bounded per task.
  • Acceptance: after any agent write, git-in-shadow can show that step's snapshot; task start is cheap (no worktree).
  • Setting: perWriteCheckpoints (boolean, default true) — master switch for the B cluster; full Persisted Setting Checklist round trip ships in this PR (§6).

B2 — Per-task change journal

  • Append-only changes.jsonl under the task dir; one entry per file write: path, operation, checkpoint id, diff stats (additions/deletions from the already-computed approval diff).
  • Torn-tail repair on load (incomplete final line discarded, rest parsed).
  • Tests: append format; load with clean tail; load with torn tail (write truncated line, repair drops it); journal entry references the B1 checkpoint id.
  • Acceptance: journal is the single per-task audit list; survives a crash mid-write (no corrupt load).

B3 — Per-step change cards + rollback

  • Per-step change cards in the existing chat flow: reuse the unified diff + computeDiffStats already produced for approval; "N files changed this step" + per-file diff.
  • Rollback to any checkpoint, per file or per step (shadow git restore, journal as the index).
  • Auto-approval paths get the same cards after the fact.
  • Diff budget note: B3a = extension host (card payloads + rollback service), B3b = webview (cards UI + rollback buttons); split point = the 800-line target per PR.
  • Setting: changeCardDetail ("full" | "summary", default "summary") — round trip ships in B3a (§6).
  • Tests: card payload for a multi-file step; rollback per file restores only that file; rollback per step restores all its files; auto-approval step still emits cards.
  • Acceptance: the user can see and undo any agent step after the fact, including on fully auto-approved runs.

4. Order

  1. Phase 0 (open): fix(mcp): preserve concurrent MCP settings during initial creation (fixes #1371) Zoo-Code-Org/Zoo-Code#1380fix(task): guard saveClineMessages against abandoned tasks (fixes #1021) Zoo-Code-Org/Zoo-Code#1382perf(write-path): remove artificial write delays by default (part of #1375) Zoo-Code-Org/Zoo-Code#1381 land in any order; they are independent.
  2. S1 — first of the series (pure function, unblocked, unblocks S2).
  3. S3 ∥ S5 ∥ L2 ∥ S2 — S3 and S5 are independent of S1/S2; S2 needs S1. Interleave by CI availability.
  4. S4 — after S1+S2+S3 all merged.
  5. L1 — after S3 (it touches the saveDirectly tail).
  6. B1 → B2 → B3 — B chain after S3 (B1 records at the publish point; independent of S4 but sequenced after S3 to sit on the atomic-publish path).
  7. S6 — whenever fix(write-to-file): address partial filesystem error review Zoo-Code-Org/Zoo-Code#1066 is ready (stack on its head).

5. Risks

Risk Mitigation
S4 changes model-visible behavior (unobserved writes now fail) Remediation suffix makes the loop self-heal; loud+recoverable-stale is the epic's explicit model; regression suites for all five tools must stay green
Zoo-Code-Org#1379 (DTE mega-PR) touches Task.ts — rebase hazard for S2/S4/S5 Land S2 before Zoo-Code-Org#1379 merges if possible; otherwise rebase S-series on the newer head
S6 depends on Zoo-Code-Org#1066 (open since 08-23) Stack on Zoo-Code-Org#1066's head; do not cherry-pick around it
S5 overlaps Zoo-Code-Org#1319 Check Zoo-Code-Org#1319 status before starting; adapt or close as covered
Windows ReplaceFile DACL edge cases (S3) Dedicated unit tests on the Windows CI lane; rename fallback kept
B1 shadow-git growth (checkpoints per write) Per-task dir, bounded by existing checkpoint policy; monitor task dir size in tests
Cross-process writes: token detects, doesn't lock Epic decision — a lockfile would block the user's own editor; loser re-reads and retries

6. Configuration / user settings

Principle: the safety core (A1–A6) is not configurable — an off-switch would reintroduce the corruption this epic fixes. Configurable surface = the B cluster (visibility/rollback convenience) + existing behavior toggles (speed, approval surface). Every new setting follows the AGENTS.md Persisted Setting Checklist — full round trip: global-settings.ts (definition + shared default constant) → ExtensionState/message types → SettingsView binding to local cachedState (never live state) → updateSettings payload → webviewMessageHandler persistence via ContextProxy → ClineProvider.getState() with default → getStateToPostToWebview() (destructure + return) → every consumer same default semantics → import/export schema → focused tests (UI binding/save, persisted + unset via posted state).

New settings

Key Type Default Controls Lands in
perWriteCheckpoints boolean true Master switch for the whole B cluster: per-write checkpoints (B1), JSONL journal (B2), change cards + rollback (B3). Off = writes behave as today, no B artifacts, no rollback. B1
changeCardDetail `"full" "summary"` "summary" Card granularity: summary = one line ("N files changed +X −Y") + per-file list + rollback, diff rendered lazily on expand; full = inline unified diff rendered by default. Auto-approval steps always get the compact card regardless.
  • No "off" level for card detail: visibility is the point; users who don't want cards turn the master off.
  • Checkpoint retention stays with the existing checkpoint policy (bounded per task); revisit only on disk-usage reports — no setting now.

Existing settings (reused — no new key)

Key Phase Action
writeDelayMs 0b (open) Done: default 1000 → 0 (DEFAULT_WRITE_DELAY_MS); setting, UI, round trip unchanged.
PREVENT_FOCUS_DISRUPTION L2 Flip default falsetrue (experimental map, src/shared/experiments.ts:22). Keep the toggle as the escape hatch for users who prefer diff-editor approval. Decision inside the L2 PR: if the experimental settings section is hard to discover, surface it as a first-class approval-surface setting — move/rename in the UI only; the storage key stays stable so existing user values carry over.

Deliberately NOT settings (decision record)

  • A1–A4, A5, A6 (version token, observation registry, CAS guard, atomic publish, internal-state firebreak, truncation guard): always on. No "legacy unsafe mode" — the epic's acceptance criteria (no silent apply failures, loud + recoverable stale) are non-negotiable.
  • L1 async diagnostics: informational follow-up event; no toggle (a setting would only re-introduce the blocking path or drop the information).

Round-trip cost and diff budget

Each new setting touches ~8–10 files (types, extension host, provider, webview settings, schema, tests) ≈ 100–300 changed lines, and counts toward the diff budget (800-line target / 1000 hard cap):

  • perWriteCheckpoints round trip ships inside the B1 PR (not a separate PR) — it is part of the 2d B1 estimate.
  • changeCardDetail round trip ships inside B3a (extension-host side); if B3a + round trip exceeds budget, B3a keeps setting + payload and B3b is UI-only.
  • L2's default flip is one line in experiments.ts + its spec update — trivial.
  • Per-setting tests (checklist): SettingsView save binds cachedState and posts updateSettings; posted state asserts both persisted and default; import/export round-trips.
  • i18n: names/descriptions added to all locale files; check-translations CI covers it.

7. E2e plan (apps/vscode-e2e, aimock)

Principle (AGENTS.md test pyramid): e2e only for real extension-host boundaries and full-workflow smoke; detailed assertions stay at unit/spec. Existing base suites to extend: suite/tools/write-to-file.test.ts (real task → write_to_file → disk assertions, aimock-replayed) and suite/subtasks.test.ts (+ fixtures/subtasks.ts). New fixtures follow the aimock workflow: TEST_FILE-filtered record runs, toolCallId matching for turn 2+, match strings without timestamps/paths, verify with pnpm --filter @roo-code/vscode-e2e test:ci:mock.

Phase E2e decision
Phase 0 none — unit-level fixes
S1, S2 none — pure function + spec level
S3 no new e2e (crash-safety = unit tests on mocked fs); existing write-to-file smoke must stay green as the real-host regression
S4 E1 stale-write self-heal (ships with S4b): scripted multi-turn fixture — tag prompt → write_to_file on an existing file the model never read → expect the tool-call failure step ("file not read yet") → read_file (matched by toolCallId) → write_to_file retry → attempt_completion; assert final on-disk content = new content and the chat step shows the failure + success. E2 concurrent subtask writers (ships with S4b or as a follow-up small PR): extend the subtasks suite — two subtasks write the same file; assert final content is one complete version (no torn/interleaved file); keep ordering assertions loose to avoid CI flake
S5 unit-level cross-instance test only; a true two-live-instance e2e would require extending the restart coordinator (sequential phases today) — stretch goal, out of scope for the series
S6 E3 truncated tool call writes nothing (ships with S6): fixture whose write_to_file arguments are truncated JSON → task step shows the "arguments were truncated" error → assert the target file is absent/unchanged on disk. Feasibility check during implementation: if aimock cannot replay a malformed arguments payload, fall back to parser-level integration test (still justified)
L1 none — timing assertions are flaky in e2e
L2 extend write-to-file suite: default approval surface resolves to chat-diff; approval completes and file is saved without the diff editor opening
B1 unit; optional restart-scenario smoke that the checkpoints dir exists after a write
B2 none — unit (journal format/repair)
B3 webview-ui tests for card rendering + rollback button state; E4 step rollback end-to-end (ships with B3b): multi-step write task → assert per-step change cards appear → invoke rollback of one step → assert on-disk content of that step's files reverts to the pre-step version

8. Per-PR execution rules (AGENTS.md)

  • One PR per phase; each PR: focused diff, its own commit history, own CI.
  • Diff budget (two tiers): total changed lines (additions + deletions) target < 800 per PR, hard cap 1000. The 800 target leaves a ~200-line buffer so that fixes for reviewer/bot (CodeRabbit) comments added to the same PR — new tests, extra branch coverage, a small refactor — still land under the 1000 cap without a force-push/reopen. If the initial diff already exceeds 800, expect to split into sequential sub-PRs (S4a/S4b, B3a/B3b below). Verify with the PR diff stat before opening, and re-check the stat after any review-fix commit so the final PR stays ≤ 1000.
  • Narrowest-layer tests: pnpm --dir src exec vitest run <path>; suite green before push.
  • pnpm --dir src exec eslint --max-warnings=0 <files>; suppression counts never increase.
  • No .changeset files, no CHANGELOG edits (per AGENTS.md).
  • Patch coverage 100% (codecov) on every PR.
  • PR body: Summary / Changes / Tests / Provenance (when extracting from a feature branch), linking this issue + epic [EPIC] File Write Safety Prevent Concurrent Write Races Data Corruption Zoo-Code-Org/Zoo-Code#1375.

9. Acceptance criteria (series complete)

  • All PRs merged (Phase 0 + S1–S6 + L1–L2 + B1–B3, including any S4/S4b and B3a/B3b splits); every PR at or under the 800-line target, and never over the 1000-line hard cap (the 800→1000 band is reserved for review-fix commits).
  • Epic [EPIC] File Write Safety Prevent Concurrent Write Races Data Corruption Zoo-Code-Org/Zoo-Code#1375 acceptance criteria met: no silent apply failures; loud + recoverable stale; per-step visibility; multi-agent safety; task start is a real baseline; write-path default latency ≈ 0; suppression counts unchanged.
  • E2e suite green on the mock lane (pnpm --filter @roo-code/vscode-e2e test:ci:mock), including E1 (stale-write self-heal), E2 (concurrent subtask writers), E3 (truncated tool call writes nothing), and E4 (step rollback end-to-end).
  • Settings: perWriteCheckpoints and changeCardDetail complete the full Persisted Setting Checklist (UI save via cachedState, posted-state persisted + default assertions, import/export round trip, all locales); PREVENT_FOCUS_DISRUPTION defaults to true with the existing toggle preserved.

Execution status

round 29 (2026-09-06): v2-16 (B3b correlated change-card failures, localized no-task errors, error-state a11y, locale fixes; upstream source a0693e9e402a + ea90ea895acb / PR Zoo-Code-Org#1412 — empty CI commits skipped). Combined increment 40 files +316/−78 = 394 shortstat lines < 400 soft cap → single stacked PR v2-16 (PR #61) head 877cb093a, base feat/fws-v2-15-restore-latest-ui (45f810db9). (a) Parity: webviewMessageHandler.ts + webviewMessageHandler.rollback.spec.ts + 18 backend common.json byte-identical to upstream v2-16 (local pre-state byte-identical to upstream v2-15; the handler t import was pre-existing); ChangeCard.tsx = upstream v2-16 + 15 lines only (the 6 surviving Stryker disable next-line directives + 9 justification lines — no v2-16 hunk touched a directive line, so all 6 carry over unchanged); ccspec = upstream v2-16 spec (schema-invalid {} test, a11y + path-label assertions, {{path}} t-mock interpolation, no-files success rename) + the local round-2 superset (425/1 vs upstream); webview chat.json ×18 = openFile {{path}} slot + locale fixes (es/hi/it/ko), What's New marketing skip retained per the v2-13 policy (3/3 per locale vs upstream). (b) Stryker (base 45f810db9): run 1 (head 76643aec6) red with 3 blocking Survived StringLiteral, all in the new aria-label fallbacks (:231 file-rollback, :306 restore, :379 step; the ?? LogicalOperator mutants on the same lines were already killed by the empty-string error: "" test) → fix = aria-label fallback assertions (new round-2 test firing no-error file + step failures asserting aria-label = "Rollback failed", and the existing restore-tooltip test extended with aria-label = "Restore failed") → final gate (head 877cb093a) green: 80 extension-lane lines → 44 valid → 44 Killed; 15 webview-lane lines → 10 valid → 10 Killed; 0 blocking, 0 Ignored. (c) Gates: check-types 11/11; rollback spec vitest 14/14 (11 at base + 3 upstream correlated-failure/no-task tests); CC.spec vitest 41/41 (39 at base + 1 upstream schema-invalid + 1 new round-2 mutation test, 1 rename); eslint 0 (src + webview-ui), suppression counts unchanged (the prune-suppressions re-write of src/eslint-suppressions.json reverted); prettier clean — workflow finding: the root .prettierrc.json has no endOfLine (default lf), so webview files must be LF: core.autocrlf=true checks blobs out CRLF, and prettier --check fails on a CRLF worktree file until --write normalizes (all repo blobs are LF; the commit diff stays clean). (d) Process: CR sweep of #53#61#59/#60 awaiting-coderabbit/coderabbit-review-active with 0 CodeRabbit comments at sweep time; PR #61 opened, tracker + labels pending the CI pass.

round 28 (2026-09-06): v2-15 (B3b per-file restore-latest + corrected rollback confirm copy; upstream source 7a1d4e61b4c1 / PR Zoo-Code-Org#1412 — empty CI commits 63dcd24e21cc/ab281f51b97c skipped). The 492-line source increment exceeds the 400-line soft cap → split into two stacked PRs on the v2-14 line: v2-15a API (PR #59) head 5ec15d56f, base feat/fws-v2-14-change-card-l5 (b1110b454), 3 files +167/−6 (173 shortstat lines); v2-15b UI (PR #60) head 45f810db9, base feat/fws-v2-15-restore-latest-api (5ec15d56f), 20 files +383/−14 (397 shortstat lines). (a) v2-15a parity: webviewMessageHandler.ts + webviewMessageHandler.rollback.spec.ts byte-identical to upstream v2-15; the sole 1-line delta in vscode-extension-host.ts is the documented v2-9 divergence (local changeCardDetail? stays optional); adds checkpointRestoreLatestFile to the WebviewMessage union + payload schema (cardTs/filePath), kind?: "rollback" | "restore-latest" + noOp?: boolean on CheckpointRollbackResult, the handler case (safeParse → current task → lazy restoreLatestFile import → checkpointRollbackResult with kind: "restore-latest" + noOp + conditional error; no-task → success: false + error so the card can clear its pending state), and 4 new handler tests (success / noOp / error / no-task). (b) v2-15a Stryker (base b1110b454): 43 changed lines → 23 valid → 23 Killed, 0 Survived / NoCoverage / timeout — first run green, no new directives. (c) v2-15b parity: ChangeCard.tsx = upstream v2-15 + 15 lines only (6 Stryker disable next-line directive lines + 9 justification comment lines; upstream carries 0 directives; the v2-13/v2-14 directives on lines upstream rewrote were dropped, 5 carry into the rewritten regions, 1 new for the cancel handler); ccspec = upstream v2-15 spec (5 restore-latest tests + 5 t-mock entries) + the local round-2 superset (StandardTooltip inline-content mock, the within import, the title assertion); two local round-2 tooltip tests now match by content (getAllByTestId(...).some(...)) — v2-15 adds a second per-row tooltip (restore-latest), so a bare getByTestId matches twice; the 18 locales take the 5 changeCard keys only (rollbackFileWarning, restoreLatest, restoreLatestWarning, restored, restoreFailed), byte-identical to the upstream translations; the upstream What's New marketing hunk skipped (v2-13 policy); changeCardDetail stays optional (v2-9 divergence, untouched). (d) v2-15b Stryker run 1 red (7 blocking, head 6c4eee73d): ChangeCard.tsx:193 StringLiteral Survived (the new per-file rollback confirm warning copy is never asserted), :272 ObjectLiteral Survived (cancel-to-IDLE { ...prev, [path]: IDLE } — read-equivalent to key removal because the sole read is fileRestores[path] ?? IDLE → provably unobservable), :274 StringLiteral Survived (the restore cancel label is never asserted), :295 StringLiteral NoCoverage + LogicalOperator Survived (the state.error ?? t("…restoreFailed") right side is only evaluated without an error; ??|| differs only for a falsy non-nullish error), :306/:310 StringLiteral Survived (the idle restore tooltip + aria-label are never asserted). (e) Fix → final green (head 45f810db9): 4 new round-2 tests (per-file rollback warning copy; the idle restore tooltip + aria-label; the restore cancel label; the two-phase error tooltip — failure without an error shows the localized fallback, an empty-string error renders verbatim, killing the ??|| mutant) + 1 Stryker disable next-line ObjectLiteral directive on the cancel handler (the first attempt used a same-line trailing directive — rejected by the gate validator, which requires the strict next-line form; the directive comment line adds 1 changed line, 97 → 98); final gate: 98 changed lines (webview lane) → 55 valid → 55 Killed, 0 Survived / NoCoverage / timeout, 1 Ignored (= the new directive; verified in-report: the L273 ObjectLiteral entry has status=Ignored). (f) Gates: both PRs — check-types 11/11 (incl. the pre-push hook), eslint 0 (--max-warnings=0, per-package invocations), prettier clean, pre-commit turbo lint 11/11; #59 — rollback spec vitest 11/11 (4 new); #60 — find-missing-translations all green, CC.spec 39/39 (35 at the previous head + 4 new). Budget: 173 + 397 shortstat lines, both < the 400 soft cap (measured git diff --shortstat <base> <head>; the rationale is recorded in both PR bodies). Review-state: #59 tracker comment 5558301219 + coderabbit-review-active/awaiting-coderabbit labels (CI passed, awaiting automated review); #60 pending at time of writing (its CI was still running).

round 27 (2026-09-06): v2-14 (B3c ChangeCard L5: open-in-editor, native action buttons, typed checkbox handlers) built, gated green and opened as stacked PR #58 against feat/fws-v2-13-loc-i18n (head b1110b454, base 6eb159311), 23 files +169/−26 (195 shortstat lines < the 400 soft cap — no split). (a) Base parity: built from base→L5 (502f8ca98) residual diffs — CheckpointSettings.tsx byte-identical to L5 (typed CheckboxEventTarget handlers, per-write-checkbox testid) plus 3 directive lines; CP.spec zero diff (base mock == L5 mock; v2 keeps its 3 extra changeCardDetail tests as a coverage superset); SV.spec mock-only (the mock forwards currentTarget — required by the typed handlers; the L5 SV test rewrites not ported); ccspec keeps the round-2 tests + the within import (the sole 1-line vs-L5 delta); the 18 locales take the changeCard block only (the openFile key + the ca rollbackFailed retranslation "La revertida ha fallat""La reversió ha fallat"); the L2→L5 What's New highlights hunk skipped as stale marketing drift (same policy as v2-13's en skip); the 13 v2-13 directive lines in ChangeCard.tsx carried over (unchanged lines → no new suppression counts). (b) First Stryker run red (10 blocking mutants): CheckpointSettings.tsx:97 all 6 mutants NoCoverage (the enable-handler line is never executed — no enable-checkbox test exists in the base spec, and the L5 SV enable test was intentionally not ported), :62/:79 OptionalChaining Survived (the mock currentTarget is never null in JSDOM, so the ?. guard is unobservable in tests), ChangeCard.tsx:332 StringLiteral Survived (the test asserted aria-label only, not title), :337 ObjectLiteral Survived (the decorative icon style={{ fontSize: 13.5 }}). (c) Fix (+24/−5 vs the first artifact):Stryker disable next-line OptionalChaining + 1× Stryker disable next-line ObjectLiteral directives (4 new directives; validator-legal single mutator names; justifications inline), the ccspec title assertion, two new CP.spec enable tests (check/uncheck as separate renders — React restoreControlledState re-asserts the controlled checked prop after each discrete event, so one render observes at most one toggle; the first dual-click attempt read true on both clicks and failed), and the t mock gained the settings:checkpoints.enable.label entry. (d) Final gate (head b1110b454): 32 changed-code mutants → 28 Killed, 0 Survived, 0 NoCoverage, 0 timeout, 4 Ignored (= the 4 new directives). (e) Gates: check-types 11/11, find-missing-translations all green (backend 7 files, frontend 9 files, package.nls 50 keys), vitest 3 specs = 66/66, eslint 0 (webview-ui config), prettier clean, stryker-diff unit tests 27/27 (WIN32 spawn shim reverted before push). CI + CodeRabbit pending.

round 26 (2026-09-06): v2-13 (B3b change-card UI) shipped as stacked fork PRs #56/#57 after a two-run Stryker red diagnosis and a targeted type-guard test fix. (a) L2 port: B3c L2 (0e021ef96) ChangeCard.tsx (314 lines, blob 92b62d4a8) and spec (352 lines, includes the two missing-task step-result tests, compatible with the tooltip mock) ported byte-exact into v2-13; my earlier duplicate T6d test dropped. (b) Round-2 tests: 13 new mutation-coverage tests + repo-pattern StandardTooltip mock (4 existing precedents) appended to the spec (299 ins / 1 del vs the L1 HEAD blob). T2 (malformed window messages) filters provider-origin throws by ChangeCard in the stack: the real ExtensionStateContext listener has no null-data guard (pre-existing, out of scope). The 13th test (drops rollback payloads carried by messages of other types) is the gate-driven survivor fix — see (f). (c) Directives: 4 new single-line Stryker disable next-line directives (repo precedent: 0 — documented deviation). Shim validator rejects comma lists containing spaces ([A-Za-z0-9]*(?:,[A-Za-z0-9]*)*), so names are comma-separated without spaces. Justifications: parsed === undefined guard (safeParse rejects undefined identically), initial expansion set (unobservable for diff-less files), filePath !== undefined branch (a "undefined" key matches no card file and never renders), effect cleanup return (idempotent listener re-added on ts change). (d) Gates: vitest 27/27, eslint 0, prettier clean (both files). (e) Restructure: old commit 62c0ac28d reset and split into the code PR and a stacked locales PR (18 files × 12 = 216, byte-parity with L1 verified). First code amend came out 1006 insertions — over the 1000 hard cap — so the new test was compacted (27 → 18 diff lines: payload without the files array, which the kill does not need — the no-file-path step branch L103 renders the same step-error) → final 997 ≤ 1000. (f) Stryker runs (the red-run story): run 1 and run 2 both red on exactly one survivor: ChangeCard.tsx:76 [ConditionalExpression] false (report id 34). Diagnosis: the report actually holds TWO L76 CExpr-false mutants — id 32 (cols 8–60, the whole || condition → if (false), KILLED by T2) and id 34 (cols 8–49, the LHS operand data?.type !== "checkpointRollbackResult" only → if (false || !result), the type check removed while !result stays). T2's three malformed dispatches carry no checkpointRollbackResult payload, so result === undefined and !result makes the mutated guard return for every dispatch — no throw, T2 passes, id 34 survives by construction. Run 2b (instrumented T2: temporary probe logging every window "message" event with stack attribution) confirmed the kill machinery works in the Stryker worker (cardTs events observed with filterMatch=true on the id 32/33/39 runs; provider-origin events correctly filtered) and pinned the survival to the LHS-only semantics. Fix: the new type-guard test dispatches {type:"state", checkpointRollbackResult:{cardTs:1000, success:false, error:"boom"}} (foreign type, matching card ts, truthy payload) and asserts no rollback state is applied — the original type guard returns (test passes), id 34 processes the foreign result through the no-file-path step branch and renders the step error (test fails → kill). Local manual-mutant verification on both test shapes: 1 failed | 26 passed, only the new test. Final gate (head 1ca004b95): 182 changed-code mutants — 168 Killed, 0 Survived, 0 NoCoverage, 0 timeout, 14 ignored (run 4 = verification run on the final compact-test artifact; run 3 on the pre-compaction test shape was green with identical counts); gate exit 0. (g) PRs: #56 (code: base feat/fws-v2-12-rollback-wiring, head 1ca004b95, 997 lines: CC 327 + spec 667 + ChatRow 3; tracker comment 5557798098) + #57 (locales: base feat/fws-v2-13-change-card-ui, head 6eb159311, 216 lines; tracker comment 5557804072), both labeled coderabbit-review-active + awaiting-coderabbit. (h) Watch sweep: #53/#54/#55 — all required checks green including mutation-diff, platform-unit-test (ubuntu+windows), CodeQL, webview-visual, check-translations, knip; no CodeRabbit comments on any of them yet (review gate in flight). (i) Next: v2-14 (B3c L2–L5 fixes: open-in-editor, native button, typed checkbox, CP/CP.spec/SV.spec, from the net of the local B3c line diffs) — re-measure the L2→L5 union shortstat against the new v2-13 head before splitting; then v2-15 (restore-latest ≈481 from 7a4d4e61b) and v2-16 (CR fixes ≈340 from a0693e9+ea90ea895).

round 25 (2026-09-06): v2-12..v2-16 were re-scoped into 5 units (the settings UI already shipped in v2-9, and the CI translation-parity gate forbids a standalone i18n unit), and v2-12 (B3b wiring) shipped as fork PR #55. (a) 5-unit re-scope (supersedes the #54 mapping): #54's "Not included here" promised UI+wiring as v2-12, restore-latest/a11y as v2-13, CR fixes as v2-14, i18n as v2-15 (v2-12 ≈1310 a+d). That mapping is invalidated: the settings UI (the changeCardDetail control + webview default + 18-locale labels) already landed in v2-9 (7ae24aff5), so the v2-12 settings delta ≈ 0; and the CI check-translations gate (code-qa.ymlscripts/find-missing-translations.js) fails any PR where a new en key lacks 18-locale parity, so new locale keys must ride with the unit that introduces them — i18n cannot be a standalone unit. Final 5 units (all ≤1000; only v2-13 exceeds the 400 soft target, unavoidably): v2-12 wiring 340 (done — #55); v2-13 L1 ChangeCard UI ≈858 (ChangeCard 309/0 + spec 312/0 + ChatRow 3/0 + 18 locales × ≈13; over-soft because the mutation gate binds the spec to the code in the same PR and CI parity binds the locales to the introducing unit — noted in the PR body); v2-14 L2+L3+L4+L5 fixes ≈350 (open-in-editor, native button, typed checkbox, CP/CP.spec/SV.spec, from the local line net diffs); v2-15 restore-latest ≈481 (from canonical 7a1d4e61b, NOT the abandoned-rebase artifact 9a430d6cd); v2-16 CR fixes ≈340 (from a0693e9e4 + ea90ea895; final-state lines from ea90ea895; it/ko = CR1 1/1 + CR2 3/3+1/1 nets). (b) v2-12 (B3b wiring) shipped as #55: worktree wt-fws-v2-12, branch feat/fws-v2-12-rollback-wiring, head aef395e05 (base 3e75647dc = #54 head). 3 files / 340 insertions: webviewMessageHandler.ts +81 (the checkpointRollbackFile / checkpointRollbackStep cases — each payload zod-validated, lazy await import("../checkpoints/rollback") so specs mocking vscode minimally never execute editor module-scope code at import time, posts checkpointRollbackResult correlated by cardTs; the no-task branch posts the correlated failure No active task to roll back from. — the only new user-visible string, a card diagnostic identical to the upstream B3b handler, not i18n-keyed → no locale change); packages/types/src/vscode-extension-host.ts +53 (B3b L1 net minus the changeCardDetail optional→required flip — deliberate deviation keeping v2-9's optional form, worktree L375; documented in the PR body); new 206-line webviewMessageHandler.rollback.spec.ts (success + per-file failure correlation, no-task failure, malformed-payload ignores; vi.mock of ../../checkpoints/rollback + vscode). Fidelity: the committed handler blob 876fec270 is byte-identical to B3b-final 502f8ca98:webviewMessageHandler.ts (0 content-diff lines after CRLF normalization); the spec blob 74a341c26 is identical across the local L1/L5 and canonical L1/L5 commits; API compatibility 39afe1b15:rollback.ts = blob 508789c95 = #54's rollback.ts (rollbackFile / rollbackStep signatures match). Local gates: check-types 11/11 (incl. packages/types tsc); vitest types 28 files / 404 tests + core/webview 28 files / 490 tests (incl. the new spec); eslint --prune-suppressions --max-warnings=0 clean + suppressions file content-equal (the re-indent artifact restored); prettier content-clean (CRLF-normalized helper; raw prettier --check is line-ending-noisy in the CRLF worktree). Stryker diff gate (local, base 3e75647dc head aef395e05, pr1066 win32 shim pair restored before push): 42 changed-code mutants in webviewMessageHandler.ts (79 changed lines), 42 Killed, 0 Survived, 0 NoCoverage, 0 timeout; test set = the 12 webviewMessageHandler*.spec.ts files (11 siblings + the new rollback spec; preferDirectTestFiles from the 42-suite related discovery). PR #55 OPEN (base feat/fws-v2-11-rollback-core; labels coderabbit-review-active + awaiting-coderabbit; tracker comment 5557055030; CI + CodeRabbit pending). (c) Next: v2-13 ChangeCard UI core from local L1 4c16c0017 (locales: port additions only — the 3 deleted en lines are stale "What's New" text, NOT ported).

round 24 (2026-09-06): the v2-7 visual regression was resolved and re-baselined, the stacked chain re-based onto the new v2-7 head, and v2-10 (shadow restore service) + v2-11 (rollback core) shipped as fork PRs. (a) Visual regression root cause (CLOSED): the two gray lines in electron-chat-dark-sidebar.png are SeeNewChangesButtons ("See New Changes" / "Restore Changes", en/chat.json L37-44) — a pre-existing upstream component (added in 0566607, Zoo-Code-Org#817; NOT part of any v2 diff) whose render trigger is getCompletionCheckpoint(messages) (packages/types/src/message.ts L373): it scans the FULL message list (ignoring suppressMessage) for a checkpoint_saved row. Upstream main's Task.ts posts the pre-prompt baseline checkpoint non-allowEmpty (no commit in the clean mock workspace → no message → no buttons in the old baseline 8f7f48ad5); B1's await this.checkpointSave(true, true) (per-write allowEmpty baseline) commits an empty baseline in the mock scene and posts a suppressed checkpoint_saved row — the row itself is hidden by the ChatView filter, but getCompletionCheckpoint still finds it and sets completionCheckpoint → the buttons render. Verdict: intentional feature UI (checkpoints + the B1 baseline are now active in the e2e mock), not a defect. Re-baselined by committing the deterministic CI actual (3/3 retries byte-identical, 33726 B, 6D6E9468…) as a new commit on #49v2-7 head is now 54bd92f71 (force-pushed; root cause documented in the PR body). #49 fully green on 54bd92f71 incl. extension-host-visual + mutation-diff. (b) Stacked rebase propagation: #50/#51/#52 did not auto-follow the v2-7 push, so the chain was re-based manually (single-commit rebases, --force-with-lease, pre-push check-types 11/11 each): v2-8a 2f6cb0acb77c435f45, v2-8b 49452076d63d9b0984, v2-9 4b8fdb6b57ae24aff5. CI on the new heads: #50 fully green; #51 all workflows green except mutation-diff, which hit the same transient related-test discovery failed as the local v2-10 run 6 (the related-tests JSON is written and green; the identical diff content passed the gate on the old-topology head 49452076d) — failed job re-ran; #52 visual (passing now, on the re-baselined snapshot) + mutation + e2e-mock + CodeQL green, Code QA in progress (incl. the previously flaky windows-unit worktree-include 5s timeout — an upstream spec untouched by the v2 chain, pure fs ops, failed job was a loaded runner). (c) v2-10 (shadow restoreFile service, B3c): code fixes committed at d9321220e (264 lines, 2 files): isRealPathContained via path.relative kills the L457 MethodExpression mutant, destructive-realpath symlink guard, success-log assertion kills the L475 StringLiteral mutant, 52-test spec (happy path, file-absent delete, Windows separators, ../symlink escape, invalid hash, uninitialized service). Local gate history vs the #52 head: run 1 6 Survived + 1 NoCoverage → fixes → run 2 2 Survived (L457/L475) → run 7 at the original head: 54 changed-code mutants, 54 Killed, 0 Survived / 0 NoCoverage (41 changed executable lines). Correction: the two earlier "silent" runs were actually successes — the local gate prints one log line on success (the summary goes to GITHUB_STEP_SUMMARY, unset locally) and the report lands at reports/mutation/extension/mutation.json; run 6's discovery failure was the only real transient (its related-tests JSON is complete with 0 failed tests; standalone discovery exits 0). The branch was re-based onto the re-baselined v2-9 head 7ae24aff5 (new head 2a499d841, blob-identical diff) after the final-head gate. PR #53 OPEN (base feat/fws-v2-9-settings-ui; labels coderabbit-review-active + awaiting-coderabbit). (d) v2-11 (rollback core, B3c): committed as 2d35562cb and re-based onto the new v2-10 head (head 7f8b54dca; rollback.ts blob 508789c9… byte-identical to B3c 10fdd195b). The first Stryker gate run reported 8 survivors — now fully diagnosed: 6 of them are the module-scope const error-message literals (rollback.ts L50–55), reported static: true / coveredBy: [] — under Stryker 10's long-lived in-process vitest runner, module-scope code executes once at first load and is never re-run per mutant (on-the-fly activation only), so those mutants are structurally unobservable at test time (proven by manual mutation: the pinning assertions DO kill the mutant when the spec is run directly); the other 2 were genuine assertion gaps (the L141 console.error message; the OptionalChaining in the changeJournal.ts L69 ENOENT guard). Fix, with both source files untouched: stryker.config.mjs +10 lines (coverageAnalysis: "perTest" + ignoreStatic: true → the 6 static mutants report Ignored with the machine-generated reason instead of Survived); rollback.spec.ts's service-error test extended to pin the exact console.error diagnostic; changeJournal.spec.ts gains a nullish-rejection rethrow test pinning the optional chaining; commit amended to 3e75647dc. Content: new src/core/checkpoints/rollback.ts (277 lines; rollbackFile / rollbackStep / restoreLatestFile — pre-step restore resolved from the B2 change journal: earlier entry's checkpoint, task-start baseline for the file's first change, multi-write steps through the first entry, latest-only rejection NOT_LATEST_ERROR, no-op success for never-written files; restores delegate to the v2-10 restoreFile) + 26-test rollback.spec.ts (real journal I/O via appendChange, mocked checkpoint service) + changeJournal.ts ENOENT-only semantics (absent file → []; any other read failure rethrown — an unreadable journal must not masquerade as empty) + 2 regression tests (EISDIR propagation; nullish-rejection rethrow). 794 changed lines (789+/5-, 5 files; above the 400 soft target — one unit by the plan's Stryker invariant; rationale in the PR body). Local gates: tsc 0, vitest 89/89 (core/checkpoints, 6 files), eslint 0 (suppressions content-equal), prettier 5/5 clean, husky full lint 11/11. Stryker diff gate at the pushed head 3e75647dc: 123 valid — 123 Killed, 0 Survived, 0 NoCoverage, 0 timeout; 6 Ignored (the static L50–55 literals; their values stay pinned by the exact assertions in rollback.spec.ts). PR #54 OPEN (base feat/fws-v2-10-shadow-restore-service; labels added). (e) Plan rows B12/B13 swap remains in force (v2-10 = service, v2-11 = core) — compile order requires the service first.

round 23 (2026-09-06): v2-8b and v2-9 shipped as fork PRs #51/#52, the stacked-chain compile fix landed, and the v2-2 CI remediation is pushed. v2-8b (remaining per-write tool wiring, B3a): completes the B3a tool-wiring slice — the per-write checkpoint writes of WriteToFileTool/EditFileTool/EditTool/SearchReplaceTool thread diffStats, the verbatim approval diff, and autoApproved so checkpointSave can force the compact change card; ApplyPatchTool now awaits its patch checkpoint (no fire-and-forget) and threads per-file stats into the whole-patch journal write. Local gates: vitest 148 passed / 5 skipped, eslint clean (suppression counts unchanged), prettier clean, Stryker diff vs 60c76ff: 84 mutants — 82 Killed, 2 Ignored (the two documented StringLiteral exclusions in WriteToFileTool.ts), 0 Survived / 0 NoCoverage. Standalone diff 789 lines (755+/34-, 11 files; above the 400 soft target, below the 1000 hard cap — the five tools share one wiring shape; rationale in the PR body). v2-9 (change-card settings UI, B3a): the CheckpointSettings "Change card detail" control (summary/full) bound to local cachedState and saved via the updateSettings payload, the ExtensionState pre-hydration default summary + mergeExtensionState retention, the changeCardDetail i18n label/description in all 18 locales, and the B3a-parity CheckpointSettings/SettingsView/ExtensionStateContext specs. Local gates: vitest 57, eslint clean, prettier content-clean 24/24, Stryker diff: 14 mutants — 14 Killed, 0 Survived / 0 NoCoverage (run vs pre-rebase base e6133eb; diff content unchanged by the rebase). Standalone diff 355 lines (337+/18-, 24 files; under the 400 soft target — 18 locale JSONs inflate the file count). PR #52 OPEN (head 4b8fdb6, base feat/fws-v2-8b-editsearch-wiring = #51; labels coderabbit-review-active + awaiting-coderabbit). Chain compile fix: the stacked PRs #49/#50/#51 failed CI tsc -b with three identical "Property 'changeCardDetail' is missing" errors — the type was added required in v2-7 but the webview context default only lands in v2-9; made it optional in v2-7 (all consumers already default to DEFAULT_CHANGE_CARD_DETAIL), amend force-pushed #49 (new head a552872), rebased + force-pushed #50 (2f6cb0a) and #51 (4945207); tsc -b now exits 0 at the v2-9 head. v2-2 CI remediation: the Windows unit failure root-caused — the CI runner's os.tmpdir() is the 8.3 short form C:\Users\RUNNER~1\... while safeWriteJson stages the temp beside the fs.realpath-resolved target (long form runneradmin), so the spec's expected path no longer matched; the deterministic-name spec now mirrors the implementation's resolution (local 23/23 green), amend force-pushed (new head d183e29). The Linux e2e restart-persistence failure (create phase fully passed; verify phase isTaskInHistory false immediately after init; no persistence errors anywhere in the log) is under investigation — v2-2's new write path adds a realpath + two fsyncs per JSON write, the most plausible timing delta; the re-run doubles as the flake-vs-regression check. CI re-running on all touched PRs (#45/#49/#50/#51 + new #52).

round 22 (2026-09-06): v2-8a shipped as fork PR #50. v2-8a (apply-diff tool wiring, B3a): apply_diff now gets the per-write checkpoint + change card with the same parity as write_to_file / edit_file / apply_patch — it reuses the unified diff + stats the tool already computed for its message (never recomputed), auto-approved steps get the compact card, the live perWriteCheckpoints setting keeps default-on semantics (skip only when explicitly false), and a checkpoint failure never blocks the write; ApplyPatchFileChange also gains the approval-diff fields (diff/diffStats/autoApproved) its apply_patch consumer wiring (v2-8b) will use. Worktree correction: wt-fws-v2-8a had been created on the v2-7 pre-amend head d549a0489 (not the final 11eeb8845), which the Stryker gate caught (it computed merge base 6c2ac075c); fixed with git rebase --onto 11eeb8845 d549a0489 (the two commits differ only in the v2-7 spec spy assertion, no overlap). Spec naming: applyDiffTool.changeCard.spec.ts renamed to ApplyDiffTool.changeCard.spec.ts — the gate's preferDirectTestFiles match is case-sensitive and apply.spec.ts is a direct match for apply.ts, so the lowercase name would have been excluded from the mutation test set (same convention as the v2-7 index.checkpointSave.spec.ts rename). Stryker: an intermediate run surfaced 4 survivors — the dead let completeMessage = "" initializer (both save branches overwrote it before use; the approval message is now computed once and shared, removing the unobservable literal) plus three checkAutoApproval call-site mutants (empty argument object, the ask: "tool" channel string, the state?. optional chaining) — all killed by hardening: call-argument pinning in the success test and a new test where providerRef.deref() returns undefined (the checkpoint path must stay alive with no provider state). Final run 21 changed-code mutants (all in the ApplyDiffTool.ts checkpoint block), all Killed — 0 Survived / 0 NoCoverage, no equivalent-mutant exclusions. Standalone diff 311 lines (294+/17−, 3 files; within the 400-line soft target). Local gates: vitest 5 suites / 57 passed (the new spec + the adjacent changeCard / apply.spec / applyPatchTool.execute / applyPatchTool.partial suites), eslint clean, check-types 11/11. PR #50 OPEN (head 60c76ff, base feat/fws-v2-7-changecard-core = #49; labels coderabbit-review-active + awaiting-coderabbit). v2-8b (apply-patch/edit/search tool wiring) is being built on the v2-8a head next; v2-9 (settings UI + i18n) stacks on v2-8b. v2-5/v2-6 remain blocked on upstream Zoo-Code-Org#1383/Zoo-Code-Org#1394 (still OPEN this round).

round 21 (2026-09-06): v2-7 shipped as fork PR #49. v2-7 (change-card core, B3a): the per-step change-card pipeline end to end on the extension host — the change_card ClineSay + payload schemas in packages/types, the changeCardDetail user setting (shared default summary), buildChangeCardPayload (reuses the approval diff/stats the tool already computed; auto-approved steps always downgrade to summary), the checkpointSave emission hook (strictly additive on the v2-4 journal hook; a card failure is logged and never disables checkpoints), and the ClineProvider storage-to-webview round trip. Built from v2-4 head 6c2ac07 as feat/fws-v2-7-changecard-core; local gates: vitest 10 (types) + 186 (src), full turbo lint 11/11 via pre-commit, check-types 11/11. Stryker diff vs 6c2ac07: the first run failed at the gate — root cause was the gate's own preferDirectTestFiles narrowing (the emission spec's basename matched no changed source name, so the suite was excluded from the mutation test set); fixed by renaming it to index.checkpointSave.spec.ts (matching the v2-4 index.checkpointJournal.test.ts convention for tests of index.ts), after which 2 survivors remained (the emission catch block as an empty block + its error-message string) — killed by a spy assertion pinning the exact console.error message in the emission-failure test. Final run 42 changed-code mutants (31 changeCard / 9 index hook / 2 ClineProvider), all Killed — 0 Survived / 0 NoCoverage, no equivalent-mutant exclusions needed. Standalone diff 566 lines (564+/2−, 10 files; over the 400 soft target — payload builder + hook + setting round trip + both specs is one unit; rationale in the PR body). PR #49 OPEN (head 11eeb88, base feat/fws-v2-4-journal-wiring = #48; labels coderabbit-review-active + awaiting-coderabbit). Ledger correction: the Phase 0c row — Zoo-Code-Org#1382 was CLOSED unmerged on 2026-09-03, superseded by #1496 (d48f46d; CodeRabbit APPROVED 2026-09-04; awaiting human approval). Merge-tree re-verified vs upstream main 4140c2c: all seven open upstream heads clean (Zoo-Code-Org#1380 dcd729b, Zoo-Code-Org#1381 065bef1, Zoo-Code-Org#1383 f89c1c6, Zoo-Code-Org#1394 05c845d, Zoo-Code-Org#1384 7519533, Zoo-Code-Org#1404 0ea153d, Zoo-Code-Org#1496 d48f46d). v2-8a (apply-diff tool wiring) is being built on the v2-7 head; v2-8b/v2-9 follow in the same stack. v2-5/v2-6 remain blocked on upstream Zoo-Code-Org#1383/Zoo-Code-Org#1394 (still OPEN this round).

round 20 (2026-09-06): v2-3 and v2-4 shipped as fork PRs. v2-3 (per-task change journal core): new module src/core/checkpoints/changeJournal.ts (ChangeJournalEntry { path, operation, checkpointId, diffStats? }, single-appendFile-syscall appendChange, torn-tail-repairing loadChanges) + an optional write parameter on checkpointSave — after a real commit, one journal line per written file with the commit SHA as checkpointId; journal failures are logged and never propagate. Built on Zoo-Code-Org#1404's (B1) head 0ea153d as feat/fws-v2-3-journal-core; local gates: vitest 45/45 (12 changeJournal + 8 checkpointJournal-hook + 25 existing checkpoint tests), full turbo lint 11/11 via pre-commit, check-types 11/11; Stryker diff vs 0ea153d: 5 Survived in the hook (default-param invisibility + catch-swallowed TypeError) killed via console.error-never-called + saveCheckpoint call-args assertions; final run 0 Survived / 0 NoCoverage; four equivalent mutants in the loadChanges parse loop excluded with scoped Stryker disable next-line directives (the JSON.parse catch makes the mutated parse path behaviourally identical). Standalone diff 522 lines (521+/1-, 4 files; over the 400 soft target — module + hook + both specs are one unit; splitting the hook spec fails the gate as NoCoverage — verified). PR #47 OPEN (head 4e95e38, base feat/per-write-checkpoints-b1 = Zoo-Code-Org#1404 head). v2-4 (write-family tool wiring): WriteToFileTool/EditFileTool hoist the approval-diff stats into the journal entry; ApplyPatchTool handlers return { succeeded, wrote } (a no-op update journals nothing), one journal entry per written file (move → final location via movePath ?? path), plus 3 behavioral fixes (partial patch flush on rooignore break instead of return; mistake-counter reset only after a fully successful patch; a failed move source-delete now reports the error + increments the counter while the written destination is still checkpointed and journaled). Built on #47 head 4e95e38 as feat/fws-v2-4-journal-wiring; local gates: vitest 109 passed | 5 skipped (5 files; new mutation-killing assertions — counter reset 1→0 on successful add/delete, first-hunk access-denied → no checkpoint, move-failure counter 1→2 + exact error message, both WriteToFileTool approval branches thread the exact diffStats into the askApproval JSON), eslint 0 (suppression counts unchanged, 346 entries), check-types 11/11; Stryker diff vs 4e95e38: run 1 → 26 Survived / 0 NoCoverage; all 16 test-visible survivors killed by the new assertions (verified mutant-by-mutant empirically before the run); the ten equivalent return { succeeded: false, wrote: false } ObjectLiteral mutants (fields consumed only in falsy contexts: succeeded in a logical-AND, wrote in an if — undefined && xfalse && x) excluded with 10 scoped Stryker disable next-line ObjectLiteral directives; run 2 99 changed-line mutants, 0 Survived / 0 NoCoverage (109 total, 10 directive-ignored). Standalone diff 567 lines (512+/55-, 6 files; over the 400 soft target — the three tools are one logical unit; the handler return-type refactor touches every return site). PR #48 OPEN (head 6c2ac07, base feat/fws-v2-3-journal-core = #47 head). Next: v2-5/v2-6 blocked on Zoo-Code-Org#1383/Zoo-Code-Org#1394 landing on main; v2-7+ queued on the v2-3/4 stack; re-sync Zoo-Code-Org#1403/Zoo-Code-Org#1408 after the v2 bases land; Phase C closures after the v2 series lands.

round 19 (2026-09-06): v2-2 shipped as a fork PR + Zoo-Code-Org#1408 CodeRabbit dispositions. v2-2 (safeWriteJson rewrite): delegates backup/commit/rollback to the v2-1 atomic-publish primitive — one safeWriteText(absoluteFilePath, "", { tempPath, backup: true }) call (content intentionally empty; the staged temp file is the content source). Built from #43 head 29110ed as feat/fws-v2-2-safewritejson; local gates: vitest 54/54 (23 safeWriteJson spec + 31 safeWriteText spec) + 465/465 across all 17 consumer suites, eslint --prune-suppressions 0 (counts only decreased: impl 4→3, spec 27→26), check-types 11/11; Stryker diff vs 29110ed: first run found 1 survivor — the equivalent StringLiteral mutant on the "" content argument (dead when tempPath is supplied); killed by a boundary pin test (spy wrapping the real safeWriteText implementation asserts the exact boundary call including the empty content string); final run 6 changed-line mutants, 0 Survived / 0 NoCoverage (StringLiteral 2, MethodExpression 1, OptionalChaining 1, ObjectLiteral 1, BooleanLiteral 1). Standalone diff 215 lines (99+/116-, 3 files; merge-tree measured vs 29110ed; well under the 400 soft target). PR #45 OPEN (head 2ecc5d4, base feat/fws-v2-1-atomic-publish-core — rebase to main after #43 merges). Zoo-Code-Org#1408 (S4b) CodeRabbit dispositions: (a) ENOENT finding — evidence reply posted (fixed at the S4a source in 7a25fc0 via replaceIfVersion normalization, inherited by the Zoo-Code-Org#1408 rebase); (b) diff-view saveChanges() unguarded user-accept path — intentionally deferred, reply posted + follow-up issue #44 (observation/version check on the diff-view save); (c) coderabbit follow-up on observation completeness (processTextFile() can deliver a slice/truncated/indentation block while the observation stays file-level — guardedWrite() update must require a complete observation) — reply posted + follow-up issue #46 (track read scope in FileObservation). Fork account cannot resolve upstream threads (no write access to upstream) — dispositions recorded in-thread. Next: v2-5/v2-6 (after Zoo-Code-Org#1383/Zoo-Code-Org#1394 land on main), then v2-3/v2-4 on the 1404 base.

round 18 (2026-09-05): Phase B kickoff — v2-1 shipped as a fork PR. Upstream main advanced 0d937c00dbd584 (v3.82.0 release prep Zoo-Code-Org#1533 + commits; 0d937c0 remains an ancestor, so the round-17 heads all still merge-tree clean vs 0dbd584 — verified for 1380/1381/1383/1394/1384/1404/1496; no upstream commits touched the file-safety paths). v2-1 atomic-publish-core: built from 0dbd584 as feat/fws-v2-1-atomic-publish-core; content = S3-reviewed head a37dd24 (Zoo-Code-Org#1395) + 4 documented micro-refactors for the 100% mutation gate (default-encoding Buffer.from; simplified ENOENT guard to typeof x === "object" && x !== null — behaviour-identical for non-null objects; const opts = options ?? {}; dropped the redundant win32 guard on the post-commit DACL restore); local gates: vitest 31/31, eslint --prune-suppressions 0 with unchanged counts, check-types 11/11, Stryker diff vs 0dbd584 = 115 mutants (109 killed + 6 timeouts, 0 surviving / 0 NoCoverage); standalone diff 976 lines (merge-tree measured; soft zone 400–1000, rationale in PR body). PR #43 OPEN (head 29110ed, base main 0dbd584). v2-2 worktree prepared from 0dbd584 (install green). Note: no legacy commit ever touched utils/safeWriteJson.ts — the safeWriteJson refactor named in the S3 spec never landed in the stack, so v2-2 is a fresh rewrite delegating to safeWriteText (branched off #43's head for the import; rebase to main once #43 lands). Next: v2-2, then v2-5/v2-6 after Zoo-Code-Org#1383/Zoo-Code-Org#1394 land, v2-3/v2-4 on the 1404 base.

round 17 (2026-09-05): Upstream/main sync + series state refresh (plan approved). Phase A: upstream/main moved to 0d937c0 (36 commits ahead of the stale base 78c712a); additively merged into the six open non-stacked PRs, each merge verified pure main drift (zero branch-file changes, zero conflicts): Zoo-Code-Org#1380 a87dda2 + main -> dcd729b, Zoo-Code-Org#1381 309ffd6 + main -> 065bef1, Zoo-Code-Org#1383 2a42f88 + main -> f89c1c6, Zoo-Code-Org#1394 a00eef8 + main -> 05c845d (all four pushed 2026-09-05). Zoo-Code-Org#1384 d038774 + main -> 7519533 and Zoo-Code-Org#1404 abc10b4 + main -> 0ea153d were initially blocked on the husky pre-push pnpm check-types gate (TS2339: mount missing on 18 main-derived webview-ui visual test files) — root cause: the worktrees still had @playwright/test 1.60.0 installed while the merged lockfile pins 1.62.1 (upstream Zoo-Code-Org#1426 reworked the visual suite: dropped @playwright/experimental-ct-react; mount is now a stable fixture) — pnpm install --frozen-lockfile in both worktrees -> tsc 0 errors, pre-push hook 11/11 -> pushed. All six heads mergeable (mergeStateStatus BLOCKED = awaiting maintainer approval per the Protect-Main ruleset, not CI). Zoo-Code-Org#1496 (head d48f46d, base = main, CodeRabbit APPROVED 2026-09-04 16:42 UTC) awaits human approval — no sync needed. Ledger correction: Zoo-Code-Org#1382 closed 2026-09-03 (not merged) — superseded by Zoo-Code-Org#1496 (same fix/abandoned-subtask-save-race-1021 fix rebased on current main). Phase B: standalone diff audit vs 0d937c0 (additions + deletions): Zoo-Code-Org#1395 = 1149, Zoo-Code-Org#1405 = 2926, Zoo-Code-Org#1406 = 1924, Zoo-Code-Org#1410 = 3944, Zoo-Code-Org#1411 = 3556, Zoo-Code-Org#1412 = 5943 — all above the 1000 hard cap -> rebuild from main as small v2 PRs feat/fws-v2-N-slug: v2-1 atomic-publish-core (~921), v2-2 safeWriteJson-rewrite (~226), v2-3 journal-core (~496), v2-4 journal-tool-wiring (~657), v2-5 guardedwrite-core (~991), v2-6 readfile-integration (~493), v2-7 changecard-core (~570), v2-8 changecard-tool-wiring (~750), v2-9 changecard-settings-i18n (~450), v2-10 rollback-core (spec describe split; <=1000; Stryker-verified), v2-11 rollback-service-settings (~500), v2-12 changecards-ui-core (<=1000), v2-13 ui-restore-openfile (~600), v2-14 ui-cr-fixes (~350), v2-15 ui-i18n (~450). Zoo-Code-Org#1403 (L1) and Zoo-Code-Org#1408 (S4b) kept as PRs — re-synced after the v2 base lands (-> ~293 / ~877). Zoo-Code-Org#1413 (TRIAL) closes after the components land (pending user confirmation). Phase C: close old Zoo-Code-Org#1395 / Zoo-Code-Org#1405 / Zoo-Code-Org#1406 / Zoo-Code-Org#1410 / Zoo-Code-Org#1411 / Zoo-Code-Org#1412 as superseded (the stale CHANGES_REQUESTED on Zoo-Code-Org#1412 voids on close); Zoo-Code-Org#1414 / Zoo-Code-Org#1402 / Zoo-Code-Org#1435 stay open; epic Zoo-Code-Org#1375 body not editable by the fork account. CI note: main now requires mutation tests on changed code (commit 79cd12f) — Stryker diff gate runs locally per PR pre-push (Windows shim).

round 13 (2026-08-29): UI/UX review of the change-card rollback flow found a P1 semantic bug - the card rollback restored a file to the step's own POST-write checkpoint (a no-op for the newest step; contradicts the epic AC "undo what the agent did" and the card's own confirm copy). User chose Option A + forward restore, shipped as: (1) Zoo-Code-Org#1410 head 630f273 - rollback now resolves the restore target from the B2 change journal to the file's PRE-step state (preceding journal entry's checkpoint; task-start baseline for the file's first change; undoing a create deletes it again, undoing a delete restores it) + new restoreLatestFile (forward direction; clean no-op success when the task never wrote the file); 20-test spec; all local gates green (vitest 20/20, tsc 0, eslint 0 with suppression unchanged, 100% changed-line coverage); merge-tree clean vs upstream/main efc30cf. (2) Zoo-Code-Org#1412 head 9a430d6 (stacked on 630f273) - per-file "Restore latest version" control (two-step confirm + warning), new checkpointRestoreLatestFile webview message + handler case (results reuse checkpointRollbackResult carrying kind rollback/restore-latest; legacy no-kind results still route to the rollback control), per-file rollback confirm now shows the warning text, 5 new/updated chat:changeCard.* keys x 18 locales, +5 ChangeCard tests + +4 handler tests; merge-tree clean vs efc30cf. New upstream tracking issue Zoo-Code-Org#1435 documents the semantic correction; both PR bodies refilled. Trial addendum 9: head 7591bc8 (cherry-picks f0f3313 + 7591bc8 applied cleanly onto d6d08e8; local gates: tsc 0 both dirs, eslint 0, vitest 31+22 green, merge-tree clean; Code QA CI running). Fork PR #34 (-> trial) remains open, CI 15/15, head 7efbf33 merge-tree clean vs the new trial tip 7591bc8. The fuzzy-match observability/perf suggestions stay tracked as upstream Zoo-Code-Org#1418; all other previously-green heads unchanged)

PR Branch State
Phase 0a Zoo-Code-Org#1380 fix/mcp-settings-stub-race-1371 open, CI green, awaiting review (tracking #1385); round 10: head 9dd9825 (CodeRabbit fixes from trial addendum: mcp merge array guard, spec-mock production parity, unknown-safe error.code; CI green); round 17: head dcd729b (merged upstream/main 0d937c0; pushed 2026-09-05; mergeable)
Phase 0b Zoo-Code-Org#1381 fix/write-delay-default open, CI green (fixed a missed DiffViewProvider spec assertion), awaiting review (tracking #1386); round 10: head 6548013 (CodeRabbit fixes from trial addendum: webview writeDelayMs now uses the shared DEFAULT_WRITE_DELAY_MS + pre-hydration test; CI re-running); round 17: head 065bef1 (merged upstream/main 0d937c0; pushed 2026-09-05; mergeable)
Phase 0c Zoo-Code-Org#1382 fix/abandoned-subtask-save-race-1021 closed 2026-09-03 (not merged) — superseded by #1496 (same fix on current main; head d48f46d; CodeRabbit APPROVED 2026-09-04; awaiting human approval; tracking #1387)
S1 Zoo-Code-Org#1383 feat/version-token-s1 open — #1383 (3 commits; final review APPROVE; CI green on ubuntu gate; tracking #1388; round 17: head f89c1c6 (merged upstream/main 0d937c0; pushed 2026-09-05; mergeable)
L2 Zoo-Code-Org#1384 feat/l2-chat-diff-default open — #1384 (74 lines, 1 commit; ALL checks green incl. ubuntu+windows unit + codecov/patch; CodeRabbit 3/3 findings confirmed fixed & replied in c0ef476; tracking #1389; round 17: head 7519533 (merged upstream/main 0d937c0; pre-push type-check gate initially failed on 18 main-derived visual test files — stale worktree deps: @playwright/test 1.60.0 installed vs 1.62.1 in the merged lockfile after upstream Zoo-Code-Org#1426's visual-suite rework; pnpm install --frozen-lockfile fixed it, then pushed 2026-09-05; mergeable)
S3 Zoo-Code-Org#1391 feat/atomic-publish-s3 open — #1395 (amended to a37dd24 after the 5th CodeRabbit pass: the "no temp left on failure" spec now drives a real post-commit backup cleanup failure (unlink EPERM) and asserts the target stays committed with no temp behind — the non-fatal cleanup path is now covered; 4th-pass tempPath fchmod fix retained; local gates green: 49 unit, safeWriteText.ts 100% stmts/branch/lines, eslint 0, tsc 0; ALL checks green incl. ubuntu unit + e2e-mock + codecov/patch; CodeRabbit review completed; 14/14 findings replied; tracking #1391)
S2 Zoo-Code-Org#1390 feat/observation-registry-s2 open — #1394 (stacked on Zoo-Code-Org#1383, targets main; CodeRabbit findings addressed in 2965ad1, ubuntu CI green; tracking #1390; round 17: head 05c845d (merged upstream/main 0d937c0; pushed 2026-09-05; mergeable)
L1 Zoo-Code-Org#1396 feat/async-save-diagnostics-l1 open — #1403 (991ab69, rebased onto S3 head a37dd24: preDiagnostics race fixed; diagnostics tail filtered to the saved file via arePathsEqual (case-insensitive on Windows); stale .catch comment corrected; as any -> bracket notation (ledger 310->306); 4th CodeRabbit pass fixed — 100 ms in-memory settle delay moved from the blocking save path into the diagnostics tail (saves with diagnostics off / writeDelayMs 0 no longer pay it); 77/77 unit, eslint 0, tsc 0; all 12 CodeRabbit findings replied — 3 cured by the S3 rebased base; e2e-mock passed (subtasks resumeTask flake did not reproduce); stacked on Zoo-Code-Org#1395 (S3); tracking #1396)
B1 feat/per-write-checkpoints-b1 open — #1404 (amended to abfbe7f (round 3: task-start baseline now awaited before the request loop + deferred-promise test) — perWrite garbled i18n fixed in 7 locales, task-start baseline forced, CheckpointSettings spec updated; ALL checks green incl. ubuntu+windows unit + codecov/patch, independent on upstream main 78c712a: per-write checkpoints in write_to_file/edit_file/apply_patch — checkpoint now gated on full patch success (handlers report success; no checkpoint after rejected approval / failed write), task-start baseline guard (incl. unset default-on test), perWriteCheckpoints setting default-on round-trip incl. explicit-false webview-state test + 17-locale translations; ubuntu CI + e2e-mock + codecov green; all 5 CodeRabbit findings replied (1 false positive — single shared default constant verified); local gates: eslint 0, tsc 0 src+webview, affected suites green; tracking #1397; round 17: head 0ea153d (merged upstream/main 0d937c0; same stale-deps type-check fix as Zoo-Code-Org#1384 — pnpm install --frozen-lockfile, then pushed 2026-09-05; standalone diff 942 lines = soft zone, noted in the PR body; mergeable)
S4a feat/guarded-write-s4a open — #1405 (amended to 7a25fc0 — enqueue settled-chain eviction + replaceIfVersion ENOENT normalization + regression tests; ubuntu + codecov/patch green after the amend; on S3 head a37dd24 + S1×3 + S2 stack: CAS core + per-path FIFO chain + registry hook; CodeRabbit pass fixed — resolveAbsolutePath always path.resolve (registry-key match), pre/post-read bigint stat token capture on native + legacy read paths (mutation mid-read leaves target unobserved), safeWriteJson locks the resolved publish target (symlink-alias coordination); local gates: 169 units, 100% line coverage on changed lines, eslint 0, tsc 0; 3/3 findings replied; tracking #1399)
S4b feat/guarded-write-wiring-s4b open — #1408 (rebased to 68be264 on amended S4a 7a25fc0 — inherits the ENOENT fix; CI re-running; guard wired once at the DiffViewProvider.saveDirectly choke point (6 tools / 7 call sites), per-tool writeKind plumbing, fail-closed on collected taskRef; local gates: 188 units, 100% on changed lines, eslint 0, tsc 0; tracking #1400; round 19: CodeRabbit dispositions complete — (a) ENOENT evidence reply posted (fixed at the S4a source in 7a25fc0), (b) diff-view saveChanges() unguarded user-accept path intentionally deferred → fork issue #44, (c) observation completeness (slice/truncated reads vs file-level observation) → fork issue #46; all threads replied (3874929258, 3874929467, 3941952878, 3941953632); thread resolution awaits maintainer access)
B2 feat/change-journal-b2 open — #1406 (amended to 93a8329 (round 3: mistake counter now resets only on a fully successful patch + 2 regression tests) on B1 head baacf59 — handler result objects, no-op/move gating, partial-flush gate patchSucceeded
B3a feat/change-cards-b3a open — #1411 (amended to 2500ab3 (round 3: apply-patch + edit-file checkpoints now awaited, no fire-and-forget interleaving + 2 deferred-promise tests) on B2 head 410591e — round-2 CR fixes: checkpointSave.spec negative assertions now filter recorded say calls by the change_card type (the three-argument toHaveBeenCalledWith can never match the seven-argument call, so the old assertion guarded nothing), mergeExtensionState spec fixture uses non-default perWriteCheckpoints/changeCardDetail and asserts a partial push that omits the keys preserves them; CI re-running; change_card payload + checkpointSave emission, tool approval-diff plumbing (WriteToFile / EditFile / ApplyPatch), changeCardDetail setting full round trip, i18n 18 locales; split from the planned B3a scope by the 1000-line cap — 934 changed lines; local gates: 429 units, eslint 0 src+webview+types, tsc 0 both dirs, 100% on changed lines; round 11: head 341a9c2 (94fea2f + CodeRabbit fix: pre-hydration defaults spec now asserts both the changeCardDetail and perWriteCheckpoints initializer defaults; CI GREEN); round 12: head 67d8525 (trial-review audit: the edit and search_replace tools wrote with no per-write checkpoint / journal entry / change card — both now wired with the same checkpointSave call site + the auto-approved compact-card flag; focused 8-test spec; merge-tree clean vs upstream/main; CI running); tracking #1401)
B3c feat/rollback-service-b3c open - #1410 (round 13: head 630f273 - rollback semantic correction, upstream Zoo-Code-Org#1435: rollbackFile/rollbackStep resolve the restore target from the B2 change journal to the file's PRE-step state (preceding journal entry's checkpoint; task-start baseline for the file's first change; undoing a create deletes it again, undoing a delete restores it); new restoreLatestFile(task, filePath) forward direction (no-op success when the task never wrote the file); 20-test spec; incremental diff 2 files +371/-134; local gates: vitest 20/20 + checkpoints dir 79/79, tsc 0, eslint 0 (suppression unchanged), 100% changed-line coverage; merge-tree clean vs upstream/main efc30cf; CI running; tracking #1409 + semantic correction #1435)
B3b feat/change-cards-ui-b3b open - #1412 (round 13: head 9a430d6 stacked on B3c 630f273 - per-file "Restore latest version" control (two-step confirm + warning text), new typed checkpointRestoreLatestFile webview message + handler case (results reuse checkpointRollbackResult with kind rollback/restore-latest so the two per-file controls never cross-talk; legacy no-kind results still route), per-file rollback confirm now shows the warning text, i18n: 5 new/updated chat:changeCard.* keys x 18 locales, ChangeCard spec +5 tests, handler spec +4 tests; incremental diff 27 files +929/-150 (includes the B3c service files arriving via the stack); local gates: tsc 0 both dirs, eslint 0 (suppression unchanged), vitest 22+11 green, 100% changed-line coverage, i18n parity 18/18; merge-tree clean vs upstream/main efc30cf; CI running; raw-diff budget exception still documented in the PR body; tracking #1402 + semantic correction #1435)
v2-1 A4/S3 rebuild Atomic text publish primitive (safeWriteText) — v2 rebuild of Zoo-Code-Org#1395 content + mutation-gate micro-refactors
v2-2 A4/S3 rebuild (safeWriteJson) safeWriteJson delegates backup/commit/rollback to safeWriteText (v2-1) — streaming serialize kept; inherits symlink-resolved staged temp, mode preservation, pre-rename fsync, Windows DACL save/restore
v2-3 B2 rebuild (journal core) Per-task JSONL change journal module (single-syscall append, torn-tail-repair load) + checkpointSave optional write-info hook — one entry per written file, commit SHA as checkpointId
v2-4 B2 rebuild (tool wiring) Journal wired into write_to_file / edit_file / apply_patch (one entry per written file, move → final location) + 3 behavioral fixes (partial patch flush, counter reset only on full success, move source-delete failure)
v2-7 B3a rebuild (change-card core) Per-step change card end to end: change_card ClineSay + schemas, changeCardDetail setting (default summary), buildChangeCardPayload (reuses tool-computed diff/stats; auto-approved → summary), checkpointSave emission hook (additive; failure never disables checkpoints), ClineProvider round trip
v2-8a B3a rebuild (apply-diff tool wiring) apply_diff per-write checkpoint + change card with parity to the other wired tools (reuses the tool-computed unified diff/stats; auto-approved → summary; live perWriteCheckpoints default-on; failure never blocks the write); ApplyPatchFileChange gains the approval-diff fields for the v2-8b consumer
v2-8b B3a rebuild (remaining per-write tool wiring) completes the B3a tool-wiring slice: diffStats/verbatim approval diff/autoApproved threading through WriteToFileTool/EditFileTool/EditTool/SearchReplaceTool so checkpointSave forces the compact card; ApplyPatchTool awaits its patch checkpoint and threads per-file stats into the journal
v2-9 B3a rebuild (change-card settings UI) CheckpointSettings "Change card detail" control (summary/full) bound to local cachedState via the updateSettings payload; ExtensionState pre-hydration default + merge retention; changeCardDetail i18n label/description in 18 locales; B3a-parity CheckpointSettings/SettingsView/ExtensionStateContext specs
v2-10 B3c rebuild (shadow restore service) ShadowCheckpointService.restoreFile(commitHash, filePath) — single-file restore without moving the branch or truncating the checkpoint list (a file absent at the checkpoint is deleted instead); lexical + realpath containment guard (symlink-transparent on the destructive branch), Windows separator normalization, commit-object evidence check before the delete branch; 52-test spec
v2-11 B3c rebuild (rollback core) rollback.ts: rollbackFile / rollbackStep / restoreLatestFile — pre-step restore resolved from the B2 change journal (earlier entry's checkpoint; task-start baseline; multi-write steps through the first entry; latest-only rejection NOT_LATEST_ERROR; no-op success for never-written files); restores delegate to #53's restoreFile; changeJournal ENOENT-only read semantics + 2 regression tests (EISDIR; nullish rethrow); 26-test spec
v2-12 B3b rebuild (message wiring) webviewMessageHandler +81: checkpointRollbackFile / checkpointRollbackStep cases (zod payload validation, lazy rollback.ts import, posts checkpointRollbackResult correlated by cardTs, no-task correlated failure); host types +53 (2 webview messages + zod payload schemas + CheckpointRollbackResult); new 206-line rollback spec (11 sibling handler specs + new); deliberate deviation: changeCardDetail optional→required flip not ported (v2-9's optional kept)
v2-13 B3b rebuild (ChangeCard UI + locales) ChangeCard.tsx 327 (B3c L2 + four Stryker disable next-line directives) + ChangeCard.spec.tsx 667 (L2 + 13 round-2 mutation-coverage tests, incl. the gate-driven type-guard drop test) + ChatRow.tsx 3 (wiring) = 997 lines in #56; 18 locales × 12 changeCard keys = 216 lines in #57; soft cap 400 exceeded — the mutation gate binds spec+component into one unit and CI parity binds locales to the introducing unit (rationale in the PR bodies)
v2-14 B3c rebuild (ChangeCard L5 open-in-editor) ChangeCard.tsx +24 (open-in-editor helper, onJumpToFile → CodeAccordion, native ghost Button change-card-file-open-N codicon-link-external), CheckpointSettings.tsx +22/−7 (typed CheckboxEventTarget handlers + 3 directives; byte-parity to L5), CP.spec +19 (enable check/uncheck tests + mock entry — the base spec had no enable coverage), ccspec +62 (openFile mock + 3 tests + title assertion), SV.spec +6/−1 (mock currentTarget forwarding), 18 locales +2/−1 (openFile key + ca rollbackFailed retranslation)
v2-15a B3b restore-latest API (message + handler) packages/types vscode-extension-host.ts +27/−5 (checkpointRestoreLatestFile + payload schema; CheckpointRollbackResult kind/noOp), webviewMessageHandler.ts +44 (safeParse → lazy restoreLatestFile import → checkpointRollbackResult with kind: "restore-latest" + no-task error), rollback spec +96/−1 (4 new tests)
v2-15b B3b restore-latest UI (per-file control + rollback warning copy) ChangeCard.tsx +105/−10 (idle→confirm→pending→success/error control, fileRestores state, per-file kind-routed results; upstream v2-15 + 15 directive/justification lines), ccspec +187/−4 (upstream v2-15 spec + local superset + 4 new mutation tests), 18 locales +5 (5 changeCard keys)
v2-16 B3b correlated failures + localized no-task errors + error-state a11y webviewMessageHandler.ts +80/−39 (try/catch on the 3 change-card cases posting correlated checkpointRollbackResult failures), rollback spec +76/−3 (i18n mock + 3 correlated-failure/no-task tests), 18 backend common.json +2 each (localized no-task keys), ChangeCard.tsx +15/−2 (focusable role="status" error spans with error aria-label, {{path}} open-file labels; upstream v2-16 + 15 directive/justification lines), ccspec +83/−8 (upstream v2-16 spec + 1 new round-2 mutation test), 18 webview locales (openFile slot + es/hi/it/ko fixes)
S5 blocked on upstream Zoo-Code-Org#1319 (open)
S6 blocked on upstream Zoo-Code-Org#1066 (CHANGES_REQUESTED)
TRIAL feat/fws-trial-all open - #1413 (trial build of all 15 + the B3c/B3b semantic correction; round 13 head 7591bc8 = d6d08e8 + cherry-picks of Zoo-Code-Org#1410 630f273 (f0f3313) and Zoo-Code-Org#1412 9a430d6 (7591bc8), both applied cleanly; local gates: tsc 0 both dirs, eslint 0, vitest 31+22 green, merge-tree clean vs efc30cf; Code QA CI running - PR body will be refilled with the new run/artifact/inner-vsix sha256 once it settles; screenshot release assets (v3.80.0-fws-trial tag) unchanged)

Blockers to watch: Zoo-Code-Org#1066 (S6), Zoo-Code-Org#1319 (S5), Zoo-Code-Org#1046 landing (raises A3 urgency), Zoo-Code-Org#1379 merge (Task.ts rebase).

Activity

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

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions