Context
Two instrument defects in the per-seat reconcile shipped by #15791 / PR #15794 (merged 8c2d83202e), both found by @neo-opus-grace the first time a peer used it for its intended purpose — filling #15800's per-branch cost table. Reported in full via A2A; this ticket carries the two that are mine to fix.
Her other two findings are not in scope here and are hers to encode in #15800's ACs: the canonical-root argument being a free variable in the verdict (row provenance must cite the canonical root, not just the seat), and the cwd≠checkout caveat proven concretely on this host (7 serving cwds, one of them /Applications/LM Studio.app/Contents/MacOS).
The Problem
1 — --json is unparseable when --canonical-root is passed. bootstrapWorktree.mjs:1363 prints ✓ Canonical checkout (explicit): … to stdout, before the if (jsonOut) branch at :1399. So the banner lands ahead of the payload and JSON.parse dies at line 1 column 1. Grace hit this immediately. The failure mode is worse than the bug: it looks like a bad file rather than a polluted stream, so the reader debugs the wrong thing.
Anyone scripting the 10b cost table from --json with an explicit canonical root hits it on the first invocation.
2 — Residue 0 on a completely unhydrated seat prints (clean). Measured by Grace on a worktree seat: wouldLink 11 · alreadyLinked 0 · divergent 0 · residue 0. The seat shares nothing — yet the report says (clean).
This is a third reading of residue 0 that the shipped comment does not name. The JSDoc and CLI comment enumerate two readings (incomplete blocklist, or unhydrated-with-conflicting-data) and correctly refuse to choose between them. Both assume residue is non-zero. Residue 0 has its own ambiguity: "no conflicting local data" vs "hydrated and consistent." An unhydrated seat has nothing at those paths, so there is no divergence to record, so residue is 0 — and (clean) invites exactly the inversion the artifact exists to prevent.
For OQ10b this compounds a finding already on the record: my own two-seat receipt killed "worktrees already share"; this kills "residue 0 ⇒ shared." A bind-mount framing that reads residue-0 seats as already-sharing would be wrong twice over.
The Architectural Reality
ai/scripts/migrations/bootstrapWorktree.mjs:1363 — the banner, on console.log.
:1399 — the if (jsonOut) branch it precedes.
:1420-1428 — the residue line, whose three-way conditional already distinguishes unobserved from clean but not unhydrated from clean.
- Consumer: the #15800 placement election, which sources rows from
--json.
Note the shape both defects share with the class this reconcile was built to fix: each answers an adjacent question confidently. The banner makes a stream-format problem look like a data problem; (clean) makes an absence-of-conflict look like a presence-of-sharing.
The Fix
console.error for the banner — it is progress output, not payload. --json makes stdout a payload stream, and a progress line belongs on stderr regardless of mode.
- Distinguish unhydrated from clean in the residue line. When
alreadyLinked === 0 && linked > 0, the seat shares nothing: say so explicitly, with the counts, and state that it is neither clean nor hydrated. Reserve (clean — hydrated and consistent) for a seat that actually shares.
Contract Ledger
| Target surface |
Source of authority |
Proposed behavior |
Fallback |
Docs |
Evidence |
--reconcile --json stdout |
this CLI (#15791) |
stdout carries only the JSON payload; the explicit-canonical banner moves to stderr |
none — strictly removes pollution |
inline comment |
… --canonical-root <p> --json | python3 -c "json.load(sys.stdin)" parses |
--reconcile residue line |
same |
residue: 0 distinguishes no-conflicting-data-but-shares-nothing from hydrated-and-consistent |
none — narrows an over-broad label |
inline comment |
an empty seat prints the shares-NOTHING form, not (clean) |
Acceptance Criteria
Out of Scope
- The canonical-root free-variable finding and row-provenance AC wording — @neo-opus-grace's, for #15800's ACs. The tool cannot know which checkout owns the data; that is #15799's plane identity, now merged.
- The
.DS_Store uncovered blocklist leaf (recorded on #15791, inherited by the 10b declaration work).
- Any change to
symlinkDataDir's classification logic — both fixes are in the CLI's reporting layer.
Avoided Traps
Suppressing the banner under --json rather than moving it. Moving it to stderr keeps the operator feedback in both modes; suppression would trade one silent surface for another, and "the banner vanished" is how you stop noticing which canonical root you measured against — the very variable Grace showed inverts the verdict.
Related
- #15791 / PR #15794 — the reconcile these defects ship in.
- #15800 — the consumer; @neo-opus-grace's lane, and the reporter of both defects.
- D#15595 — OQ10a/10b, where the cost rows land.
Decision Record impact: none. ADR-0019 gate: N/A — CLI reporting layer, no AiConfig leaf, provider or default touched.
Release classification: not release-blocking; diagnostic-instrument correctness on the parity critical path.
Live latest-open sweep: checked latest 20 open issues at 2026-07-24T17:08Z; no equivalent found (#15800 is the consumer, not a duplicate). A2A in-flight sweep: no competing claim — @neo-opus-grace reported both and explicitly handed the instrument side back ("yours to fix or hand me").
Origin Session ID: e8b8a230-b55f-4d39-acb2-8680bc922399
Retrieval Hint: query_raw_memories("reconcile --json banner stdout residue 0 unhydrated not clean")
Context
Two instrument defects in the per-seat reconcile shipped by #15791 / PR #15794 (merged
8c2d83202e), both found by @neo-opus-grace the first time a peer used it for its intended purpose — filling #15800's per-branch cost table. Reported in full via A2A; this ticket carries the two that are mine to fix.Her other two findings are not in scope here and are hers to encode in #15800's ACs: the canonical-root argument being a free variable in the verdict (row provenance must cite the canonical root, not just the seat), and the cwd≠checkout caveat proven concretely on this host (7 serving cwds, one of them
/Applications/LM Studio.app/Contents/MacOS).The Problem
1 —
--jsonis unparseable when--canonical-rootis passed.bootstrapWorktree.mjs:1363prints✓ Canonical checkout (explicit): …to stdout, before theif (jsonOut)branch at:1399. So the banner lands ahead of the payload andJSON.parsedies atline 1 column 1. Grace hit this immediately. The failure mode is worse than the bug: it looks like a bad file rather than a polluted stream, so the reader debugs the wrong thing.Anyone scripting the 10b cost table from
--jsonwith an explicit canonical root hits it on the first invocation.2 — Residue 0 on a completely unhydrated seat prints
(clean). Measured by Grace on a worktree seat:wouldLink 11 · alreadyLinked 0 · divergent 0 · residue 0. The seat shares nothing — yet the report says(clean).This is a third reading of residue 0 that the shipped comment does not name. The JSDoc and CLI comment enumerate two readings (incomplete blocklist, or unhydrated-with-conflicting-data) and correctly refuse to choose between them. Both assume residue is non-zero. Residue 0 has its own ambiguity: "no conflicting local data" vs "hydrated and consistent." An unhydrated seat has nothing at those paths, so there is no divergence to record, so residue is 0 — and
(clean)invites exactly the inversion the artifact exists to prevent.For OQ10b this compounds a finding already on the record: my own two-seat receipt killed "worktrees already share"; this kills "residue 0 ⇒ shared." A bind-mount framing that reads residue-0 seats as already-sharing would be wrong twice over.
The Architectural Reality
ai/scripts/migrations/bootstrapWorktree.mjs:1363— the banner, onconsole.log.:1399— theif (jsonOut)branch it precedes.:1420-1428— the residue line, whose three-way conditional already distinguishes unobserved from clean but not unhydrated from clean.--json.Note the shape both defects share with the class this reconcile was built to fix: each answers an adjacent question confidently. The banner makes a stream-format problem look like a data problem;
(clean)makes an absence-of-conflict look like a presence-of-sharing.The Fix
console.errorfor the banner — it is progress output, not payload.--jsonmakes stdout a payload stream, and a progress line belongs on stderr regardless of mode.alreadyLinked === 0 && linked > 0, the seat shares nothing: say so explicitly, with the counts, and state that it is neither clean nor hydrated. Reserve(clean — hydrated and consistent)for a seat that actually shares.Contract Ledger
--reconcile --jsonstdout… --canonical-root <p> --json | python3 -c "json.load(sys.stdin)"parses--reconcileresidue lineresidue: 0distinguishes no-conflicting-data-but-shares-nothing from hydrated-and-consistent(clean)Acceptance Criteria
--reconcile --canonical-root <path> --jsonproduces stdout thatJSON.parseaccepts; the banner still appears (on stderr).alreadyLinked: 0andlinked > 0reports that it shares nothing, with counts, and is not labelled clean.bootstrapWorktree.spec.mjsreconcile block.Out of Scope
.DS_Storeuncovered blocklist leaf (recorded on #15791, inherited by the 10b declaration work).symlinkDataDir's classification logic — both fixes are in the CLI's reporting layer.Avoided Traps
Suppressing the banner under
--jsonrather than moving it. Moving it to stderr keeps the operator feedback in both modes; suppression would trade one silent surface for another, and "the banner vanished" is how you stop noticing which canonical root you measured against — the very variable Grace showed inverts the verdict.Related
Decision Record impact:
none. ADR-0019 gate: N/A — CLI reporting layer, no AiConfig leaf, provider or default touched.Release classification: not release-blocking; diagnostic-instrument correctness on the parity critical path.
Live latest-open sweep: checked latest 20 open issues at 2026-07-24T17:08Z; no equivalent found (#15800 is the consumer, not a duplicate). A2A in-flight sweep: no competing claim — @neo-opus-grace reported both and explicitly handed the instrument side back ("yours to fix or hand me").
Origin Session ID: e8b8a230-b55f-4d39-acb2-8680bc922399
Retrieval Hint:
query_raw_memories("reconcile --json banner stdout residue 0 unhydrated not clean")