refactor(cli): reconcileRun becomes a reconcileSession (#232) - #253
Merged
Conversation
reconcileRun was 375 lines (internal/cli/reconcile.go:101-475) with five `goto done`s (three EOF exits, two [q]uits), two labeled loops and six pieces of run-scoped state travelling as loose locals. It is now a 28-line entry point over a reconcileSession whose methods are the two prompts (promptOrphan, promptItem), the --auto-* dispatch (resolveAuto), the action switch (applyAction) and the run's tail (finish). Every goto is a plain return out of walk; finish has exactly one call site and is deliberately not deferred, because it writes (the override re-apply, the state save) and returns the run's error. The bulk-action byte + `ch | 0x20` folding becomes a typed reconcileAction (zero value invalid, the adapter.SkipKind convention) and one parseItemKey table, which writes down the two keystrokes the prompt deliberately does NOT fold: 'I' is not a bulk ignore and 'D' is not a diff. Uniform folding would add a bulk-ignore with no confirmation step (#155). attemptWriteBack, removeDroppedSource and itemSourceFile become methods; canonicalHookEvents(c) is computed once per run instead of per [w] keystroke. No dest→source write moves: capture.Capture 1→1, source.Write* 1→1, iox.AtomicWrite 2→2, os.Remove 3→3, //nolint 4→4; removeDroppedSource keeps its keystroke gate and withinDir bound; the redaction map is read at the same two renderItemValues sites. .golangci.yml is untouched. Behaviour preservation: 39 scripted-stdin scenarios (every prompt, bulk confirmation, EOF, quit — including quit with a queued override — auto mode, project-scope override, exit code, masked secret and the resulting tree) are byte-identical between the 309fde0 binary and this one, after a base-vs-base run proved the harness deterministic (195 files). Zero test files are edited. Measured against this suite, nine control-flow mutations of the new methods fail no test (item-prompt EOF not stopping, EOF mid bulk-confirm not stopping, quit not stopping, orphan-prompt EOF not stopping, bulkTargets = len(rest), uniform key folding, a confirmed bulk choice not recorded, stateDirty not reset after the override save, finish called twice); the next commit pins them. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M4VNyoCuGXx7pYxNfLVbFG
…#232) Before the session type, the interactive loop was reachable only by running the whole 375-line reconcileRun against a real tree, so the paths that decide when a pass STOPS were pinned by nothing. Measured on the commit-1 tree with the pre-existing suite, these mutations failed zero tests: item-prompt EOF not stopping, EOF mid bulk-confirm not stopping, [q]uit not stopping, orphan-prompt EOF not stopping, bulkTargets = len(rest), uniform key folding (I = bulk ignore, D = diff), a confirmed bulk choice not recorded, stateDirty not reset after the override save, finish called twice. reconcile_session_internal_test.go drives the methods directly over production's own bufio.Reader (not a fake, so readChar sees the real EOF): parseItemKey's table including the two deliberate non-foldings, bulkTargets over mixed queues, the three EOF exits, both quits, a walk that stops after [q] with items still queued, a confirmed bulk [S]kip that prompts once, and applyAction's contract (only quit stops; [o] queues one op per agent+path, deduplicated on a second [o]). reconcile_test.go adds two end-to-end pins. TestReconcile_FinishRunsExactlyOnce is the behavioural half of "finish has one call site and is never deferred": a finish that ran twice prints its summary line twice. Its third subtest, [o] then [q], also pins that quitting still applies the queued override — neither quit was covered by any test or scripted scenario before. TestReconcile_ProjectScope_OverrideRecordsProjectState pins the scope and project root finish hands to render.NewWriter/RecordOpsState: at user scope a transposition to ScopeUser/"" is invisible; at project scope it backs the project's own .mcp.json up as a foreign collision. Break-verified: each mutation above fails exactly the test written for it. The stateDirty reset is the one exception — dropping it causes a second, byte-identical state.Save through the same atomic write, which no observable distinguishes, so no test is written for it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M4VNyoCuGXx7pYxNfLVbFG
printItemDiff's body was exactly renderItemValues(w, p, it, redact, canMask) — one caller (the [d]iff arm of the item prompt), no test callers, no doc mentions. The [d] arm now calls renderItemValues directly, the same call the prompt's initial render already makes two lines above it. The `unused` linter is not enabled, so this is a deliberate cleanup rather than a lint fix; no behaviour changes and the fixture stays byte-identical. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M4VNyoCuGXx7pYxNfLVbFG
canonicalHookEvent built its own adapter registry — registryFactory(), all 31 adapters, ~10µs — every time a hook pointer's native event spelling was inverted (reconcile's write-back through itemSourceFile → pointerSourceFile, and explain's componentFromPointer / pointerSource). The caller already holds the registry the plan was rendered with, so it is threaded through: pointerSourceFile and canonicalHookEvent take a leading *adapter.Registry, explainInputs gains a reg field that explain.go fills from its own registry, and the session passes s.reg. The answer cannot change: registryFactory() is a package var only tests reassign, never mid-run, so the session's registry has exactly the contents the per-call rebuild had. What was implicit is now written down and pinned: a non-renaming adapter (claude, codex) and an UNREGISTERED agent take the same passthrough, because Registry.Lookup yields a nil adapter.Adapter and a comma-ok assertion on a nil interface is (nil, false) rather than a panic. A nil *Registry — which Lookup would dereference — is guarded explicitly and takes that same passthrough; the two test literals that build an explainInputs without one now set it as well. TestCanonicalHookEvent pins the passthrough rows, the renaming rows (gemini BeforeTool / cursor preToolUse → PreToolUse), the unknown native spelling, and the nil registry; TestCanonicalHookEvent_UnregisteredLookupIsNil pins the Lookup contract the passthrough rests on. registryFactory() call sites in reconcile.go: 2 → 1. The 39-scenario fixture is still byte-identical to the 309fde0 binary. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M4VNyoCuGXx7pYxNfLVbFG
Round 1 (four lenses on 18c4f42) — every finding accepted after audit: - newReconcileSession enforces reconcileAuto's "at most one mode" as its first statement, before anything is loaded; reconcileRun no longer checks it in the caller. Same error text, same timing (harness s27 identical). - canonicalHookEvent's nil-registry guard resolves nothing (ok=false) instead of passing the native segment through: a caller that forgot its registry gets "no source", not a plausible wrong path. - Doc corrections: seven loose locals, not six (reconcile.go, CHANGELOG, components.md); removeDroppedSource's gate is a chosen write-back — interactive [w] or --auto-writeback — not a keystroke; the session test helper names the two arms that would panic on the zero session; the entry point is 22 lines now. - Tests: TestNewReconcileSession_RejectsMultipleAutoModes; the [d]iff arm, the declined bulk confirm (no menu re-print), an unknown key (capital I), a bulk choice that must not sweep an orphan, and the [i]gnore arm are pinned at the session level; the override dedup key is pinned as agent AND path; the nil-registry row flips to "resolves nothing". Byte-identity re-measured: all 39 harness scenarios, base (309fde0) vs this tree, diff -r empty over 195 files. Eight mutations each fail exactly their target test. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M4VNyoCuGXx7pYxNfLVbFG
…ons (#232) "No session with two modes can exist" overclaimed: a struct literal — which the session tests build — bypasses newReconcileSession's check. Say exactly what the constructor guarantees. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M4VNyoCuGXx7pYxNfLVbFG
…t a keystroke (#232) The secret-handling invariants called reconcile's dest-dropped MCP-server removal "keystroke-gated". It never was for --auto-writeback, which reaches removeDroppedSource with no keystroke, and a confirmed bulk [W] reaches it with no per-item one either. Name the three routes — a per-item [w], a confirmed bulk [W], or --auto-writeback — in the canonical project memory (.agentsync/memory/AGENTS.md), re-render CLAUDE.md and AGENTS.md from it with `agentsync apply --scope project`, and say the same in SECURITY.md, docs/architecture.md §5 and the reconcileSession doc. The withinDir bound is unchanged and still stated. CHANGELOG notes the wording fix under the #232 bullet. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M4VNyoCuGXx7pYxNfLVbFG
Round 2 (four lenses on 20bf626, after the memory/docs fix in eabe6d8): - The per-item menu is one production const, itemMenu, printed at both promptItem sites; the session tests keep a deliberate copy (wantItemMenu) so a wording change still fails a test. - TestReconcile_OrphanFile/"remove backs up and deletes" now asserts the state entry for the removed orphan is gone after the run: dropping the prune or its stateDirty flag failed zero tests before this. - TestNewReconcileSession_ChecksModesBeforeLoading is a source-text guard (funcBody, as the apply pipeline's) that the --auto-* check precedes every load in the constructor; moving it below loadProjectedForScope left the rejection test passing. - TestNewReconcileSession_RejectsMultipleAutoModes points both home lookups at an empty temp dir, so a regressed constructor loads nothing real. - The override dedup pin (agent AND path) is its own test, TestApplyAction_OverrideDedupsByAgentAndPath; the table test is back to its one-line queue check. - The session test file's header no longer claims no test touches the filesystem, and newTestSession's doc is the rule plus the two arms that panic on a zero session, not an inventory of actions. Byte-identity re-measured: 39 harness scenarios base (309fde0) vs this tree, diff -r empty (195 files). Six mutations each fail exactly their target test. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M4VNyoCuGXx7pYxNfLVbFG
Round 3 (four lenses on 58201c5): correctness and API design CLEAN, adversarial and test rigor ship-it. What changed: - TestReconcile_DroppedServer_WriteBackRemovesSource pins the deletion-only exception to the capture funnel on each of the three routes the secret-handling docs name for it — per-item [w], confirmed bulk [W], --auto-writeback — asserting the dropped server's canonical mcp/<id>.toml is unlinked and the drifted sibling is written back. removeDroppedSource had no in-repo coverage before; only the out-of-tree harness reached it. - TestReconcile_OrphanFile now applies two skills so the removed orphan's state prune is asserted as exact: the in-sync sibling's entry must survive. A wipe of state.Files passed the whole package before. - pkgSource(t, name) is the one way the two source-text guards read a production file; the runtime.Caller preamble no longer lives in two files. - The mode-rejection test's doc points at the load-order guard for the "before anything loads" half; itemMenu's doc says it is menu and prompt. - Docs: the project memory states removeDroppedSource's gate once (its CLAUDE.md/AGENTS.md renders regenerated with agentsync apply --scope project), SECURITY.md is re-wrapped, the CHANGELOG clause is one sentence. Production code is unchanged this round (one doc comment), so the byte-identity harness was not re-run. Four mutations each fail exactly their target test, including the --auto-writeback and bulk-[W] route pins. The adversarial lens's pre-existing finding — a confirmed bulk [W] writes back ForeignCollision items --auto-writeback refuses — is filed as #255. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M4VNyoCuGXx7pYxNfLVbFG
Round 4 (four lenses on 4f1f492): three ship-it, API design with two findings both introduced by round 3's helper placement. What changed: - The source-text guards read production files through the package's existing readFileForGuard(t, repoRootFromCaller(t), rel) convention, the one seven other guard files already use; the round-3 pkgSource helper is gone, and funcBody has its doc comment back (pkgSource had been inserted between the comment and the function). - The three-route dropped-server test pins the --auto-writeback route by what only that route can show: no prompt marker at all. Its stdin now carries a [q] that must never be read, so a flag that regressed into the interactive pass quits at the first prompt and fails the assertions instead of blocking on a terminal's stdin. The old "write-back: " pin was a substring of the shared assertion and could not fail on its own. - The orphan test's in-sync sibling skill is named insync, not keep, so it no longer reads as the [k]eep subtest's subject. - The two remaining comments that framed removeDroppedSource's gate as a per-item [w] keystroke name the three routes like every other copy. Production change is comments only; four lenses re-verified byte-identity in round 4 (correctness independently ran 11 scenarios on the dropped- server fixture against the base binary). Two mutations each fail exactly their target test. Test rigor's pre-existing finding — the per-item diff labels source/dest but renders the sides the other way round — is filed as #256. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M4VNyoCuGXx7pYxNfLVbFG
Final round (four lenses on 75d0e1a): API design, correctness and test rigor CLEAN, adversarial ship-it with two comment findings. All three residuals are comment-only: - reconcileAction's doc counted the old `ch | 0x20` case fold at three sites; the base file had two. - The tombstone comment inside writeBackItem, where errDestDroppedServer is returned, was the last place that framed the deletion's gate as a per-item [w]; it names the three routes like every other copy. - writeBackItem's doc paragraph had been sitting on top of attemptWriteBack's doc block (pre-existing); it is above writeBackItem. No production code changed. The loop ends at its five-round budget with every finding from every round fixed; the pre-existing findings it surfaced are #254, #255 and #256. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M4VNyoCuGXx7pYxNfLVbFG
This was referenced Sep 9, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #232. Sixth PR in the #226 code-quality series (after #240, #244, #247, #249, #252). A behaviour-preserving restructure of
reconcile's interactive pass, plus the two per-item rebuilds the issue asked about. No user-visible change, and that claim is measured rather than argued (below).1.
reconcileRunbecomes areconcileSession(commit 1,34bbf3c,internal/cli/reconcile.go+ docs). The old body was 375 lines with fivegoto dones, two labeled loops and seven pieces of run-scoped state travelling as loose locals. It is now a 22-line entry point over areconcileSessionstruct: the wiring the run was built with (printer, buffered input, registry, loaded state, scope, redaction map, the hoisted canonical hook-event vocabulary) and the run-scoped bookkeeping (queued overrides + dedup map, the confirmed bulk choice,stateDirty,autoSkipped,writeBackFailed, the per-source write ledger). Each phase is a method a test can drive directly:promptOrphan,resolveAuto,promptItem,applyAction,finish;attemptWriteBack,removeDroppedSourceanditemSourceFilebecome methods too. Everygoto doneis a plain return out ofwalk, which returns no error, andfinishhas exactly one call site. It is deliberately not deferred: it writes (the override re-apply, the state save) and returns the run's error, and adeferwould run those writes while a panic unwound.The bulk-action byte (
'w'/'o'/'s'/'i'/'q'withch | 0x20open-coded at two sites) is a typedreconcileActionwhose zero value is invalid (theadapter.SkipKindconvention, notadapter.Action's).parseItemKeyis now the one place a keystroke becomes an action, and it writes down the asymmetry the oldcase 'w', 'W', 'o', 'O', 's', 'S', 'i', 'q', 'Q'switch encoded by omission:Iis not a bulk ignore andDis not a diff; folding uniformly would have silently added an unconfirmed bulk-ignore keystroke.bulkTargetsis the blast-radius count as a function; the per-item menu and its prompt are oneitemMenuconst printed at both prompt sites. The at-most-one--auto-*check is the constructor's first statement, so the invariantreconcileAuto's doc states is enforced where a session is built, before anything loads.The dest→source surface is unchanged.
[w]rite-backstill runs throughwriteBackItem→capture.Capture; the deletion-only exceptionremoveDroppedSourcekeeps its gate (it runs only for a chosen write-back, a per-item[w], a confirmed bulk[W]or--auto-writeback, whose destination dropped the server) and itswithinDirbound. The secret-invariants docs used to call that gate "keystroke-gated", which--auto-writebacknever was; the canonical project memory (.agentsync/memory/AGENTS.md), its rendersCLAUDE.md/AGENTS.md(re-rendered withagentsync apply --scope project),SECURITY.md,docs/architecture.md§5 and every in-code comment now name the three routes (eabe6d8,75d0e1a,93ff469), and each route is pinned by a test (round 3).Why one file. The session stays in
reconcile.gorather than a newreconcile_session.go. A split would not move any fenced site (bothiox.AtomicWritecalls stay put, and the threeos.Removesites carry line-scoped//nolint), but it would falsify the prose of.golangci.yml's fence entry, which names "reconcile.go's interactive orphan removal". A behaviour-preserving refactor does not touch.golangci.yml, not even its prose.2. Tests (commit 2,
7eea03b, extended in review rounds 1–4). Seventeen session-level unit tests (reconcile_session_internal_test.go) drive the methods on a scriptedbufio.Reader:TestParseItemKey(15 rows, including the two non-folded capitals),TestBulkTargets, the four exits (TestPromptItem_EOFStopsThePass,TestPromptItem_EOFMidBulkConfirmStopsThePasswhose transcript must end at[y/N],TestPromptOrphan_EOFStopsThePass,TestPromptOrphan_QuitStopsThePass),TestWalk_QuitLeavesRemainingItemsUnprompted,TestWalk_ConfirmedBulkSkipsLaterPrompts,TestApplyAction_OnlyQuitStopsThePass, and from the review rounds: the[d]iffarm, the declined bulk confirm (no menu re-print), an unknown key, a bulk choice that must not sweep an orphan, the[i]gnorearm, the override dedup key (agent and path), the constructor's--auto-*rejection (against an empty temp home), and a source-text guard that the check precedes every load (reading source through the package'sreadFileForGuard/funcBodyguard convention). End-to-end inreconcile_test.go:TestReconcile_FinishRunsExactlyOnce(three subtests: the auto-safe summary prints once; EOF after an[o]applies the queue once;[q]after an[o]still applies the queue once),TestReconcile_ProjectScope_OverrideRecordsProjectState(a project-scope[o]records project state and takes no backup),TestReconcile_OrphanFile's remove subtest asserts the removed orphan's state entry is pruned and an in-sync sibling's survives (a wipe of state passed the whole package before), andTestReconcile_DroppedServer_WriteBackRemovesSourcepinsremoveDroppedSourceon each of its three routes (per-item[w], confirmed bulk[W], and--auto-writeback, the last pinned by the absence of any prompt with a never-read[q]on stdin): the dropped server'smcp/<id>.tomlis unlinked and the drifted sibling written back. That function had no in-repo coverage before.3.
printItemDiffdropped (commit 3,fab41c2): it was a pure alias ofrenderItemValues; the[d]iffarm calls the real thing.4.
canonicalHookEventuses the caller's registry (commit 4,18c4f42). Inverting a hook pointer's native event spelling rebuilt all 31 adapters viaregistryFactory()on every resolution (~10 µs measured). It now takes the registry the caller already holds: the session's inreconcile, andexplainInputs.regthreaded throughexplain.go/explain_model.goforexplain. The comma-ok assertion already meant it could not panic on a non-renaming adapter (the issue's question); areg == nilguard exists becauseRegistry.Lookupdereferences its receiver, and it resolves nothing (ok false) rather than passing the native spelling through, so a caller that forgot its registry gets "no source" instead of a plausible wrong path.registryFactory()call sites in the file: 2 → 1.Review provenance. The plan and execution spec were reviewed by a fresh reviewer who replicated all four commits and the fixture from the artifacts and found five issues before execution: quit-with-a-queued-override was unpinned (added as two scenarios and a subtest),
finish's hoisted scope/projectRoot was unpinned (added as a project-scope scenario and test), the CHANGELOG compared against the wrong binary, the one-file reason was wrong (corrected above), and commit 4's registry could be nil from two test literals (guard + row). All fixed before execution.Review loop (five rounds, the full budget; every finding from every round fixed). Round 1 (four lenses on
18c4f42; closed by97682da+20bf626): the constructor now enforces the at-most-one--auto-*invariant its own doc states; the nil-registry guard resolves nothing rather than passing through; "six loose locals" was seven; six session-level tests pin the arms that were covered only by the uncommitted harness. Round 2 (on20bf626; closed byeabe6d8+58201c5): the docs' "keystroke-gated" claim corrected at its canonical source and every copy (a confirmed bulk[W]was a third route nobody had named); the productionitemMenuconst with the tests keeping a deliberate copy; the orphan-remove state-prune assertion; the load-order source-text guard and a hermetic constructor test; the dedup pin in its own test. Round 3 (on58201c5; correctness and API design CLEAN, adversarial and test rigor ship-it; closed by4f1f492): the three-route test forremoveDroppedSource, the exact-prune sibling assertion, prose tightening; the adversarial lens's pre-existing finding, that a confirmed bulk[W]writes back ForeignCollision items--auto-writebackrefuses, is filed as #255. Round 4 (on4f1f492; three lenses ship-it; closed by75d0e1a): the guards read source through the package's existingreadFileForGuard/repoRootFromCallerconvention instead of a round-3 helper that had also displacedfuncBody's doc; the--auto-writebackroute is pinned by the absence of any prompt, with a never-read[q]on stdin so a regressed flag fails instead of blocking on a terminal; test rigor's pre-existing finding, that the per-item diff labels--- source / +++ destbut renders the sides the other way round, is filed as #256. Round 5 (on75d0e1a; API design, correctness and test rigor CLEAN, adversarial ship-it; closed by93ff469): three comment-only residuals (a miscount of the old case-fold sites, the last per-item-keystroke framing of the deletion gate, and a doc paragraph that had sat on the wrong function). Byte-identity re-measured after every round that touched production code: 39 scenarios,diff -rempty; round 4's correctness lens independently re-ran 11 dropped-server scenarios against the base binary, and round 5's proved the sorted string-literal sets of base and head differ only by the deduplicated menu. Declined with reason: pinning the orphan[r]backup/remove-failure arms andfinish's two error returns (each needs a failing backup dir or a failing adapter Apply); folding--auto-*into a single mode enum (it would only relocate the same check to the flag boundary; a follow-up candidate); turning the nil-registry guard into a panic (house style is a documented guard, and every production call site is verified non-nil). One reviewer's closing reflection, recorded rather than acted on: the load-order source-text guard is the one test they would cut as mild over-pinning; two other lenses had validated it as complementary to the behavioural test.Type of change
Test plan
Byte-identity oracle. Two binaries,
mainat309fde0and each of commit 1, the round-0 head, the round-1 tree and the round-2 tree (rounds 3–5 changed only comments in production), were run through 39 scripted-stdin scenarios covering every prompt, bulk confirmation (confirmed and cancelled), EOF at both prompts and mid-confirm,[q]uitat both prompts (including a quit with a queued override), every--auto-*mode and the mutually-exclusive error, a project-scope override, the write-back conflict and dropped-server paths, exit codes, and a masked secret value. Per scenario the harness captures stdout, stderr, exit status and the normalized source/state tree (195 files per arm). Determinism was proved first (base-vs-basediff -rempty), then each arm against base: all empty. Run from a working directory with no.agentsync/ancestor.Break-verifies (literal
--- FAILsets, each mutation asserted to land exactly once):bulkTargets→len(rest), foldingI/D, dropping the bulk latch, dropping thestateDirtyreset, callingfinishtwice) failed zero tests. That is the gap commit 2 closes.TestPromptItem_EOFStopsThePass; BV-2 →TestPromptItem_EOFMidBulkConfirmStopsThePass; BV-3 →TestApplyAction_OnlyQuitStopsThePass+TestWalk_QuitLeavesRemainingItemsUnprompted; BV-4 →TestPromptOrphan_EOFStopsThePass; BV-5 → threeTestBulkTargetsrows; BV-6 → the twoTestParseItemKeycapital rows; BV-7 →TestWalk_ConfirmedBulkSkipsLaterPrompts; BV-11 (finishtwice) →TestReconcile_FinishRunsExactlyOnceand all three subtests. Discarding the queue on[q]→ exactly thequit_after_overridesubtest. Hard-coding user scope infinish→ exactlyTestReconcile_ProjectScope_OverrideRecordsProjectState.s.stateDirty = falseafter the override save) fails 0 tests on both trees and the fixture is identical: it is an equivalent mutation (a second byte-identicalstate.Save). Reported, no test added.TestCanonicalHookEventrows andTestExplainPath_RenamedHookEventResolves; a single-value type assertion → theclaude_does_not_renamerow with themissing method NativeHookEventpanic the issue asked about; removing the nil guard → thenil_registryrow with a nil-pointer dereference insideRegistry.Lookup.--auto-*check back to the caller →TestNewReconcileSession_RejectsMultipleAutoModes+TestReconcile_AutoFlagsMutuallyExclusive; nil guard passing through → thenil_registry_resolves_nothingrow; deleting the[d]iffre-render, re-printing the menu after a declined confirm, echoing an unknown key, letting a bulk choice sweep an orphan, dropping theappendIgnorecall, keying the dedup on path only → each exactly its new test.stateDirty = true, or thepruneStateFilesForPathcall → exactlyTestReconcile_OrphanFile/remove_backs_up_and_deletes; moving the--auto-*check belowloadProjectedForScope→ exactlyTestNewReconcileSession_ChecksModesBeforeLoading; a column-0 brace inside the constructor → the guard's truncation check; changing the production menu wording → the four count/suffix tests, each printing the transcript; dedup on path only → exactlyTestApplyAction_OverrideDedupsByAgentAndPath.errDestDroppedServerbranch → all threeTestReconcile_DroppedServer_WriteBackRemovesSourcesubtests;--auto-writebackresolving to skip → exactly its--auto-writebacksubtest; not latching the confirmed bulk choice → exactly itsconfirmed_bulk_[W]subtest (+TestWalk_ConfirmedBulkSkipsLaterPrompts); replacing the orphan prune with a wipe ofstate.Files→ exactly the orphan remove subtest, on the sibling assertion.--auto-writebackfalling through to the interactive prompt → exactly its subtest (must not prompt); the ordering check moved below the loads → exactly the guard, through thereadFileForGuardread path.Accepted fixture coverage holes: no #247 refused-symlink/shape item (covered by
dest_fifo_e2e_unix_test.go); noQ/Ycapitals; no--auto-overridewith an orphan; no interactivewon a ForeignCollision.Post-conditions on
internal/cli/reconcile.go:goto5 → 0; labels 3 → 0;reconcileRun375 → 22 lines;registryFactory()2 → 1;capture.Capture(1 → 1;source.Write*(1 → 1;iox.AtomicWrite(2 → 2;os.Remove(3 → 3;//nolint:4 → 4;.Canonical()in non-testinternal/cli0 → 0;.golangci.ymlabsent from the diff.Gates, each commit independently and again on the head:
go build,go vet,gofmt -lempty, gofumpt +go mod tidyno rewrites,AGENTSYNC_TEST_IN_CONTAINER=1 go test ./...(29 packages ok),go test -race ./internal/cli/...,go test -tags=e2e ./test/e2e/...,go test -tags=bdd ./test/bdd/...,GOTOOLCHAIN=go1.26.2 golangci-lint@v2.12.2 run ./...→ 0 issues.just test-releaseis green (the release bar) —justis not installable in this session; every layer the recipe orchestrates (vet → build → race → e2e → bdd) was run directly as listed above, and CI's hermetictest-releasejob runs on the PR.just lintis clean — run directly with the pinned toolchain;go.mod/go.sumuntouched.Checklist
fix(secrets): …).internal/secrets,internal/capture, or anysource.Write*path, I've re-read the secret-handling invariants inCLAUDE.md/SECURITY.mdand not weakened them. — Neither package is touched.reconcile.go's onecapture.Capturecall and onesource.Write*call are unmoved (counts above);removeDroppedSourcekeeps its gate andwithinDirbound as a method, and each of its three routes is now pinned by a test; no.Canonical()unwrap is added; the redaction map reaches bothrenderItemValuessites. The invariants' own wording was made accurate (the gate is a chosen write-back, not a keystroke), not weakened.docs/components.md(areconcileSessionentry in theinternal/clikey list),CHANGELOG.md(Internal bullet under Changed, incl. the gate-wording note),.agentsync/memory/AGENTS.md+ renderedCLAUDE.md/AGENTS.md,SECURITY.md,docs/architecture.md§5. No CLI surface or capability change.🤖 Generated with Claude Code
https://claude.ai/code/session_01M4VNyoCuGXx7pYxNfLVbFG