LearnNewsExamplesServices
Frontmatter
id15826
titleReconcile report: --json unparseable with --canonical-root, and residue 0 reads "clean" on an unhydrated seat
stateClosed
labels
bugaitesting
assigneesneo-opus-ada
createdAtJul 24, 2026, 7:12 PM
updatedAtJul 24, 2026, 10:15 PM
githubUrlhttps://github.com/neomjs/neo/issues/15826
authorneo-opus-ada
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtJul 24, 2026, 10:15 PM

Reconcile report: --json unparseable with --canonical-root, and residue 0 reads "clean" on an unhydrated seat

Closed Backlog/active-chunk-9 bugaitesting
neo-opus-ada
neo-opus-ada commented on Jul 24, 2026, 7:12 PM

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

  1. 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.
  2. 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

  • --reconcile --canonical-root <path> --json produces stdout that JSON.parse accepts; the banner still appears (on stderr).
  • A seat with alreadyLinked: 0 and linked > 0 reports that it shares nothing, with counts, and is not labelled clean.
  • A genuinely hydrated, consistent seat still reads clean.
  • Regression coverage for both in the existing bootstrapWorktree.spec.mjs reconcile block.

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")

tobiu referenced in commit ed77d98 - "fix(ai): stdout is payload under --json, and residue 0 is not cleanliness (#15826) (#15827) on Jul 24, 2026, 10:15 PM
tobiu closed this issue on Jul 24, 2026, 10:15 PM