Skip to content

The stranded-attach warning repeats where the run ends (#614) - #625

Open
philcunliffe wants to merge 7 commits into
masterfrom
fix/issue-614
Open

The stranded-attach warning repeats where the run ends (#614)#625
philcunliffe wants to merge 7 commits into
masterfrom
fix/issue-614

Conversation

@philcunliffe

Copy link
Copy Markdown
Contributor

Base note: this is stacked on #608, not on master

Issue #614 is a deferred review finding about PR #608, which is still open
and unmerged. Every symbol the finding names (FinaleSummary.attachedNotConfigured,
findAttachedNotConfiguredClients, the finale warning, LLP 0185) exists only on
fix/issue-604. Basing this on master would have meant re-carrying #608's three
commits under a second PR number, so it is stacked instead, matching the pattern
this repo already uses (#611-#621 on integration/reconciler-refused-marker).
Merge #608 first; this then rebases onto master cleanly. The diff below is just
this change.

Root cause

runPickerFinale prints the stranded-attach warning and returns. What happens
next depends entirely on which entry point called it, and nothing in the code
accounted for that:

  • runPickerWalkthrough writes its short run summary and stops. The warning is
    within a handful of lines of the end.
  • runInitWizard writes the run summary, then runWizardFirstLook (roughly
    sixty lines of real query output, by its own design in LLP 0135 #first-look),
    then on the team path the privacy narration. Nothing pauses between them, so
    on a real terminal the warning has scrolled away by the time hyp init
    finishes, and a user who does not scroll back never reads it.

writeWalkthroughRunSummary never read finaleSummary.attachedNotConfigured,
which is what the issue reports, but that is the symptom rather than the cause:
the cause is that the finale's placement is correct for the daemon restart and
wrong for the end of the run.

It is buried worst exactly where it matters most. On a managed host hyp status's mirror diagnostic (client_attached_not_configured) is gated to hosts
with no central layer (LLP 0185 #status-backstop), so the finale's print is the
only signal a wizard-attached client the reconfigure run left unpicked ever
gets. A joined host has no backstop to scroll back to.

The fix

The finale's print stays exactly where LLP 0185 put it, after the config
write and before the daemon restart. runInitWizard additionally writes a short
repeat after the first look and before the privacy narration, naming the same
clients and the same hyp detach --client <name> lines.

  • writeAttachedNotConfiguredReminder (new export,
    src/core/cli/walkthrough.js) is the compact form: names plus one repair
    command each, with the same dry-run tag rule. The finale's own
    writeAttachedNotConfiguredWarning, which explains the consequence in full,
    stays private and unchanged.
  • src/core/cli/wizard/index.js calls it only when its closing sequence
    actually ran: the first look ran (attended, uncancelled, not a dry run) or the
    pathway is team. A scripted --yes / --dry-run wizard run writes nothing
    between the finale and its summary, so its output is unchanged.
  • It goes before narratePrivacyIfTeamPath, so the privacy narration is
    still the wizard's last words (LLP 0135, LLP 0100/0101). Seven lines of
    narration is a reminder that survives on one screen; sixty lines of query
    output is not, and that is the difference this turns on.

The double-print hazard, and how it is avoided

Round 2 of #608's review flagged that echoing attachedNotConfigured from
writeWalkthroughRunSummary would print the warning twice within a few lines of
itself on the runPickerWalkthrough path, where the summary follows the finale
with nothing in between. That function is shared by both entry points, so the
naive echo is wrong twice over: it double-prints on one path and still lands
before the first look on the other, which is the complaint it was meant to fix.

So writeWalkthroughRunSummary is untouched. The repeat belongs to the caller
that buried the first print
, because only the caller knows whether anything
came between. runPickerWalkthrough does not repeat; runInitWizard does, and
only when its own closing output ran. No path prints the warning twice on one
screen.

This is a print-contract change to an Accepted LLP, so it is recorded as an
extension rather than an edit: LLP 0188 (new, Accepted) with an
Extended-by: forward-ref and an inline note added to LLP 0185
§status-backstop. LLP 0185's decision itself is unchanged.

Ground truth

New regression test, test/core/cli/wizard/index.test.js:
runInitWizard: an attended run repeats the stranded-attach warning after the first look.

Pre-fix (git stash push -- src/, then
node --test test/core/cli/wizard/index.test.js):

ok 16 - runInitWizard: a non-interactive or dry run skips the first look
not ok 17 - runInitWizard: an attended run repeats the stranded-attach warning after the first look
ok 18 - runInitWizard: a scripted run does not repeat the stranded-attach warning
# tests 20
# pass 19
# fail 1

Post-fix, plus the double-print guard:

node --test test/core/cli/wizard/index.test.js \
             test/core/walkthrough-unpicked-attached-clients.test.js
# tests 25
# pass 25
# fail 0

Two more tests pin the hazard rather than the feature, and pass on both sides
because they assert what must not change:

  • runInitWizard: a scripted run does not repeat the stranded-attach warning
    (no closing sequence, no repeat).
  • The existing the finale names a still-attached client the new config no longer collects now counts occurrences: exactly one
    hyp detach --client codex and one Still attached, no longer collected on
    the runPickerWalkthrough path. That assertion fails under the naive
    shared-summary echo.

Full suite (npm test): # tests 3389 / # pass 3388 / # fail 0 / # skipped 1.
npm run typecheck: clean.

Fixes #614

test and others added 5 commits August 4, 2026 21:13
Re-running `hyp init` and unchecking a previously attached client
regenerates a config without that client's adapter while the client's own
settings still route through the HypAware gateway. The finale only
attaches, and the action reconciler's reverse lane undoes only
config-named (org/central) keys, so a wizard attach on the local layer is
never reversed: capture stops and the client's requests can fail. Picking
only OpenTelemetry composes no gateway at all, and every still-attached
client points at a dead port after the finale's restart.

The finale now names the clients it left stranded (attach marker on disk,
not picked, enabled by neither the written config nor the central layer)
and prints the `hyp detach --client <name>` that clears each. It does not
detach: rewriting a client's settings file is not what unchecking a
picker row asked for, and that call is the maintainer's to make (LLP
0185). `hyp status` gains the matching `client_attached_not_configured`
diagnostic as the after-the-fact backstop, gated to solo hosts because on
a joined host that shape is a reconciler pass that has not run yet.

The org/central attach markers are untouched throughout.

Co-Authored-By: Claude <noreply@anthropic.com>
…604)

Three defects in the #604 fix, found in neutral review.

An unreadable local config emptied the active-plugin set for a reason that
has nothing to do with what the operator enabled, and the new
`client_attached_not_configured` branch read that as "nothing enables this
adapter": `hyp status` answered a parse failure with a `hyp detach` for
every attached client, stacked on top of the `config_unreadable` error that
is the actual repair. The branch now stands down when the local layer is
present but did not parse.

`findAttachedNotConfiguredClients` counted a plugin entry with
`enabled: false` as configured, while `hyp status` builds the same set from
`activePlugins`, which skips it. A switched-off adapter collects exactly as
little as an absent one, so the two surfaces disagreed about the same file.

LLP 0185 records both gates, and the forward-refs the corpus convention
wants land on the docs it extends: LLP 0180 (the finale's attach lane) and
LLP 0086 (the client drift diagnostic family). The status test's `@ref`
carried an empty gloss; it now says what it tests.

Tests: an unreadable local config draws no detach advice; a disabled plugin
entry strands its client; and the central-layer suppression LLP 0185 #scope
promises for the finale, which had no coverage on the walkthrough side.
Both new gates fail without the src change.

npm test: 3386 tests / 3385 pass / 0 fail / 1 skipped. npm run typecheck
clean. Smokes status_diagnostics and walkthrough_picker_to_first_query ok.

Co-Authored-By: Claude <noreply@anthropic.com>
…ad unfiltered (#604)

The status gate stands down on a local layer that is present but does not
parse, and deliberately does not stand down when there is no config at all:
nothing collects then, so a marker still on disk really is stranded. That
boundary (`errorKind !== 'config_missing'`, not `!ok`) had no test, so
widening the gate would have dropped the case silently. Adds one.

`readCentralPluginNames` is deliberately not filtered by `enabled`, unlike the
local set the caller builds: LLP 0185 #scope settles the org lane on the name
alone, and filtering would hand the operator a detach for an org-owned client.
Says so at the read, and scopes #not-configured-means-not-active to the local
layer so the two sections cannot be read against each other.

Co-Authored-By: Claude <noreply@anthropic.com>
The finale names the clients this run left attached but no longer collects,
before the daemon restart (LLP 0185). `runInitWizard` then writes the run
summary, a first look that is roughly sixty lines of real query output, and on
the team path the privacy narration, none of which pause, so on a real terminal
the warning is gone by the time setup ends. It is buried worst where it is the
only signal there is: on a managed host `hyp status`'s mirror diagnostic is
gated to hosts with no central layer.

The wizard now repeats it, short, after the first look and before the privacy
narration. The repeat is the caller's rather than the shared
`writeWalkthroughRunSummary`'s, because only the caller knows whether anything
came between: `runPickerWalkthrough` writes a short summary and stops, so it
keeps its single print instead of showing the same paragraph twice within a few
lines of itself.

LLP 0188 records the print contract and carries the forward-ref on LLP 0185.

Co-Authored-By: Claude <noreply@anthropic.com>
The gate was `firstLookRan || pathway === 'team'`. A `pathway` is only ever
resolved on an interactive run, so a team run that is neither cancelled nor a
dry run already has `firstLookRan`; the disjunct added exactly the runs where
the first look did *not* run. Those are the runs that wrote nothing between
the finale and here, so on a team run cancelled at the backfill consent the
repeat landed three lines under the finale's own full warning, which is the
same double print the shared run summary was rejected for.

The gate is now `firstLookRan` alone. LLP 0188 #when says so and says why,
and a new test pins the cancelled team run (fails on the old gate).

Co-Authored-By: Claude <noreply@anthropic.com>
@philcunliffe

Copy link
Copy Markdown
Contributor Author

Neutral review, round 1 of 2

Verdict: approve after the one fix below, which is pushed. Reviewed as
git diff origin/fix/issue-604...ce11b67 (the stacked base, not master). The
diagnosis is right, the placement is right, and the choice not to touch
writeWalkthroughRunSummary is right. One condition in the gate was wrong,
and it was wrong in exactly the direction the design doc rules out.

Finding 1 (medium, fixed): the || pathway === 'team' disjunct fires only where the repeat must not

src/core/cli/wizard/index.js:234 gated the repeat on
firstLookRan || pathway === 'team'. Expand it:

  • firstLookRan === interactive && !cancelled && opts.finale?.dryRun !== true
  • pathway is assigned only inside if (interactive) (index.js:71-131), so
    pathway !== undefined implies interactive === true.

Therefore pathway === 'team' adds a run to the gate only when
cancelled || dryRun, that is, only when the first look did not run. Those
are precisely the runs where nothing was written between the finale and the
repeat, so the disjunct did not widen the fix, it produced the double print
LLP 0188 says no path produces.

Reachable case: a team/managed run cancelled at the backfill consent prompt.
summary.cancelled is set inside runFinaleBackfill
(src/core/cli/walkthrough.js:1238), which runs before the finale computes
and prints attachedNotConfigured (walkthrough.js:1024-1032), so the finale's
full six-line warning is printed on a cancelled run too. The wizard then wrote
the run summary (3 lines) and repeated the same two lines under it. Probed
against the real orchestrator with a cancelled team finale, stdout was:

✓ Wrote /tmp/x/config.json
Still attached, no longer collected: codex
Their requests can start failing until you run:
  hyp detach --client codex

(the second block is the repeat; the finale's own print sits directly above it
in production, where the finale is not stubbed). The privacy narration does not
reliably follow either: narratePrivacyIfTeamPath returns early when there is
no first-sync deadline on disk (index.js:375), so the repeat can be the last
thing on screen, four lines under its own original.

The dryRun half is unreachable from the CLI today (--dry-run goes through
runPickerInit, which always sets picks, so interactive is false and no
pathway is resolved) but it fails the same way for any other caller.

Fixed in e21952a: the gate is firstLookRan alone, with the derivation in
a comment and a @ref LLP 0188#when [constrained-by]. LLP 0188 §when is
rewritten to state the actual condition and why the team disjunct is wrong
(that section is new in this PR, so nothing settled was edited). New test
runInitWizard: a run cancelled at the finale does not repeat the stranded-attach warning pins it; verified it fails on the old gate
(not ok 19) and passes on the new one.

Verified independently

  • Regression strength. Reverted the whole source hunk
    (git checkout origin/fix/issue-604 -- src/) and ran the two test files:
    not ok 17 - runInitWizard: an attended run repeats the stranded-attach warning after the first look, # tests 25 / # pass 24 / # fail 1. Exactly
    the claimed failure, and exactly one: the scripted-run test and the two new
    exactly-once counters in
    test/core/walkthrough-unpicked-attached-clients.test.js:96-98 do pass on
    both sides, so they pin the double-print hazard rather than the feature, as
    claimed.
  • Entry points and early exits. runPickerWalkthrough never reaches the
    new code. In runInitWizard, every exit before the finale (gate quit/status,
    fork quit, picked.cancelled) returns with finaleSummary undefined, so
    stranded is [] and no bare repeat is possible. opts.finale absent,
    same. After the fix, no path prints the warning twice.
  • The solo case. An attended, uncancelled, non-dry local run has
    firstLookRan === true, so it repeats: correct, the first look buried the
    original there too. A solo run that skipped the first look also skipped
    everything else, so the finale print is still on screen and no reminder is
    right.
  • Ordering (LLP 0135 / 0100-0101). The call sits after the first-look block
    and before narratePrivacyIfTeamPath (index.js:242-245); the narration is
    still the last words. LLP 0135 §first-look does say the ~60-line block runs
    and that the narration "is written after the block and stays the last thing
    on screen", so the cited claim is true.
  • Managed-host claim. True. src/core/daemon/status.js:650 gates
    client_attached_not_configured on !configured && probe.attached && !hasCentral && !localConfigUnreadable, annotated
    @ref LLP 0185#status-backstop. On a joined host there is no backstop, so
    the wizard's print is the only signal.
  • LLP 0188. Number 0188 is unclaimed elsewhere (0186/0187 are taken by
    integration/reconciler-refused-marker; nothing else holds 0188). All three
    relative links resolve, both anchors used by @refs
    (#repeat-at-the-end, #when) exist, Systems: matches 0185's exactly, and
    Status: Accepted matches the corpus pattern for a decision landing with its
    code. The Extended-by: addition to 0185 is a forward-ref plus an inline
    note in §status-backstop; it does not alter what 0185 settled, which
    CLAUDE.md explicitly permits.
  • Style. No em dashes, no semicolons, no @typedef, no inline
    import('...') types in the diff or in my fix.

Residual findings

  1. (nit, not fixed) writeAttachedNotConfiguredReminder's dryRun
    argument is now provably false at its only call site, since firstLookRan
    excludes dry runs. Left as the honest source of the value rather than a
    hardcoded false, so a future caller does not inherit a wrong tag. LLP 0188
    §shorter's "both prints share the dry-run tag rule" is therefore a rule with
    no reachable instance today.
  2. (nit, not fixed) LLP 0188 introduces a **Extends:** header field that
    no other doc in the corpus uses (0185 lists the docs it extends under
    Related:). Symmetric with the established Extended-by: and nothing
    parses these headers, so it is harmless, but it is a new convention landed
    without a note.
  3. (informational) The PR body's "The wizard ... calls it only when its
    closing sequence actually ran: the first look ran ... or the pathway is
    team
    " and the "Ground truth" counts now describe the pre-fix commit. The
    body was left untouched per the review protocol; the code and LLP 0188 are
    the current record.
  4. (informational) The feature test stubs finaleRunner, so the finale's
    own print never appears on the wizard path in tests. The exactly-once
    property is pinned only on the runPickerWalkthrough path
    (walkthrough-unpicked-attached-clients.test.js) and, on the wizard path,
    by the two negative tests. A test that drives the real finale end to end
    through the wizard would close that last gap; it is not cheap, and the
    negative tests cover the reachable regression.

Numbers

  • npm test at the reviewed head ce11b67: `# tests 3389 / # pass 3388 /

    fail 0 / # skipped 1` (matches the PR body).

  • npm test after the fix (e21952a): `# tests 3390 / # pass 3389 /

    fail 0 / # skipped 1`.

  • npm run typecheck: clean at both.

Pushed to fix/issue-614 as e21952a. Base branch fix/issue-604 untouched.

Base automatically changed from fix/issue-604 to master August 5, 2026 01:36
…mpting

`firstLookRan` says the closing block was attempted. It is documented to
degrade to a silent skip rather than fail a finished install (LLP 0135
#first-look), so an unregistered `ai_gateway_messages` dataset, an unreadable
cache, or a render that throws leaves an attended run that wrote nothing
between the finale's warning and the closing repeat. Probed against the real
orchestrator with no overview dataset: stdout was the config-write line and
the repeat, with the finale's own six-line warning directly above it in
production. That is the same-screen double print LLP 0188 says no path
produces, reached through the first look's designed degradation instead of
through the pathway disjunct round 1 removed.

Gate on the result: `runWizardFirstLook` already returns `shown`, which is
exactly "the block reached the screen". LLP 0188 §when restated to match (the
condition is the closing sequence having *written* something), and the
`**Extends:**` header folded into `Related:`, which is how the corpus's other
167 docs name what they extend.

Co-Authored-By: Claude <noreply@anthropic.com>
@philcunliffe

Copy link
Copy Markdown
Contributor Author

Neutral review, round 2 of 2

Verdict: approve after the one fix below, which is pushed (e1bfbc3).
Reviewed as git diff origin/fix/issue-604...e21952a (the stacked base, not
master). Round 1's fix is correct as far as it goes and its reasoning holds up
under re-derivation. It removed one route to the same-screen double print;
there is a second route, through the first look's designed degradation
rather than through the pathway disjunct, and it is closed here.

Finding 1 (low, fixed): firstLookRan means "attempted", and the repeat needs "printed"

src/core/cli/wizard/index.js:243 gated the repeat on firstLookRan, which is
interactive && !cancelled && opts.finale?.dryRun !== true: the conditions
under which the first look is called. But runWizardFirstLook
(src/core/cli/wizard/first_look.js:126-229) is documented to never fail a
finished install (@ref LLP 0135#first-look) and returns { shown: false }
having written nothing to stdout on two of its three skip reasons:

  • no-dataset (first_look.js:135): no runner, or the registry has no
    ai_gateway_messages. overviewRunnerFromCtx yields this whenever the boot
    context has no query capability or the gateway plugin is not active.
  • error (first_look.js:224): the whole block is inside one try precisely so
    that an unreadable cache, an unforeseen row shape, or a stream that throws on
    write ends quietly. The JSDoc names the unreadable cache explicitly.

(The third, slow, writes two lines, so it is the same hazard in weaker form.)

On any of those, an attended run has firstLookRan === true and has written
nothing at all between the finale's warning and the repeat. Probed against the
real orchestrator (real runInitWizard, real runWizardFirstLook, only the
finale and the pick stubbed, no overview dataset), stdout was:

✓ Wrote /tmp/x/config.json

Still attached, no longer collected: codex
Their requests can start failing until you run:
  hyp detach --client codex

In production the finale's own six-line warning sits directly above that
✓ Wrote line, so the user reads the same warning twice inside eight lines.
That is the outcome LLP 0188 §repeat-at-the-end asserts no path produces and
§shorter says "reads as a bug rather than a reminder", and it is the same
defect class round 1 fixed, reached by a different route.

Fixed in e1bfbc3: runWizardFirstLook already returns exactly the fact
the gate needs, so the wizard now captures it (firstLookShown = look.shown)
and gates on that. The failure direction is safe either way (an extra reminder
beats a lost one), which is why this is low and not medium, but the doc's claim
is now true rather than nearly true. LLP 0188 §when is restated to say the
condition is the closing sequence having written something, and why that is
narrower than having run it. New test
runInitWizard: an attended run whose first look skips itself does not repeat the stranded-attach warning; verified it fails on the round-1 gate
(not ok 20, # fail 1) and passes on the new one.

Finding 2 (nit, fixed): the **Extends:** header field

Round 1's carried item 2. Checked the corpus: across 167 docs the header field
histogram is Type 170, Status 170, Related 170, Systems 166, Date
162, Author 146, Generated-by 35, Extended-by 14, and Extends 1,
which is this PR. The established shape is asymmetric on purpose: the extending
doc names the extended one under Related: (LLP 0086 -> 0044, LLP 0105 ->
0070, LLP 0093 -> 0080 all do this), and the extended doc carries
Extended-by:. Nothing parses it either way (/llp-list reads only Status,
Systems, Type), so this was harmless, but a one-off field in a corpus this
size is a convention landed without a decision. Folded into Related: with the
Extended-by relationship named inline; LLP 0185's forward-ref is unchanged
and still carries the semantics.

Round 1's fix, re-derived independently

  • firstLookRan is genuinely right on the paths round 1 named. Confirmed
    summary.cancelled is set in runFinaleBackfill
    (src/core/cli/walkthrough.js:1238) and the attachedNotConfigured compute
    and print happen after it (walkthrough.js:1022-1031), so the finale's full
    warning is printed on a cancelled run, which is what makes the removed
    || pathway === 'team' disjunct a double print rather than a widening.
  • No path silently loses the reminder. Scripted --yes, --from-file,
    preset: interactive false, nothing written between the finale and the end,
    finale print is the last substantive output. --dry-run: same, and the
    config that strands them was not written. Cancelled: finale print, then
    hyp init: cancelled (stderr), a three-line summary, and at most the
    narration. Every exit before the finale returns with finaleSummary
    undefined, so stranded is []. opts.finale absent: same. After finding 1,
    the degraded first look joins that list, correctly.
  • Nothing widened by the new gate. shown: true includes the partial
    (deadline-expired) render, which is still tens of lines, so the ordinary
    reconfigure case this PR exists for is unaffected.
  • Exactly-once still holds on the picker path. 27/27 in
    test/core/cli/wizard/index.test.js +
    test/core/walkthrough-unpicked-attached-clients.test.js, including the two
    occurrence counters round 1 checked.

The other two carried items, determined

  • Item 1 (the dryRun argument is provably false): leave it. Still true
    after this round, and still the right call. writeAttachedNotConfiguredReminder
    is an export whose signature should describe the print, not the one call site
    that currently reaches it; hardcoding false would hand a future caller a
    silently wrong tag. LLP 0188 §shorter's shared-tag rule stays a rule with no
    reachable instance, which is a property of the call graph, not a defect.
  • Item 4 (the feature test stubs finaleRunner): not a real gap. The
    finale's own print lives in runPickerFinale, and exactly-once for it is
    pinned against the real function on the picker path
    (walkthrough-unpicked-attached-clients.test.js:92-98, which builds real
    descriptors, real attach markers and a real home). The wizard calls that same
    function unmodified and adds exactly one conditional print, and that print is
    now pinned in both directions by four tests (one positive, three negative:
    scripted, cancelled, skipped-first-look). An end-to-end wizard test driving
    the real finale would re-stage the picker test's entire fixture to re-prove a
    property that composes; the cost is not worth the coverage it adds.

Numbers

  • npm test at e1bfbc3: `# tests 3391 / # pass 3390 / # fail 0 /

    skipped 1`.

  • npm run typecheck: clean.
  • Targeted: node --test test/core/cli/wizard/index.test.js test/core/walkthrough-unpicked-attached-clients.test.js -> `# tests 27 /

    pass 27 / # fail 0`.

  • Style: no em dashes, no semicolons, no @typedef, no inline import('...')
    types in the delta or in my fix. All four LLP 0188 anchors and every relative
    link resolve.

Residual findings

  1. (nit, not fixed) writeAttachedNotConfiguredReminder's dryRun
    argument is unreachable-true at its only call site, as above. Deliberate;
    no action recommended.
  2. (informational) The PR body still describes the original commit: it
    states the gate as "the first look ran ... or the pathway is team" and
    quotes pre-fix test counts. Bodies are not edited by review. The code, LLP
    0188 and this thread are the current record.
  3. (informational) This PR remains stacked on fix/issue-604 (Unpicking an attached client is named, not silently left broken (#604) #608). It
    must not be merged before Unpicking an attached client is named, not silently left broken (#604) #608, and the base branch was not touched.

Pushed to fix/issue-614 as e1bfbc3. Base branch fix/issue-604 untouched.

PR #608 was squash-merged as 6ab0d99, so this branch's copies of its
commits conflicted with the same content under new SHAs. Every conflicted
hunk that was #608's takes master; the branch keeps only its own work
(LLP 0188, writeAttachedNotConfiguredReminder, the wizard's firstLookShown
gated repeat, and its tests).

Co-Authored-By: Claude <noreply@anthropic.com>
@philcunliffe philcunliffe added the neutral:stuck neutral attempted this but cannot complete it autonomously — needs a human label Aug 5, 2026
@philcunliffe

Copy link
Copy Markdown
Contributor Author

Neutral triage on bc57742 (fix/issue-614), after the review fix-loop hit its 2-round bound.

What I was checking

The task was to re-verify, with the same skepticism that caught the first two
gate bugs, whether firstLookShown (round 2's fix) is finally the right
condition for the closing repeat, and to confirm the post-#608-merge conflict
resolution didn't resurrect either of the two earlier broken conditions.

Conflict resolution: clean

git diff origin/master...HEAD --stat against current master
(db33f04) touches only llp/0185-* (forward-ref), the new llp/0188-*,
src/core/cli/walkthrough.js (writeAttachedNotConfiguredReminder),
src/core/cli/wizard/index.js (the gate), and the two test files. Nothing of
#608's own work is in the diff (#608 is already in master as 6ab0d99). The
merge commit message correctly states the resolution rule ("every conflicted
hunk that was #608's takes master"), and a full-text search of
wizard/index.js on this branch finds exactly one firstLookShown gate, no
surviving pathway === 'team' disjunct, and no reversion to the round-1
firstLookRan condition. Round 1 and round 2's fixes both survived the merge
intact.

firstLookShown: still wrong, on a path neither round exercised

I read src/core/cli/wizard/first_look.js directly rather than the diff.
runWizardFirstLook has four exit points:

  • no dataset -> {shown: false}, nothing written. Correct.
  • an exception during the query/render -> {shown: false}, nothing written
    (in every path I found). Correct.
  • success -> {shown: true}, writes the block. Correct.
  • the deadline expires with nothing renderable yet (expired && !hasRenderableOverview(rows), first_look.js:167-179) -> writes two lines
    to stdout ("Skipped the first look: summarizing this much history would
    hold up setup." / "Run hyp query overview to see it.") and returns
    {shown: false, reason: 'slow'}
    .

That fourth branch is not hypothetical: it is the documented purpose of
FIRST_LOOK_BUDGET_MS (OVERVIEW_TIME_BUDGET_MS + headroom, 8s total), whose
own doc comment calls out "a pathological day, a disk that stalls" as the
scenario it exists for. It is exercised today by
test/core/cli/wizard/first-look.test.js ("a slow cache skips within budget
and says what to run"), which already asserts both the shown: false result
and the "Skipped the first look" text landing on stdout in the same call.
Neither prior round's fix, nor this PR's own new tests, exercise this branch
through the orchestrator: test/core/cli/wizard/index.test.js's "first look
skips itself" case only covers the no-dataset skip (hasDataset: () => false, which correctly writes nothing), not the slow skip.

I reproduced it directly against this branch's runInitWizard (team pathway,
attachedNotConfigured: ['codex'], a first-look runner that never resolves
inside the 8s budget):

--- elapsed ms: 8027
--- contains "Skipped the first look": true
--- contains "Still attached, no longer collected": false
--- contains "hyp detach --client codex": false
--- contains privacy narration "Nothing has been uploaded yet": true

firstLookShown is false (the 'slow' branch), so the closing repeat's
guard (stranded.length > 0 && firstLookShown, wizard/index.js:263) does
not fire, even though two lines were written by the first look and the
7-line privacy narration follows after. The finale's original warning
(before the daemon restart) is left as the only place hyp detach --client codex ever appears, buried under both blocks with nothing to bring it back
into view.

This is the same failure mode issue #614 exists to fix, reached through a
different door than either prior round checked. The code comments at
wizard/index.js:220-226 assert "a first look that skipped itself wrote
nothing" as the justification for reading shown instead of ran — that
claim is true for the no-dataset and error skips but false for the slow skip,
which is the actual bug: firstLookShown conflates "rendered the block" with
"wrote nothing," and only the former is what it measures.

User-visible impact: on a slow/cold cache during hyp init (large
backfill just landed, disk under load), a user who unpicked a previously
wizard-attached client never sees the warning that its requests can start
silently failing until hyp detach is run — worst on a managed host, where
(per LLP 0185 §status-backstop, cited in this PR's own body) hyp status's
mirror diagnostic is gated off entirely, so this print is the only signal
that ever existed. I judge that a production defect (a warning about an
active failure mode that silently never reaches the user), not a preference.

Non-blocking findings, carried over from round 2 (unchanged by my check)

  1. Nit, deliberate, confirmed still true on this head: dryRun passed to
    writeAttachedNotConfiguredReminder (wizard/index.js:267) is provably
    false at its only call site, because the call is gated by
    firstLookShown which requires firstLookRan, which requires
    opts.finale?.dryRun !== true. Round 2's call to leave it (the export's
    signature should describe the print, not its one caller) stands; this
    does not change with the fix below.
  2. Informational: the PR body describes a pre-fix, stacked-on-Unpicking an attached client is named, not silently left broken (#604) #608 state
    of the world; review protocol does not edit bodies.
  3. Informational: the feature test stubs finaleRunner rather than
    asserting end-to-end against the real finale print; round 2 judged this
    adequate because the finale's own exactly-once print is separately pinned
    against the real function on the picker path. I have no new objection to
    that reasoning, but it's moot until the blocker above is resolved, since
    the new tests also don't cover the 'slow' skip branch at all.

What would unstick this

The fix is narrow: change the closing-repeat gate (or runWizardFirstLook's
contract) so that "wrote something" is measured directly rather than assumed
from shown. Two shapes that would work:

  • Broaden FirstLookResult so reason: 'slow' counts as shown-enough for the
    repeat's purposes (it did put ink on the screen), or
  • Have runWizardFirstLook return whether it wrote anything, independent of
    whether the block itself rendered, and gate the repeat on that instead.

Either needs a new orchestrator-level test exercising the slow-skip branch
with attachedNotConfigured set, alongside the three existing gate tests.

I have not modified any code — this comment is diagnostic only. Reply on this
thread to unstick: confirm the fix shape, or tell me the "warn or don't"
tradeoff here is intentional and I'm misjudging severity.

I did not merge, did not mark ready, and did not push to master.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

neutral:stuck neutral attempted this but cannot complete it autonomously — needs a human

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Follow-up: deferred review findings from PR #608

1 participant