Skip to content

Onboarding language and flow fixes from first user-onboarding feedback - #771

Merged
platypii merged 10 commits into
masterfrom
onboarding-language
Aug 14, 2026
Merged

Onboarding language and flow fixes from first user-onboarding feedback#771
platypii merged 10 commits into
masterfrom
onboarding-language

Conversation

@platypii

@platypii platypii commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Fixes from the 8/12 onboarding session (first outside user walking hyp init on a screen share), plus follow-ups from re-running the flow on a configured machine.

What changed

  • Backfill consent says what the import can actually deliver. "last N days" (the retention window) overpromised what clients keep on disk; now "already on this machine (up to N days)".
  • Elapsed-time spinner over the two silent waits (org-config converge after login, backfill import). Non-TTY output is byte-identical to before.
  • Claude Desktop is never pre-checked by detection. A detected needs_setup row seeded the defaults, so "record all of these" opted users into a setup flow that then re-negotiated. Detection still labels the row; checking it is the deliberate opt-in.
  • Defaults gates label a seeded needs_setup row ("· needs extra setup") when a config already composes it, so the later setup step is announced rather than a contradiction.
  • Privacy narration stops restating the sync offer one screen early; paths that end without the offer keep the hyp sync sentence.
  • Overwrite confirm defaults to yes. It is the last keystroke of the happy path, and a bare enter aborted the whole walk; the config is backed up before the rewrite either way.
  • Desktop setup: lead with the decision, ask once, never re-ask. The consent screen now opens with the decision instead of two paragraphs of credential-helper mechanism. Its one question defaults to yes and names the browser sign-in a yes launches (a build that briefly dropped the prompt threw the user straight into the OAuth flow). And it is asked for a new pick only: the configure phase skips a needs_setup row the existing config already composed, so reconfiguring an unrelated setting never re-opens Desktop setup. LLP 0139 amended in place.

Testing

Full suite (4039) and typecheck clean on every commit.

The prompt promised "last N days" from the retention window, but each
client keeps far less on disk (Claude prunes after ~30 days), so a
90-day team install read as a promise the import could not keep. Say
"already on this machine (up to N days)" instead.
The org-config converge after login can block a full minute and the
backfill import runs multi-second with no output; both showed a static
line that read as a hang (first onboarding feedback, 8/12). On a TTY
the announce line now animates in place with elapsed seconds and clears
when the wait settles; off a TTY (and under HYP_NO_TUI=1) output is
byte-identical to before.
A detected needs_setup row seeded the defaults, so the express gate and
pick gate said "record all of these" with Claude Desktop in the list -
and the configure phase then asked again with a consent screen
defaulting to no, the wizard contradicting itself (first onboarding
feedback, 8/12). Detection still labels the row detected; checking it
is now the deliberate opt-in its consent gate expects. Config- and
selection-seeded rows are a user's recorded answer and still check.
The closing sequence said "to send it sooner, run hyp sync..." and then
immediately asked "send now or wait?" with a row carrying the same
command and the same asks-first promise - two blocks saying one thing,
in the stretch of setup that already reads as a wall of text (first
onboarding feedback, 8/12). The narration now drops that sentence
exactly when the offer follows; aborts, dry runs, and a close that
cannot prompt keep it, with the cannot-prompt skip stating it as a line
instead of a question.
…ater

A config that already composes Claude Desktop re-seeds it on every
reconfigure, so the gate said "record and sync all of these" over a row
whose configure phase then asks its own consent question with a default
of no. The gate keeps the row (it is a recorded answer) but now says
"needs extra setup" on it, so the later screen is announced rather
than a contradiction. Locked rows keep the fleet suffix alone.
The confirm lands at the end of an attended run, after every question
was answered, and a bare enter aborted the whole walk with exit 1 -
the happy path's last keystroke threw its answers away. Enter now
completes the run; only an explicit no declines. Safe as a yes because
the file is backed up before the rewrite either way.
The install opened with two paragraphs of credential-helper mechanism
and a y/N defaulting to "leave Claude Desktop alone" - jarring right
after the user chose to record it. The explanation now leads with the
decision (Claude Desktop needs extra setup, here is why and what will
change), and the single question after it defaults to yes: the picker
row is never pre-checked, so a user at this prompt chose to be here,
and the plist write still cannot happen without the sudo password.

The question survives (an earlier cut removed it) because a yes on a
signed-out machine immediately launches the Claude OAuth flow in a
browser, and that launch must never be a surprise: the prompt names it
when that is what enter does next. Non-answers still decline with the
--yes / --print-commands hint and EOF cannot hang.

Asked for a new pick only: the wizard's configure phase skips a
needs_setup row the existing config already composed, so reconfiguring
an unrelated setting never re-opens Desktop setup. The standalone
command stays the finish and repair path. LLP 0139 amended in place.
@platypii platypii added the neutral:adopt Foreign PR adopted into neutral's reconcile scope label Aug 14, 2026
@philcunliffe philcunliffe added the neutral:adopted Adoption completion record: merged while carrying neutral:adopt (LLP 0031) label Aug 14, 2026
The question's sign-in clause was sharpened by running
`claude-account status` as a predicate. Sub-commands share the
dispatcher's stdout, so that probe printed `mode: subscription` and
`signed in: no (run 'hyp claude-account login')` between the disclosure
and the question - a diagnostic in the middle of a consent screen,
telling the user to run a command the flow was about to run for them.
Tests could not see it: the fixture's `commands.run` writes nothing.

The clause is now conditioned on the reader's state rather than probed,
and true either way; `org_key` mode still drops it, being the one case
config alone settles. Two assertions pin it: the status probe belongs to
the login step and is called once, and a decline runs nothing at all.

Also in the same screen: the undo instruction pointed at the credential
helper, which is not what attaches Claude Desktop. Removing the helper
leaves Desktop pointed at the local gateway with nothing to answer it.
It names the managed plist now.

Two claims that were not true: `hyp status` has no line for an
incomplete `needs_setup` row, so the configure-phase skip has no
standing pointer at its repair path (recorded as a known gap, in the
comment and the doc, rather than asserted as covered); and the sync
offer's "same polarity as the overwrite confirm" no longer holds, since
that confirm flipped to yes in this branch.

Finally, the amended `#default-no` block opened its blockquote after an
inline anchor on the same line, so it did not render as one.

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

Copy link
Copy Markdown
Contributor

Verdict: findings. Seven actionable items; six fixed and pushed as bbe37d1, one left for you because it is a process call I should not make unilaterally. Nothing here disputes the direction of the PR: the copy is a clear improvement, the flow reasoning holds up, and the new branches are reachable. What follows is the residue.

Fixed (commit bbe37d1)

1. The sign-in probe printed itself into the middle of the consent screen (high)

hypaware-core/plugins-workspace/claude-desktop/src/install.js:172-173

const signInFirst = opts.credential.mode !== 'org_key'
  && (await cmdCtx.commands.run('claude-account status', [])) !== 0

cmdCtx.commands.run closes over the dispatcher's own stdout (src/core/cli/dispatch.js:427-437 passes stdout straight into runCommandByName), and runStatus writes user-facing lines (hypaware-core/plugins-workspace/claude-account/src/index.js:281 and :304). So the real screen, between the disclosure and the question, was:

The credential never leaves this machine ...

mode: subscription
signed in: no (run 'hyp claude-account login')
Set up Claude Desktop now? The first step opens the Claude sign-in in your browser. [Y/n]:

Two diagnostic lines inside a consent screen, one of which tells the user to go run a sign-in that the flow is about to run for them. It is invisible to the suite because the fixture's commands.run stub writes nothing (test/plugins/claude-desktop-install.test.js:50-56), which is why CI stayed green.

There is no way to silence it at the call site: commands.run(name, argv) takes no stream override. So the fix drops the probe and conditions the clause on the reader's own state instead, which is true either way:

Set up Claude Desktop now? If you are not signed in yet, the first step
opens the Claude sign-in in your browser. [Y/n]:

org_key mode still drops the clause entirely, since that case is settled by config alone. signInFirst became mayNeedSignIn so the name does not overclaim.

If you want the crisper unconditional sentence back, the clean route is a signedIn() predicate on AnthropicCredentialCapability (hypaware-core/plugins-workspace/claude-account/src/types.d.ts:9) rather than a command probe: the capability is the sanctioned coupling between these two plugins, and claude-desktop already receives it. I did not build that here because it widens a plugin contract, which is yours to decide.

Two assertions now pin the behavior (test/plugins/claude-desktop-install.test.js): the claude-account status probe is called exactly once and belongs to the login step, and a decline runs no sub-command at all. I verified both fail if the probe line is reinstated.

2. The undo instruction names the wrong file (medium)

hypaware-core/plugins-workspace/claude-desktop/src/consent.js:73

"To undo it later, delete the helper file and run hyp claude-account logout." The helper is not what attaches Claude Desktop; the root-owned managed plist is. Following that literally leaves Desktop still pointed at the local gateway with nothing left to answer the credential request, which is worse than either doing nothing or undoing properly. There is no claude-desktop uninstall to point at either (index.js registers profile, install-helper, status, install, verify). The pre-PR text said "the file above", which at least sat directly under the plist line, so this is a regression the rewrite introduced. It now names args.plistPath and says it needs sudo.

3. Two claims that are not true (medium)

src/core/cli/wizard/configure.js:57-58 and the new LLP 0139 amendment both asserted "hyp status names it when setup is incomplete". It does not: src/core/commands/status.js has no needs_setup branch, and needsSetup appears nowhere outside pick.js and configure.js. The only mention of hyp claude-desktop install in core is hyp attach's help text (src/core/cli/core_commands.js:310).

That matters because it was the stated mitigation for the new skip, and the skip has a real hole under it: "composed in the config" is the only signal the configure phase has, so a row whose setup was declined or failed is carried exactly like a finished one. Such a machine can never be re-offered the setup by a reconfigure, and nothing points at the repair.

I recorded it honestly as a known gap in both the comment and the doc rather than deleting the sentence, but the gap itself is real and unfixed. See "left open" below.

4. Stale cross-reference in an untouched file (low)

src/core/cli/wizard/sync_now.js:128-130 said the sync offer uses "the same polarity the overwrite confirm and the hyp sync prompt itself use". The overwrite confirm flipped to yes in this branch, so that sentence became false the moment b2b6c7b landed. Rewritten to name only hyp sync, and to say why the two now differ: a bare enter there commits answers to a file that is backed up either way, where here it would send data off the machine.

5. The #default-no amendment does not render as a blockquote (low)

llp/0139-desktop-picker-consent.decision.md:135

<a id="default-no"></a>> **Amended 2026-08-13: the prompt now defaults to

CommonMark needs > as the first non-space character of the line, so with the inline anchor in front of it that > is literal text: the first line renders as a paragraph with a stray > and a dangling **, and the blockquote only opens on line two, mid-sentence. Every other amendment in the corpus puts the marker at line start. The anchor now sits on its own line, blank line, then the quote. Mechanical fix, allowed under LLP 0156.

6. The new not-interactive line had no test (low)

src/core/cli/wizard/sync_now.js:73 adds a user-visible line, and the narration upstream now depends on it firing (offerFollows drops the hyp sync sentence whenever this step is expected to render). test/core/cli/wizard/sync_now.test.js:103 asserted the return value only, so the line could be deleted with a green suite. Assertion added.

Left open, for you

LLP 0139 is Status: Accepted and this PR edits what it decided (medium, process, not a production risk)

llp/0139-desktop-picker-consent.decision.md:93 and :143. CLAUDE.md is explicit: an Accepted/Active LLP is a record, not a worksheet; intent changes by minting a new rfc/spec/decision that @refs what it supersedes, plus an Extended-by:/Superseded-by: forward-ref on the old doc. This PR instead reverses #default-no and reshapes #informed-consent through two in-place > **Amended 2026-08-13.** blocks with no LLP number attached.

The corpus is consistent on the other side of this: every existing amendment block is **Amended by [LLP NNNN](...)** (LLP 0011 lines 13, 23, 27; LLP 0025 lines 82, 122, 341), and 0139's own prior amendment is **Scope correction ([LLP 0140](...))**. A bare date is reachable from nowhere, so a reader arriving at 0139 from an @ref has no way back to the reasoning or to this PR.

I did not fix this because minting the next LLP and deciding whether 0139 is extended or superseded is an authoring call that belongs to you, and because I would rather not add a second in-place edit to an Accepted doc while reporting that in-place edits are the problem. My own doc edits were confined to text this PR itself added, plus the mechanical blockquote repair.

Preference/process finding, not a blocker. The shipped behavior is fine either way.

The incomplete-setup hole (finding 3) is a production risk, in my judgment

Not a merge blocker at this size, but it should not be lost. Concretely: tick Claude Desktop in hyp init, answer no to the setup question (or have the sudo step fail), and the plugins stay composed in the config with no plist and no credential. Every later hyp init now carries the row and skips it silently, and no core surface mentions it. Before this PR the reconfigure re-ran the command, which is exactly the annoyance being fixed, so the fix is right and the missing half is a completeness signal. Two ways out, both yours:

  • a hyp status line for a picked needs_setup row whose adapter reports itself unfinished (claude-desktop status already returns nonzero when the helper is missing, so the signal exists, it is just not surfaced by core); or
  • narrow the skip to rows the adapter reports as converged, rather than to rows merely present in the config.

Checked and clean

  • No U+2014 anywhere in the diff, added lines or otherwise. Grepped explicitly.
  • offerFollows is an exact mirror. index.js:667 computes interactive && !cancelled && dryRun !== true; the offer at :679 additionally requires holdDeadline !== null, and the narration returns non-null exactly when it printed. Every path that suppresses the sentence either renders the offer (whose "Send now" summary names hyp sync and the asks-first promise) or hits sync_now.js's not-interactive branch, which now states it. The one uncovered seam is runWizardSyncNow's catch returning reason: 'error', which would swallow the sentence; reaching it needs a throwing confirm factory, and I did not think that worth a guard.
  • Spinner off-TTY output is byte-identical to what the two call sites printed before, so the scripted transcripts are safe, and HYP_NO_TUI=1 routes to the plain path. The docblock's claim checks out.
  • @ref targets resolve. LLP 0011#autodetect-vs-default matches ## Autodetect vs default (0011:102); LLP 0188#never-silent matches the explicit anchor at 0188:67.
  • Detection no longer seeding needs_setup rows is consistent end to end: detectedSeed filters only the detection tier, so configured and initialSelection still carry the row, seedOrigin is unaffected, and the · detected label is still applied from the unfiltered detected set, as the comment claims.
  • defaultOverwriteConfirmFactory's EOF hang (rl.question never settles on a stdin that ends without a line, walkthrough.js:315) is pre-existing and unchanged by the default flip, so I left it. Worth knowing it is there, given the sibling gate in consent.js was explicitly rewritten to avoid exactly that failure.

Gates

npm test and npm run typecheck in an isolated worktree, before and after my commit: identical results both times. 15 test failures and 1 typecheck error, all environmental in this sandbox (hyparquet version skew, missing marked) and all green on CI at this head. No delta from my change. The four touched test files pass in isolation, 56/56.

…h it

The privacy narration now drops its `hyp sync` sentence whenever the
closing sync offer is about to render, on the reasoning that the offer's
"Send now" row states the same command. But that offer is a TUI select
with `clearOnResolve`, so its frame is erased the moment it resolves -
and waiting is the default answer, reached by a bare enter or an esc.
On the ordinary attended enrolled close the run therefore ended with the
release verb nowhere on screen: the narration had dropped it and the only
other sighting had been wiped.

The wait now writes the same still-held line the sync-declined and
spawn-failed paths already write, which names the deadline and `hyp sync`.
Dropping the sentence upstream stays right; it is only safe because the
wait restates it after the choice instead of before it.

Also: LLP 0139's new amendment claimed "only a real enter is a yes", which
the prompt it describes does not do - anything that is not `n` or `no`
takes the default. Says what the code does instead.

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

Copy link
Copy Markdown
Contributor

Verdict: findings. Round 2 of 2. One new actionable item, found and fixed (e1ad58e), plus a small doc-accuracy repair alongside it. Round 1's five fixes all hold up under re-verification; none is inert. The two items round 1 left for you are restated at the bottom with triage classifications, unchanged in substance.

New this round

1. A declined first sync ends the run with hyp sync nowhere on screen (medium, fixed)

src/core/cli/wizard/sync_now.js:82 (pre-fix), reached from src/core/cli/wizard/index.js:667-670.

This branch is the one the round-1 record checked and cleared, so it is worth being precise about what was missed. The claim was: every path that suppresses the narration's hyp sync sentence either renders the offer, whose "Send now" summary names the command, or hits the not-interactive branch, which now states it. The first half does not survive contact with the TUI.

askSendNow goes through defaultConfirmSelectPromptFactory, which on a TTY is tuiConfirmSelectPromptFactory with clearOnResolve: true (walkthrough.js:408). The runtime's cleanup then does exactly what the option says (tui/runtime.js:74-79):

if (clearOnResolve && previousLineCount > 0) {
  // Move the cursor back to the top of the rendered frame and clear
  // everything below it, leaving the screen as it was before the
  // prompt drew.
  stdout.write(`\x1b[${previousLineCount}A\r${CLEAR_TO_END}`)
}

So the row that named hyp sync is erased the instant it is answered. And WAIT is the default answer, taken by a bare enter or an esc, on which runWizardSyncNow returned immediately and wrote nothing at all. writeStillHeld (the line that names the deadline and the command) was wired only to spawn-failed and sync-declined, both of which are downstream of choosing Send now.

Net effect on the ordinary attended enrolled close, which is the modal path this PR is about: the narration drops "To send it sooner, run hyp sync", the user presses enter to wait, the frame is wiped, and the run proceeds to the first ask having never left the release verb on screen. Before this branch the sentence was unconditional, so this is a regression the offerFollows change introduced, and it is the exact never-silent floor (LLP 0188) the sibling sync_now.js:73 line was added to hold.

It was invisible to the suite because syncNow.confirm is stubbed, so no frame is drawn and none is cleared. Worse, the added assertion at test/core/cli/wizard/index.test.js:919 pinned the symptom: assert.doesNotMatch(text, /To send it sooner/) passed on a transcript in which hyp sync appeared nowhere.

Fixed by writing the existing still-held line on the wait, and on the esc that means the same thing:

Nothing was sent. Your history stays on this machine until <deadline>;
run `hyp sync` any time to send it sooner.

after the choice rather than one screen before it, so the "said twice in a row" complaint this PR fixes stays fixed. Three assertions now pin it (sync_now.test.js wait and cancel, index.test.js end to end); I confirmed all three fail against the pre-fix sync_now.js and pass with it.

2. LLP 0139's new amendment describes a prompt rule the code does not have (low, fixed)

llp/0139-desktop-picker-consent.decision.md:151. The amendment this PR adds ends "Only an explicit no declines; only a real enter is a yes." The second clause is not what confirmProceed does:

return !/^n(o)?$/i.test(answer.trim())

Anything that is not n or no is a yes, including a typed "nope" or "no thanks", at a gate that acquires a credential and escalates to root. walkthrough.js:325 states this honestly for the sibling overwrite confirm ("a bare enter (and any stray answer) proceeds"); 0139 and consent.js did not. Both now say what the code does. I did not change the parsing itself, see below.

Round 1's fixes, re-verified

  • The dropped probe (fix 1) is correct and complete, not merely quieter. mayNeedSignIn = opts.credential.mode !== 'org_key' is strictly broader than the old signInFirst, so replacing the runtime probe with static state cannot hide the clause on a machine that would have seen it; it can only show it on an already-signed-in machine, where the new conditional wording ("if you are not signed in yet") is still true. The wording also matches what the step actually does: ensureCredentialLogin re-checks claude-account status and skips with "already signed in" at code 0, so a signed-in yes opens no browser and the sentence did not promise one. org_key returns "no sign-in needed" before any probe, which is the case the clause is dropped for.
  • The undo text (fix 2) names the file the install actually writes. buildConsentExplanation receives the same plistPath that ensurePlistWritten is handed (install.js:120, :205), so the undo line and the "will write" line above it can never name different paths. Sudo is correctly stated.
  • Fixes 3, 4 and 5 verified as described. The #default-no anchor still resolves and its amendment explicitly preserves the non-answer rule, so test/plugins/claude-desktop-install.test.js:464's @ref LLP 0139#default-no remains honest even though the section's own default inverted.

Checked and clean

  • No U+2014 anywhere in the full branch diff or in my commit. Grepped both.
  • No semicolon-terminated JS statements added by the branch. The one hit is prose inside a comment.
  • The --yes behavior change is deliberate and covered. Moving !assumeYes inward means --yes now prints the disclosure and skips only the question, which claude-desktop-install.test.js:457-459 pins on both sides.
  • previouslyConfigured cannot false-positive derivatively. configuredPickerSources requires every plugin a row contributes to be present, and claude-desktop's own plugin is one of them, so the set never reads a Desktop row back off some other row's composition. It is empty on a first run (configured is undefined), and back-navigation within a first run cannot populate it, so a newly picked row still gets its setup.
  • defaultRows is drawn from seed (pick.js:231), and detection no longer seeds a needs_setup row, so the new · needs extra setup suffix can only appear on a row backed by a recorded answer, exactly as its docblock claims.
  • The spinner is off-TTY inert and its label writes are transient on a TTY, so no scripted transcript moves.
  • Full suite and typecheck run in an isolated worktree before and after my commit: identical outcomes both times, 15 failures and 1 typecheck error, every one environmental in this sandbox (hyparquet skew, missing marked) and green on CI at this head. No delta from my change.

Carried forward from round 1, for triage

A. A needs_setup row whose setup was declined or failed is carried like a finished one

src/core/cli/wizard/configure.js:57 and LLP 0139's #informed-consent amendment. Unchanged this round: round 1 replaced the false "hyp status names it" claim with an explicit known-gap note (src/core/commands/status.js has no such branch), but the hole under it is real. Tick Claude Desktop in hyp init, decline the setup question or have the sudo step fail, and the plugins stay composed in the config with no plist and no credential; every later hyp init carries the row and skips it silently, and no core surface points at the repair.

Classification: production risk, not a merge blocker. The pre-PR behavior it replaces (re-running the sign-in on every reconfigure) was worse, so this should not hold the PR. It should not be lost either. Two ways out, both design calls that belong to a maintainer:

  • a hyp status line for a picked needs_setup row whose adapter reports itself unfinished (claude-desktop status already exits nonzero when the helper is missing, so the signal exists and is simply not surfaced by core); or
  • narrow the skip to rows the adapter reports as converged, rather than to rows merely present in the config.

B. LLP 0139 is Status: Accepted and this PR edits what it decided

llp/0139-desktop-picker-consent.decision.md:93 and :145. Unchanged this round, and deliberately so. The PR reverses #default-no and reshapes #informed-consent through in-place > **Amended 2026-08-13.** blocks carrying no LLP number and no Extended-by: line. CLAUDE.md requires a new rfc/spec/decision that @refs what it supersedes plus a forward-ref on the old doc, and every other amendment in the corpus is **Amended by [LLP NNNN]** (LLP 0011:13, 0011:23, 0025:82, 0025:341), including 0139's own earlier **Scope correction ([LLP 0140])**. A bare date is reachable from nowhere, so a reader arriving at 0139 from an @ref has no route back to this reasoning.

I did not author the replacement LLP, for the same reason round 1 did not: authorizing a design reversal of an Accepted doc, and deciding whether 0139 is extended or superseded, is the maintainer's call and not neutral's. My own edits to that file this round stayed inside text this PR itself added.

Classification: process finding, preference, not a production blocker. The shipped behavior is correct either way; what is missing is the paper trail.

@philcunliffe philcunliffe added the neutral:stuck neutral attempted this but cannot complete it autonomously — needs a human label Aug 14, 2026
@philcunliffe

Copy link
Copy Markdown
Contributor

neutral triage: stuck. The review fix-loop hit its 2-round cap with findings still open. Triage re-verified both rounds and finds one of the two residuals is a hole this PR itself widens, so the triage marker is withheld and the PR is parked for a maintainer decision.

What neutral was doing

Triage of PR #771 at head e1ad58e after two marker-signed review rounds (5f19a12, bbe37d1). Every fix both rounds claim was confirmed present in the committed tree (list at the bottom); none is inert. Two residuals remained, and triage classified them independently of the rounds' own classifications.

Blocker: a declined or failed Desktop setup is now permanently stranded, and this PR is what makes it permanent

src/core/cli/wizard/configure.js:65-69 (the new carried skip) with src/core/cli/wizard/index.js:589-610 (config committed to disk before the configure phase runs).

The decisive fact, determined from git history rather than taken from the review records:

  • Pre-existing on master: the composed config is written at the commit point, before the configure phase, and a dropped configure does not un-compose the row (structure unchanged since ccef8e3). So "a declined or sudo-failed setup leaves @hypaware/claude-desktop composed with no plist and no credential" predates this PR.
  • Introduced by this PR: the previouslyConfigured skip (commit 5f19a12; git log -S previouslyConfigured finds no other commit). On master the configure phase re-ran every picked needs_setup row's configure_command on every attended run, so an abandoned setup was re-offered at the next reconfigure. That re-run was the only repair trigger core had, and this branch removes it.

Net effect at this head: tick Claude Desktop in hyp init, decline the consent question (exit 1 at hypaware-core/plugins-workspace/claude-desktop/src/install.js:181-183) or have the sudo step fail, and the config permanently claims a source that captures nothing. src/core/commands/status.js has no needs_setup branch, no reconfigure will ever re-offer the setup, and the only pointers to the repair are transient lines printed at the moment of the decline or failure. In a capture product, a configured source that silently records nothing is a production defect, the same never-silent class (LLP 0188) that round 2 already caught this branch regressing once. It also matters that the PR's original mitigation claim, "hyp status names it when setup is incomplete", turned out to be false and was downgraded to a known-gap note in round 1: the design as written relied on a safety net that does not exist.

Triage did not fix it for the same reason both review rounds did not: every remedy widens a contract (wizard/picker or core status) that is yours to change, not neutral's.

Decision needed, with options

Any one of these unblocks the PR:

  1. Narrow the skip to adapter-converged rows. Skip a carried needs_setup row only when its adapter reports done, for example claude-desktop status exit 0 (it already exits nonzero when the helper is missing, so the signal exists and is simply not consulted). Keeps the 8/13 fix (no re-ask on a finished machine) while an abandoned row is re-offered. Touches the wizard/picker contract with a per-descriptor completeness probe.
  2. Surface the stranded state in hyp status. A line for a composed needs_setup row whose adapter reports itself unfinished, naming hyp claude-desktop install as the repair. Leaves the skip as-is; makes the state visible. Touches the core status contract.
  3. Accept the gap explicitly. If you judge the printed hints at decline/failure time sufficient for now, say so in a reply; neutral will re-triage this head treating it as a deferral, open the follow-up issue, and append the marker.

Also open, not a blocker: LLP 0139 amended in place

llp/0139-desktop-picker-consent.decision.md:93 and :145. The doc is Status: Accepted, and this PR reverses #default-no and reshapes #informed-consent via inline **Amended 2026-08-13** blocks with no LLP number and no Extended-by: forward-ref. CLAUDE.md requires change-by-new-doc, and the corpus convention is **Amended by [LLP NNNN]** (LLP 0011, LLP 0025, and 0139's own earlier Scope correction (LLP 0140)), so a reader arriving at 0139 has no route back to this reasoning. Authoring the replacement LLP and deciding extended versus superseded is a maintainer call; both rounds and triage deliberately declined to make it. Process only, no production risk, but it needs an author before or shortly after merge.

Verified resolved at e1ad58e, no action needed

  • Round 1: the consent-screen probe is gone (install.js:180, static mayNeedSignIn; a decline invokes no sub-command, test/plugins/claude-desktop-install.test.js:347, and the one status probe belongs to the login step, :400); the undo text names the managed plist and sudo (consent.js:71-76); the false hyp status claim is a known-gap note (configure.js:58-64 and the 0139 amendment); the stale cross-ref is rewritten (sync_now.js:139-144); the #default-no anchor renders (0139:143-145); the not-interactive line is pinned (test/core/cli/wizard/sync_now.test.js:112).
  • Round 2: a declined or escaped sync offer restates hyp sync after the cleared frame (sync_now.js:91 and :126; end-to-end assertion test/core/cli/wizard/index.test.js:925); 0139's "only a real enter is a yes" is corrected to what confirmProceed does (0139:152, consent.js:163-165).

How to unstick

Reply on this PR with the option you pick, or push to the branch. neutral monitors the thread and re-engages next tick. Options 1 and 2 can be neutral-implemented once authorized; option 3 needs only your reply.

…orrected and pinned

The review flagged two leftovers. First, the branch amended Accepted
LLP 0139 in place with bare dated blocks, against the corpus rule that
change mints a new numbered doc: LLP 0224 now carries the decisions
(deliberate tick, decision-led disclosure, one question defaulting to
yes, ask once per pick, and the repair surface), and 0139's blocks are
Amended-by/Superseded-by pointers to it.

Second, the recorded claim that no core surface points at an
incomplete Desktop setup was overstated: hyp status already warns via
client_attach_missing, whose repair names the row's configure_command
(0139 #repair-must-be-runnable) - the string is assembled in the
daemon collector, which is why grepping core missed it. The configure
phase comment now states the surface instead of a gap, and a new
status-collector test pins the diagnostic and its repair string so the
claim the skip rests on cannot silently rot. Known follow-up recorded
in 0224: claude-desktop has no attach probe, so the warning clears via
the reconciler rather than by observing the plist.
@platypii

Copy link
Copy Markdown
Contributor Author

Thanks for the pass, the consent-screen stdout leak especially. Both leftovers are handled in 2d3ad70:

LLP process. Minted LLP 0224 (desktop setup second pass: deliberate tick, decision-led disclosure, one question defaulting to yes, ask once per pick, repair surface). 0139's two dated blocks are now standard Amended by [LLP 0224] / Superseded by [LLP 0224 §one-question-default-yes] pointers, with the original §default-no text kept below as the record.

The incomplete-setup hole. Pushing back on finding 3: the surface exists. client_attach_missing in src/core/daemon/status.js (the 0139 #repair-must-be-runnable implementation) fires for an enabled plugin with no attach marker and builds its repair from the descriptor's configure_command, so the literal string hyp claude-desktop install never appears in core, which is why the grep came back empty. I verified on a machine in exactly the declined state (plugins composed, no plist): hyp status prints [WARN] client_attach_missing: ... run 'hyp claude-desktop install'. So the skip's mitigation was real, just undocumented and unpinned. Now it's both: the configure-phase comment and the doc name the diagnostic instead of a gap, and test/core/status-needs-setup-repair.test.js pins the kind and the exact repair string.

One real residue, recorded in 0224 rather than fixed here: claude-desktop declares no attach probe, so its client reports unattached unconditionally and the warning clears via the reconciler's attach action, not by observing the plist. A plist-reading attach probe is the named follow-up.

@philcunliffe philcunliffe removed the neutral:stuck neutral attempted this but cannot complete it autonomously — needs a human label Aug 14, 2026
@philcunliffe

Copy link
Copy Markdown
Contributor

Re-engaging with your guidance - taking the client_attach_missing counter-evidence on finding 3 (the repair is built from the descriptor's configure_command, so the literal-string grep that drove the blocker call was the wrong probe), plus LLP 0224 and the two forward-ref pointers on 0139 closing the process item.

The stuck label is removed. Neutral re-runs the review rung at the current head (2d3ad70) on its next tick and will verify the diagnostic path and the new pin in test/core/status-needs-setup-repair.test.js directly rather than by grep. The attach-probe residue you recorded in 0224 is noted as a follow-up, not a blocker on this PR.

@philcunliffe

Copy link
Copy Markdown
Contributor

neutral triage: pass at 2d3ad70. No blockers. One non-blocking follow-up is deferred to #772, and the triage marker is appended. The PR is clear from neutral's side; the terminal is held for the maintainer as usual for an adopted PR.

The previous blocker call was wrong, and platypii's pushback was right

The stuck report claimed no core surface points at an incomplete Desktop setup, evidenced by a tree-wide grep for the literal string hyp claude-desktop install coming back empty. That was the wrong probe: the repair string is assembled at runtime in the status collector from the picker descriptor's configure_command, so the literal never appears in core and an empty grep proved nothing. The surface existed on master all along. The blocker should not have been raised on that evidence, and the contributor's refutation is confirmed correct on every point. Thank you for taking the time to demonstrate it rather than just asserting it.

What triage independently verified at this head

  • The client_attach_missing path traced end to end. src/core/daemon/status.js:894-927: for each contributes.client descriptor, a plugin that is enabled (activePlugins.includes(descriptor.plugin)) with no attach probe yields { attached: false }, so configured && !probe.attached fires the warning; the repair is built from catalog.pickerDescriptors.get(clientName).configureCommand, which claude-desktop's manifest declares as claude-desktop install (hypaware-core/plugins-workspace/claude-desktop/hypaware.plugin.json). The whole branch is pre-existing on master (this PR touches nothing in the collector), so the ask-once skip's mitigation was real from the start.
  • The new pin is a real pin, not vacuous. test/core/status-needs-setup-repair.test.js writes a real config in exactly the declined state (composed plugins, no plist), runs the actual collectHypAwareStatus, and asserts the kind, the severity, repair deep-equal to ['hyp claude-desktop install'], and the message text, plus a negative case with the plugin absent. Both tests pass in a clean worktree at this head, and triage mutation-tested the pin: breaking the configureCommand repair construction in status.js makes the first test fail. It cannot pass vacuously.
  • LLP 0224 and 0139's forward-refs. llp/0224-desktop-setup-second-pass.decision.md exists with a sane header (Decision, Accepted, 2026-08-14, Related back to 0139/0011/0131/0190) and carries the five decisions under explicit anchors. 0139's two bare dated blocks are replaced by Amended by [LLP 0224] and Superseded by [LLP 0224 one-question-default-yes] pointers, the original default-no text is preserved beneath as the record, and the only 0139 text 2d3ad70 rewrote is text this PR itself had added. Every new @ref anchor resolves (0224#ask-once-per-pick, 0224#repair-surface, 0139#repair-must-be-runnable). This satisfies the change-by-new-doc rule.
  • Both prior rounds' fixes still present at 2d3ad70. The final commit touched only the two LLP docs, configure.js, and the new test, and triage re-verified the earlier fixes in the tree directly: static mayNeedSignIn with no consent-screen probe (install.js:180), the undo text naming the plist and sudo (consent.js:73-75), the still-held hyp sync line on wait and cancel (sync_now.js:91, :105, :126), and the corrected 0139 prompt-rule text. The touched suites pass 99/99 in a clean worktree (claude-desktop-install, sync_now, wizard index, both status collector tests). No U+2014 anywhere in the branch diff.

Deferred to #772, not blocking

The residue platypii recorded in 0224 is real but acceptable: claude-desktop declares no attach probe, so the client reports unattached unconditionally. That makes the warning correct for the declined state and a standing false positive on a machine whose setup finished. Triage confirmed this facet is pre-existing master behaviour, unchanged by this PR, and documented with the fix named (a plist-reading attach probe). One clarification for the record: with no probe, the collector cannot observe attachment at all, so nothing clears the warning by observation today; the probe follow-up is what makes it clearable. Tracked in #772 along with two minor pre-existing notes from the review rounds.

@philcunliffe philcunliffe added the neutral:approved neutral reviewed this and holds it for a maintainer merge (own or adopted PR; LLP 0025/0030) label Aug 14, 2026
@platypii
platypii merged commit 3304d38 into master Aug 14, 2026
9 checks passed
@platypii
platypii deleted the onboarding-language branch August 14, 2026 17:29
philcunliffe pushed a commit that referenced this pull request Aug 14, 2026
Conflict: src/core/cli/walkthrough.js, defaultOverwriteConfirmFactory's
return expression. #771 flipped the confirm's default to yes ("only an
explicit no declines"); this branch routes the prompt through
queuedLineAsker so EOF settles as `null` instead of hanging. Both
intents compose: the null is coalesced to the empty line and read by
#771's parse, so a spent stdin takes the yes the printed [Y/n] promises
and the config is regenerated over the backup the caller already takes.

The EOF regression test's two overwrite cases follow the default they
assert: EOF now returns true and the prompt reads [Y/n]. Its third case
now scripts an explicit `n` rather than a `y`, since after the flip only
the decline distinguishes an answered prompt from a defaulted one.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

neutral:adopt Foreign PR adopted into neutral's reconcile scope neutral:adopted Adoption completion record: merged while carrying neutral:adopt (LLP 0031) neutral:approved neutral reviewed this and holds it for a maintainer merge (own or adopted PR; LLP 0025/0030)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants