Fix #1445: Wizard sync-now step trusts exit code 3 as proof of no-destinations with no corroborating signal - #1448
Conversation
…ld as a user decline Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…xit code outrank a fail-open marker read Two defects found reviewing the fix for #1437. `hyp sync --dry-run` on a held machine with no instantiated sinks returned the new exit 3 with an error on stderr, where it previously exited 0. The new branch sits ahead of every dry-run path in `runSync`, so it caught a run that never offered to send anything. That contradicts the exemption stated 40 lines below it ("--dry-run is exempt: it sends nothing") and breaks a scripted inspection run after an attended enroll. The branch is now gated on `!dryRun`, so a dry run keeps the exit code it has with no window open. The wizard checked the child's exit code only inside `if (stillHeld !== null)`. `readFirstSyncDeadline` fails open by design (LLP 0101): a corrupt, unreadable, or lapsed marker reads as absent. A child that exited SYNC_HELD_NO_DESTINATIONS_EXIT, and so provably sent nothing (the code is returned before any export), would then have been reported as `{ released: true }` and `sync_now: released` - the false "your history is on its way" claim the step exists to prevent, and the opposite skew on the metric the PR is protecting. The code is now read before the marker. LLP 0203 #read-back's new paragraph and docs/CLI_REFERENCE.md updated to match. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…p's no-destinations line - `hyp sync --history <client>` on a held machine with no destinations now exits 0 again rather than 3. A replay can never end the window (`runHistorySync` already refuses with 2 while the hold is live), so the new code has no early release to be silent about, and its advice named a command the caller did not run. - The wizard's no-destinations line kept the deadline instead of dropping it. The hold marker is untouched on that path and still lapses on schedule, and the driver gates on the marker alone, so a destination that appears before the deadline by a route the user did not drive forwards this history with no `hyp sync` from anyone. The line now states the deadline conditionally rather than omitting it as inapplicable, and no longer repeats the child's own "run `hyp sync` again" verbatim. - `SYNC_HELD_NO_DESTINATIONS_EXIT`'s rationale said "1 is an export that ran and failed"; `runSync` also returns 1 for a marker that would not clear and for a hold that reappeared mid-run. Corrected. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
# Conflicts: # src/core/cli/wizard/types.d.ts
…tice beside it Exit 3 is not exclusively HypAware's: Node returns it on an internal parse error, and nothing stops a later `runSync` path from picking it. The wizard's send-now step took it as proof and printed the no-destinations explanation with no corroborating signal. `hyp sync` already prints a distinctive notice on exactly that branch, so the first line of it moves next to the exit code as a shared constant and the child's stderr is piped (and echoed straight back out) so setup can read it. Both halves are now required; an exit 3 with no notice falls back to the marker-driven outcome. The corroboration is deliberately not a second marker read, which fails open and would reintroduce a claimed release. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…not end the run Three defects in the corroborating-notice read, all found reviewing #1448. - The match compared against text the child may have styled. `paintLine` paints the `hyp sync:` prefix of this exact line, dropping a reset inside the sentence, so a coloured notice no longer contains the constant it was built from. Colour is TTY-gated and this child's stderr is a pipe, so it is inert today - but the corroboration exists precisely because a signal that can silently be wrong is not evidence, and this one silently depended on `useColor` staying `isTTY`-only. Matched through `stripSgr` (lifted out of `visibleWidth`, which already carried the regex) with the carried tail widened to cover a notice that arrives styled. - `child.stderr` was piped with no `error` listener. An emitter `error` no `try`/`catch` can contain would end a setup whose every act had already succeeded, which is the failure `installStreamErrorHandlers` exists for on the write side. - `WizardSyncNowResult`'s doc still said `no-destinations` is told apart by the exit code alone. Tests: the echo is asserted (nothing verified that the piped stderr still reaches the terminal), a chunk-split painted notice is still read, and a failing read pipe leaves the outcome intact. The stderr stub is now a real EventEmitter so an unlistened `error` throws the way the pipe does. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Neutral review - PR #1448 @
|
master's #1434 deleted the wizard's own send-now select and gave every non-zero child exit the `child-failed` outcome. This branch's `no-destinations` arm keeps its place ahead of it: exit 3 read with the notice the child prints beside it is not a run that broke, and its closing statement says so rather than restating the deadline as if a destination existed. An exit 3 with no notice now falls through to `child-failed`, which is the fix #1445 asked for - it is not explained as a machine with nowhere to send. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The piped stderr carries the child's confirm prompt, not only its diagnostics: askYesNo builds its readline over ctx.stderr, so the echo loop is the prompt's only path to the terminal. Record that, and the two measured consequences (readline builds with terminal: false, and the newline-less question leaves the parent colorizer mid-line), so a later narrowing of this pipe cannot take the prompt out with it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review round: head
|
Neutral triage - PR #1448 @
|
Ship risk:
|
Feature or issue
The wizard's send-now step read the spawned
hyp syncchild's exit code and, on3, unconditionally printed the "no destination is configured" closing statement and recordedsync_now: no-destinations, with nothing else corroborating it. Exit 3 is not exclusively HypAware's: Node itself returns 3 on an internal JS parse error, before a line of sync code has run, and nothing structurally stops a futurerunSyncpath from returning 3 for another reason. In either collision setup states a confident false explanation on its last screen and writes a wrong outcome into the LLP 0203 telemetry.Solution
hyp syncalready writes on exactly that branch. Its first line moves next to the exit code asSYNC_HELD_NO_DESTINATIONS_NOTICEinsrc/core/usage-policy/first_sync_hold.js, so the command that writes it and the wizard that matches on it cannot drift apart. No new config key or schema field.runSyncChildnow spawns with['inherit', 'inherit', 'pipe']: stdin and stdout stay on the terminal the child prompts on, and its stderr is echoed straight back out while being scanned for the notice (only the notice's own length is retained, so a loud failure cannot buffer unboundedly). Theno-destinationsbranch requires both halves; an exit 3 with no notice falls through to the marker-driven outcome. The corroboration is deliberately not a second marker read, which fails open and would reintroduce the claimed-release polarity LLP 0203#read-backsettles against.an exit 3 the child never explained falls back to the marker, not to no-destinations, which reportedno-destinationsbefore the fix and now reportssync-declined; the existinga no-destinations child is not a release even when the marker reads absentstill asserts the same outcome, with its fake child now emitting what the real one emits. Full suite 6135 pass / 0 fail,npm run typecheckandnpm run build:typesclean.This branch is stacked on #1441 (
fix/issue-1437), which introduced the code under repair and is not yet merged; the diff narrows to this fix once that lands. Own delta against #1441's head: +55 / -9 lines.Code: +160 / -14 lines
Fixes #1445