Skip to content

Setup offers the first sync, and two wizard screens get shorter (LLP 0200) - #677

Closed
bgmcmullen wants to merge 4 commits into
masterfrom
wizard-offers-first-sync
Closed

Setup offers the first sync, and two wizard screens get shorter (LLP 0200)#677
bgmcmullen wants to merge 4 commits into
masterfrom
wizard-offers-first-sync

Conversation

@bgmcmullen

@bgmcmullenbgmcmullen commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Two wizard changes, smallest first.

Trim two screens

Overwrite confirm ran 44 words for a y/N. It still says the two things LLP 0183 #say-so requires (the file is rewritten from the picks; what carries over) in 27:

/Users/you/.hyp/hypaware-config.json will be rewritten from your picks. Retention,
exports, hand edits, and plugins the picker doesn't manage are carried over;
a backup is kept. Continue? [y/N]:

The finale's client-asset step printed one line per copy — a dozen destination paths on the last screen of setup. Now:

installed 7 skills and 1 agent for claude
installed 5 skills for codex

Counted per client, not summed: six skills copied to two clients is twelve copies and neither client got twelve. hyp skills install keeps the per-copy output, where the copies are the subject; the paths stay in the run summary and the skills.install span.

Setup offers the first sync (LLP 0200)

LLP 0101's amendment already conceded the case — the hold "blocks the demonstration that the product works at all, and the person running it has no way to say I have seen enough" — and minted hyp sync for it. It stopped one step short: the wizard names the verb in the fifth line of a six-line narration, then hands the terminal to a client. So an enrolled attended run now asks:

Send your recorded history to the server now, or wait?
> Wait until Aug 8, 2026, 11:59 PM PDT
Nothing leaves this machine before then
Send now
Runs `hyp sync`: it lists every destination and asks before sending

Waiting leads and is the default: a mis-keyed wait costs hours of latency, a mis-keyed send cannot be walked back.

The question is an offer, not the consent. "Send now" runs the real hyp sync, which prints the plan, escalates its warning because the window is open, and asks its own y/N — only that y clears the marker. LLP 0100 R2 and LLP 0101 #no-release hold verbatim; there is still exactly one release path in the codebase.

It spawns a child, and that is correctness rather than convenience.hyp init boots all-available, which withholds @hypaware/central (a CLI boot must not acquire a server identity), so this process has no central sink handle — an in-process sync would render a plan missing the very destination the release unblocks, the misleading artifact R2 exists to prevent.

Whether it sent is read back from the hold marker, never inferred from the exit code: hyp sync exits 0 both when it releases and when the user reads the destination list and says no. Any path that didn't send restates the deadline.

Position: narration → offer → first ask. The narration stays the last thing HypAware says about privacy (LLP 0135 #privacy); the first ask stays last on screen because it may take the terminal for good (LLP 0198). Attended-only, TTY-only, and it can never fail a finished install.

Not offered on the aborted-but-enrolled path: an abort means "get me out", not "ask me differently".

Docs

New llp/0200-setup-offers-the-first-sync.decision.md (Draft), with Extended-by: forward-refs on LLP 0101 and LLP 0198.

Testing

  • 7 unit tests for the new step (default is wait; declining spawns nothing; "send now" spawns hyp sync with inherited stdio; a 0-exit child that didn't release reports not-sent; spawn failure and cancellation both degrade to the wait).
  • 2 orchestrator tests (offer sits between narration and first ask; no hold → never asked) and 1 for the per-client asset counts.
  • npm test: 3757/3758. The one failure (resolve emits a hashed usage_policy.fold_tightened…) also fails on a stashed tree — pre-existing, unrelated. Same for the client_attach_idempotent smoke.
  • npm run typecheck clean; walkthrough_picker_to_first_query smoke green.

🤖 Generated with Claude Code

bgmcmullenand others added 2 commits August 7, 2026 15:51
The overwrite confirm ran 44 words for a y/N. It still says the two
things LLP 0183 #say-so requires - that the file is rewritten from the
picks, and what carries over - in 27: the path already reads as "the
config", and the enumeration compresses without losing its reassurance.
The finale's client-asset step printed one line per copy, which on a
two-client install is a dozen destination paths burying the step's one
fact. It now reports counts per client, and counts them per client
rather than summed: six skills copied to two clients is twelve copies,
and neither client got twelve. The materializer keeps its per-copy
output for `hyp skills install`, where the copies are the subject; the
paths stay in the run summary and the skills.install span.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
LLP 0101's amendment already conceded the case: on an attended
onboarding the hold blocks the demonstration that the product works,
and the user had no way to say "I have seen enough". It minted `hyp
sync` for that and stopped one step short - the wizard names the verb
in the fifth line of a six-line narration, then hands the terminal to
a client. A user with no privacy concern has to notice a sentence,
remember a command, and find a terminal setup is giving away.
So an enrolled attended run now asks: wait until the deadline, or send
now. Waiting leads and is the default; the unwanted outcome of a
mis-keyed wait is a few hours' latency, and of a mis-keyed send is
permanent.
The question is an offer, not the consent. "Send now" runs the real
`hyp sync`, which prints the destination plan, escalates its warning
because the window is open, and asks its own y/N - only that y clears
the marker. LLP 0100 R2 and LLP 0101 #no-release hold verbatim: there
is still exactly one release path, and this adds no second one.
It spawns a child rather than running in process, and that is
correctness, not convenience: `hyp init` boots all-available, which
withholds @hypaware/central because a CLI boot must not acquire a
server identity, so this process has no central sink handle. An
in-process sync would render a plan missing the destination the
release unblocks - the misleading artifact R2 requires the plan to
prevent. Whether it sent is read back from the hold marker rather than
from the exit code, which is 0 both for a release and for a user who
read the list and said no.
Position: narration, offer, first ask. The narration stays the last
thing HypAware says about privacy; the first ask stays last on screen
because it may take the terminal for good.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@bgmcmullenbgmcmullen added the neutral:adopt Foreign PR adopted into neutral's reconcile scope label Aug 7, 2026
@philcunliffephilcunliffe added the neutral:adopted Adoption completion record: merged while carrying neutral:adopt (LLP 0031) label Aug 7, 2026
LLP 0200 #read-back settles the direction: "An unreadable re-read is
treated as 'still held', because claiming a sync happened is the one
wrong answer that cannot be corrected later." `readHold` said the same
thing in its own docstring and then did the opposite - it returned
`null` on a failed read, and `null` is the caller's word for "the marker
is gone, it sent". So a state dir that would not resolve ended the run
reporting `released: true`, logging `sync_now: released`, and skipping
the line that restates the deadline: setup's last word on the subject
was a release nobody observed.
A throw from the `readDeadline` seam was worse in a second way - it
escaped to the outer catch and returned `{ asked: false, reason:
'error' }` after the child had already run, so the run ended on silence
with no statement of what still held.
Both now return the deadline the step started from, which reads as
"still held" and prints the restatement.
Co-Authored-By: Claude <noreply@anthropic.com>
@philcunliffe

Copy link
Copy Markdown
Contributor

neutral review - 6449d54 - findings (1 fixed, pushed)

Reviewed the two commits (54c1717 wizard trims, 6449d54 the sync offer) against f0a793f. One correctness finding, fixed and pushed as d168b86. Everything else below is either sound or deliberately left for you.

Finding 1 (High, fixed): an unreadable hold re-read claimed the sync happened

src/core/cli/wizard/sync_now.js:208-216 (at the reviewed head; the return null is line 214).

readHold's own docstring, three lines above the bug, states the rule, and LLP 0200 #read-back settles it: "An unreadable re-read is treated as 'still held', because claiming a sync happened is the one wrong answer that cannot be corrected later." The implementation did the opposite. null is the caller's word for the marker is gone, it sent:

conststillHeld=awaitreadHold(opts)span.setAttribute('released',stillHeld===null)if(stillHeld!==null){ ...notsent... }return{asked: true,released: true}

so catch { return null } reported released: true, logged wizard.finish sync_now: 'released', and skipped writeStillHeld. A run whose state dir would not resolve ended with setup's last word on the subject being a release nobody observed, and with no restatement of the deadline that still applied. That is precisely the false claim this screen exists to be honest about.

The readDeadline seam was wrong in a second, separate way: it sat outside the try, so a throw from it escaped to the outer handler and returned { asked: false, reason: 'error' }after the child had already run - asked: false for a question that was asked, and again no restatement, so the run ended on silence.

Both paths now return the deadline the step started from, which reads as "still held" and prints the restatement.

Worth noting why the tests did not catch this: all 7 pass readDeadline, so the production readHold body has no coverage at all. The added regression test drives the seam to throw, which exercises both defects at once. Proven to fail without the fix: reverting only sync_now.js and re-running the file gives not ok 6 - a re-read that throws is reported as not sent, not as a release (# pass 7 / # fail 1); with the fix, 8/8.

Not fixed, deliberately

  • readFirstSyncDeadline collapses "unreadable" into "absent" (Low).src/core/usage-policy/first_sync_hold.js:160-185 returns null for a genuinely missing marker and for an EACCES/EIO read or a malformed JSON parse. So the narrow case LLP 0200 #read-back names - the marker is there but cannot be read - still reads as a release, and my fix cannot reach it from the caller. Fixing it means changing the contract of a helper the LLP 0101 narration path also depends on, which is a design change owed a new LLP rather than something a review should slip in. Flagging it for a follow-up; the residual risk is small (the release path unlinks the marker rather than rewriting it, so a torn read is unlikely).
  • LLP number 0200 is claimed by three open PRs (maintainer sequencing).Setup offers the first sync, and two wizard screens get shorter (LLP 0200) #677 (this one), A picker row can be hidden from the menu without ceasing to be a source (LLP 0200) #674, and New folders sync by default; one gate accepts the lot (LLP 0200/0201) #675 all add a different llp/0200-*.md. origin/master has nothing above 0199, so duplicate-numbers passes for each in isolation and will start failing for the second and third to merge. Renumbering is mechanical and allowed (LLP 0156), but which doc keeps 0200 is your call, not mine to guess.

Checked and sound

  • LLP discipline. LLP 0200 is Draft, so it is the right place for this design. The only edits to Accepted docs are the Extended-by: forward-refs on LLP 0101 and 0198 - explicitly permitted editorial changes, and no accepted text is contradicted. Every anchor referenced from code resolves: 0200#offer, #child-process, #read-back, plus 0100#requirements, 0101#no-release, 0135#privacy, 0198#first-ask, 0190#abort-narration.
  • The no-second-release-path claim holds. "Send now" spawns bin/hypaware.js sync and clears nothing itself; the marker is only cleared by the child's own y/N. LLP 0100 R2 and LLP 0101 #no-release are intact - I looked for a shortcut and there isn't one.
  • The child-process argument is correct, not a rationalization.hyp init boots all-available, which withholds @hypaware/central, so an in-process commands.run('sync') really would render a plan missing the destination the release unblocks. The ../../../../bin/hypaware.js URL resolves to the repo root correctly.
  • Gating matches the runWizardFirstAsk precedent exactly (attended, non-cancelled, non-dry-run), plus a real TTY check on both ends and a live hold. Ordering is narration to offer to first ask, as LLP 0200 requires, and nothing needing the terminal follows the first ask.
  • Wait-first polarity is the defensible one and is tested.
  • clientAssetCountLines is safe: ClientAssetKind is exactly 'skill' | 'agent', so the else branch cannot mis-bucket. stderr is still threaded, so the warnings that matter survive dropping stdout; only the per-copy lines go, and hyp skills install keeps them. The per-client-not-summed reasoning is right.
  • Overwrite confirm still satisfies LLP 0183 #say-so in its shorter form: it states the file is rewritten from the picks and names what carries over.
  • Conventions: no semicolon-terminated statements, no U+2014 anywhere in the diff, no smart quotes, no @typedef, no inline import('...'), type-import specifiers root-anchored. The { settled = true; resolve(r) } separator matches existing house style (src/core/dep_graph.js:80), so not raised.

Checks

npm test 3755 pass / 0 fail / 6 skipped (3761 tests, +1 from the regression test); npm run typecheck clean. The pre-existing usage_policy.fold_tightened failure the description mentions did not reproduce here. GitHub checks on 6449d54 were all green.

Fix verified by reading the committed remote blob at origin/wizard-offers-first-sync (d168b86), not by inference from a green suite.

Not merging or approving - this is a contributor PR and terminates in your decision.

Every test of `runWizardSyncNow` replaced the hold re-read with the
`readDeadline` seam, so the body that actually decides the release claim -
resolve the state dir from the environment, read the marker there - had no
coverage at all. That is the body a wrong answer cannot be walked back in
(LLP 0200 #read-back), and the blind spot is what let the earlier
`catch { return null }` report a release nobody observed.
Two tests drive it end to end against a real marker on disk with no seam:
a spawn stub that clears the marker (a release) or leaves it (a declined
plan). Proven to bite: dropping `opts.env` from the state-dir resolution -
the drift that would make every run claim a release - fails only these two
and leaves all eight pre-existing tests green.
Co-Authored-By: Claude <noreply@anthropic.com>
@philcunliffe

Copy link
Copy Markdown
Contributor

neutral review (round 2, final) - d168b86 - clean on correctness, one coverage gap closed

Round 2 caps the ladder, so this record is written to be disposed of by a human without re-deriving anything.

I reviewed the current head d168b86 (which contains round 1's own fix) with the specific brief of distrusting that fix. It holds. I found no new correctness defect. I did find that the code path the fix repaired has zero automated coverage, verified the gap empirically, and closed it. Pushed as 6827735.

Round 1's fix, re-checked

readHold now reads:

asyncfunctionreadHold(opts){try{if(opts.readDeadline)returnawaitopts.readDeadline()conststateDir=readObservabilityEnv(opts.env).stateDirreturnawaitreadFirstSyncDeadline({ stateDir })}catch{returnopts.deadline}}
  • Does widening the try change any other throw site? No. The only statement pulled inside is the readDeadline seam, which production never passes (index.js supplies deadline/stdout/stderr/env/interactive/stdin and nothing else). Its previous escape to the outer handler returned asked: falseafter the child had run, which was wrong; nothing else routed through it.
  • Is null still overloaded further along? The remaining overload is readFirstSyncDeadline's own fail-open contract, which is round 1's deferred Low and is restated below. Inside sync_now.js the caller's stillHeld === null now has exactly one meaning on every reachable path.
  • Adjacent paths with the same shape? I walked all of them: the declined early return, the cancelled-prompt branch, spawn-failed, runSyncChild's error/close/throw seams, narratePrivacyIfTeamPath's null, and clientAssetCountLines. None turns an error into a success-shaped value. Two notes on the near-misses, both benign:
    • materializeClientAssetscontinues past a failed copy without pushing, so the new "installed N skills for X" counts cannot include a copy that did not land. The counts are honest.
    • narratePrivacyIfTeamPath returning null for an unreadable state dir now also suppresses the offer. That errs toward silence, never toward a claimed release, and the catch predates this PR.

Finding 1 (Medium, fixed): the read-back's production body had never been executed by a test

src/core/cli/wizard/sync_now.js:214-222, covered from test/core/cli/wizard/sync_now.test.js.

All eight tests on d168b86 - including round 1's own regression test - pass readDeadline, which short-circuits the first line of readHold. So the two lines that actually decide the release claim in production (resolve the state dir from the environment, read the marker there) had no coverage at all. That is the one place in this feature where a wrong answer cannot be walked back, and the absence of coverage is precisely what let the original catch { return null } sit there unnoticed.

This is not theoretical. I proved the blind spot by mutating the production line to a plausible drift - dropping the threaded env, readObservabilityEnv(opts.env) to readObservabilityEnv() - which makes the re-read look in the caller's real home, find no marker, and report released: true for every run:

ok 1..7 (all pre-existing tests, including "a re-read that throws is reported as not sent")
not ok 8 - the read-back resolves a real hold marker from the environment: left in place
# pass 9 / # fail 1

Every pre-existing test stayed green against a wizard that falsely claims it sent your history. Only the added tests see it.

The fix is test-only: two cases drive runWizardSyncNow end to end with no seam, against a real first-sync-hold.json under a temp HYP_HOME, with a spawn stub standing in for hyp sync that either clears the marker (a release) or leaves it (a declined plan). Both assert the result and the presence/absence of the restatement line. Reverted mutation, 10/10 green.

Before landing it I also confirmed by direct execution that the production body is correct today: with a real marker present it returns { asked: true, released: false, reason: 'sync-declined' } and prints Nothing was sent. Your history stays on this machine until ...; with the marker cleared it returns { asked: true, released: true } and prints nothing. So this commit locks in behaviour that already works, rather than repairing anything.

Not fixed, deliberately

  • A failed re-read is reported as sync-declined (Low).src/core/cli/wizard/sync_now.js:102-107. Round 1's fix makes an unreadable re-read return a deadline, which is the correct user-facing answer (still held, deadline restated). But the caller then labels it sync-declined, and LLP 0200 #consequences defines that value as the user reading the destination list and backing out - it names the declined/sync-declined split as the measurement that says whether the window is sized right. The span also keeps status: 'ok' with no error_kind, against the Log-Driven Development rule in CLAUDE.md. I left it: the path is very close to unreachable in production (see the next item - readFirstSyncDeadline is documented never to throw, and readObservabilityEnv is pure path.join over env strings), and distinguishing it means widening the public WizardSyncNowResult union and amending LLP 0200's enumerated sync_now values. That belongs with the follow-up below, not in a review round.
  • readFirstSyncDeadline collapses "unreadable" into "absent" (Low, round 1's deferral, unchanged).src/core/usage-policy/first_sync_hold.js:159-184. Its fail-open polarity is deliberate and documented (LLP 0093 #bounded), so the narrow case LLP 0200 #read-back names still reads as a release at that layer, and sync_now.js cannot reach it from the caller. Changing that contract touches the LLP 0101 narration path and is owed a new LLP. Confirmed still true at d168b86; not touched.
  • readHold's catch { return opts.deadline } is type-safe only by a distant invariant (Low, latent).opts.deadline is number | null, so the catch can type-check while returning null - the exact High defect round 1 fixed. It is safe today only because the function is unreachable unless the typeof opts.deadline !== 'number' early return at line 57 has already fired. tsc will not catch it if that guard ever moves. Noted rather than churned, since narrowing the helper's signature is a refactor with no present bug behind it.
  • LLP number 0200 is claimed by three open PRs (maintainer sequencing).Setup offers the first sync, and two wizard screens get shorter (LLP 0200) #677 (this one), New folders sync by default; one gate accepts the lot (LLP 0200/0201) #675 (0200/0201) and A picker row can be hidden from the menu without ceasing to be a source (LLP 0200) #674 each add a different llp/0200-*.md; origin/master still tops out at 0199, so duplicate-numbers passes for each in isolation and starts failing for the second and third to merge. Renumbering is mechanical and permitted (LLP 0156), but which doc keeps 0200 is a maintainer call.
  • Ctrl-C at the child's y/N kills setup (observation, not a finding).stdio: 'inherit' puts the child in the caller's foreground process group, and no SIGINT handler exists on the CLI path, so ctrl-C at the hyp sync prompt terminates hyp init too - no restatement line, no first ask, exit 130. This is inherent to the stdio: 'inherit' pattern LLP 0198 #real-launch already established and which LLP 0200 #child-process explicitly inherits; typing n (the ordinary way to decline) behaves correctly. Raised only so it is on the record.

Checked and sound

  • The child-process argument, re-verified.bin/hypaware.js resolves correctly from ../../../../ at src/core/cli/wizard/, is spawned unscoped (['sync']) so LLP 0100 R2 permits the release, and clears nothing itself. There is still exactly one release path in the codebase.
  • Gating.holdDeadline !== null && interactive && !cancelled && dryRun !== true, plus a real TTY check on both ends inside the step. ctx.stdin is process.stdin in production (dispatch.js:205) and the guided-wizard entry already gates on isTty(ctx.stdout), so the offer genuinely appears on a real attended run and genuinely does not on a piped one. Ordering is narration to offer to first ask, and nothing needing the terminal follows the ask.
  • clientAssetCountLines.ClientAssetKind is exactly 'skill' | 'agent' so the else cannot mis-bucket; parts can never be empty because the map is built from the items; failed copies are excluded upstream; stderr warnings still survive; hyp attach and hyp skills install keep their per-copy lines (clients.js:989).
  • Overwrite confirm still satisfies LLP 0183 #say-so in 27 words: it states the file is rewritten from the picks, names what carries over, and names the backup.
  • LLP discipline. LLP 0200 is Draft. The only edits to Accepted docs are the Extended-by: forward-refs on 0101 and 0198, which are the permitted editorial change and contradict no accepted text. Every anchor referenced from code resolves: 0200#offer, #child-process, #read-back, #no-new-consent, plus 0100#requirements, 0101#no-release, 0135#privacy, 0198#first-ask.
  • Conventions. No statement-terminating semicolons, no U+2014, no smart quotes, no @typedef, no inline import('...'), type-import specifiers root-anchored - checked over the full diff including my commit.

Checks

npm test3757 pass / 0 fail / 6 skipped (3763 tests, +2 from this round; baseline on d168b86 was 3755/0/6). npm run typecheck clean. The usage_policy.fold_tightened failure the PR description calls pre-existing did not reproduce in either run.

The pushed commit was verified by reading the committed remote blob at origin/wizard-offers-first-sync (6827735), not inferred from a green suite.

No approval, no verdict label, no merge - this is a contributor PR and terminates in a maintainer decision.

@philcunliffephilcunliffe added the neutral:changes-requested neutral reviewed an adopted PR and requests changes (non-binding; maintainer decides) label Aug 8, 2026

@philcunliffephilcunliffe left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review capacity for this PR is spent (2 rounds, the ladder's cap), and the current head 6827735 carries neutral's own round-2 work, which has not itself been reviewed. Handing the remainder back rather than deferring it - this is your PR, so the residual calls are yours.

Round 1 (head 6449d54) found one High and fixed it.src/core/cli/wizard/sync_now.jsreadHold had a catch { return null }, where null is the caller's word for "marker gone, it sent". An unreadable re-read therefore ended setup reporting the sync as released, logging it released, and skipping the deadline restatement - a false claim about the one thing that screen exists to be honest about. The function's own docstring and LLP 0200 #read-back both settle the opposite. A second defect in the same function had the readDeadline seam outside the try, so a throw returned asked: false after the child had already run.

Round 2 (head d168b86) re-examined that fix and it holds. Checked three ways: widening the try pulls in only the test seam production never passes; null is no longer overloaded on any reachable path in the file; and every adjacent seam (declined, cancelled-prompt, spawn-failed, runSyncChild's three seams, narratePrivacyIfTeamPath, clientAssetCountLines) was walked for the same "error collapses into a success-shaped value" defect, with none found.

What round 2 did find is a coverage gap, and it is worth your attention. The production body of readHold (sync_now.js:214-222) had zero test coverage - all eight tests, including round 1's own regression test, inject the readDeadline seam that short-circuits it. Demonstrated by mutation: changing readObservabilityEnv(opts.env) to readObservabilityEnv() (a plausible drift) makes the wizard report released: true for every run - a false "your history is on its way" - and all eight pre-existing tests stayed green. Two seam-free tests now drive a real hold marker under a temp HYP_HOME; only they catch it. The production body is correct today, so this locks in working behaviour rather than repairing a live bug.

Suite at the current head: 3757 passing, 0 failing; typecheck clean.

What needs your decision:

  1. A failed re-read is labelled sync-declined, which corrupts the measurement LLP 0200 #consequences names. Low and near-unreachable, but fixing it widens a public union and amends the LLP, so it is not a review-time edit.
  2. first_sync_hold.js:160-185 collapses "unreadable marker" into "absent" - the exact case LLP 0200 #read-back names still reads as a release at that layer. Unreachable from the caller-side fix; changing that helper's contract touches the LLP 0101 narration path and is owed a new LLP.
  3. readHold's catch { return opts.deadline } is type-safe only via a distant guard - Low, latent, worth a local assertion if you touch this again.
  4. ctrl-C at the child's y/N kills setup - inherent to the LLP 0198 stdio: 'inherit' pattern. Observation only, no action proposed.
  5. The current head is unreviewed.6827735 is neutral's own round-2 work.

Cross-PR conflict that no single PR's checks can see.origin/master stops at LLP 0199, and three in-flight PRs each add a differentllp/0200-*.decision.md:

  • #6740200-hidden-picker-rows.decision.md
  • #6750200-folder-ask-is-a-preference.decision.md
  • #677 (this one) 0200-setup-offers-the-first-sync.decision.md

duplicate-numbers passes for each in isolation and will fail for the second and third to merge. Renumbering is mechanical under LLP 0156, but which doc keeps 0200 is your call, so neutral has not renumbered anyone's branch.

Both review records are in this thread with full file:line evidence. Nothing here is a known production defect - this is an exhausted-ladder handback.

Push to the branch or reply here and neutral will re-engage on its next tick.

bgmcmullen added a commit that referenced this pull request Aug 8, 2026
…, LLP 0200 collision renumbered, cross-PR seams closed (#679)
* A picker row can be hidden from the menu without ceasing to be a source (LLP 0200)
The `raw-anthropic` / `raw-openai` wizard rows cost every first-run user a
question and earn it back for almost nobody. Beside a client row they compose
byte-identical config (claude already contributes the `anthropic` upstream,
codex `openai` + `chatgpt`, and the fold dedupes by name), so their checked
state is cosmetic. Alone they capture nothing at all: projection is
adapter-owned, `@hypaware/ai-gateway` registers no projector of its own, and
the default `config` boot profile activates only what the config names - so a
raw-only install proxies traffic correctly and writes zero rows.
Add `hidden` to the `contributes.picker` contract rather than a row-id list in
core, which would undo what LLP 0130 settled about who owns the picker table.
`visiblePickerDescriptors` is the one display filter, used by the legacy
prompt and by the wizard's gate and menu.
Hiding is a display filter, never a catalog deletion. The owner map arming
LLP 0192's fail-closed withholding is folded from picker descriptors, so
deleting the two `contributes.picker` blocks would give `ai_gateway_messages`
an empty owner list - which both withhold rules read as "never withhold",
turning off a privacy guard under cover of a UI cleanup. The descriptors stay.
Carry-through is narrower than it first looks. A hidden row rides through the
selection only when the config on disk collects nothing the menu can show (a
`--source raw-anthropic` install being reconfigured). Carrying on seed
membership was tried and is wrong: seeding is derivative for these rows -
`raw-openai` reads as configured whenever codex's `openai` upstream is present
- so it resurrected that upstream the moment someone unchecked codex. The
existing "unchecking a row still removes its gateway upstream" test caught it.
What this does not fix: raw traffic still needs an adapter's projector to be
recorded, and generic Anthropic-dialect traffic is still stamped
`client_name: 'claude'` by the LLP 0115 fallback. Both are projector
behaviour, independent of any picker row, and belong to LLP 0192's deferred
attribution decision. Filed as #673.
npm test: 3728 pass, 1 fail (`usage_policy.fold_tightened`, pre-existing on
master). typecheck clean. Smokes green: walkthrough_picker_to_first_query,
cli_bundled_plugins_activated, status_diagnostics.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* New folders sync by default; one gate accepts the lot (LLP 0200/0201)
Two changes to the same complaint: setup asks too much, and one of its
questions comes back every session.
LLP 0200 makes the per-folder classification ask (LLP 0106) opt-in. A
machine-local preference decides whether an unclassified folder is asked
about at session start, and its default is `sync` - the folder keeps the
implicit default it already had and nobody is interrupted. The ask still
exists for people who want it, chosen in the wizard's new-folder step or
with `hyp policy folders ask`. This gates the question only: no class
moves, `.hypignore` is untouched, and nothing new leaves a machine that
was not already syncing by default. `hyp status` names the mode on every
enrolled run so it is never a silent state, and the classification prompt
itself now teaches its own off switch.
The new-folder question is its own wizard step, after the per-adapter
sync lane rather than a row on it: that lane answers "which adapters
ship" and this one answers "what happens the next time I work somewhere
new", and folding the second into the first let a checklist decide a
policy silently.
LLP 0201 adds an express gate before the question lanes: it lists what
was found and offers "Record and sync all of these" or "Let me choose".
Accepting skips the prompts, never the statements - each lane still
narrates the block its gate would have shown, so the fast path prints the
same rows in the same order, minus the keypresses. The rows come from the
pick lane's own computation (hoisted to `resolvePickSeeding`), with
detection run once per wizard run, so the two screens cannot disagree
about what "all of these" means. With nothing detected and nothing locked
there is nothing to accept, so no gate is shown.
Also: the overwrite confirm is laid out instead of run together as one
paragraph, same facts and order.
npm test: 3765 pass, 1 fail (usage_policy.fold_tightened, identical on a
clean checkout). npm run typecheck: clean. Smokes
walkthrough_picker_to_first_query, status_diagnostics,
cli_bundled_plugins_activated, daemon_foreground_start_stop: green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Review fixes: carry-through is scoped to a chosen seed, and the docs stop offering the hidden rows
Three review findings on the `hidden` picker field.
1. Carry-through could fire off a DETECTION seed. `promptPickSelection`
read `carried` from `seed`, but `seed` is `initialSelection ?? config
?? detected`. LLP 0200 #carry-through scopes carry-through to "the
config on disk collects nothing the menu can show"; off a detection
seed it would instead compose a hidden row the user was never shown
and cannot uncheck, which is what LLP 0011 #autodetect-vs-default
forbids and what `pick.js`'s own @ref to it promises. Unreachable
today only because no bundled hidden row declares a `detect` probe,
and `hidden` is a kernel-contract field any plugin can set beside
one. `seedIsChosen` now gates it, and a test drives detection
directly to pin it.
2. README and CONTEXT.md still listed `raw-anthropic` / `raw-openai`
among the rows the interactive walkthrough offers. Both now say the
rows are `--source`-only and why.
3. `picker-disclosure.test.js` justified its raw-row assertions with
"these two rows carry no adapter to disclose it anywhere else",
which stopped being true when the menu (the only consumer of
`summary`) stopped rendering them. Comment corrected; the
assertions stay, because hiding is a display filter, not a deletion.
Co-Authored-By: Claude <noreply@anthropic.com>
* Script detection in the wizard end-to-end so the express gate always shows
CI failed on `runInitWizard end-to-end`: it read the real machine for
detection, and the express gate (LLP 0201) is shown only when there is
something to accept. A developer laptop with Claude installed gets the
gate; a CI runner with no AI clients does not, so every scripted answer
after the fork landed on the wrong prompt and the run finished on the
team pathway it was supposed to step back out of.
The rows themselves were never what the test was about ('all' picks
whatever the menu offers); that there *are* rows is. So detection is
injected like every other seam in that call, and the script is stable on
any host.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Trim two wizard screens: the overwrite confirm, and the asset install
The overwrite confirm ran 44 words for a y/N. It still says the two
things LLP 0183 #say-so requires - that the file is rewritten from the
picks, and what carries over - in 27: the path already reads as "the
config", and the enumeration compresses without losing its reassurance.
The finale's client-asset step printed one line per copy, which on a
two-client install is a dozen destination paths burying the step's one
fact. It now reports counts per client, and counts them per client
rather than summed: six skills copied to two clients is twelve copies,
and neither client got twelve. The materializer keeps its per-copy
output for `hyp skills install`, where the copies are the subject; the
paths stay in the run summary and the skills.install span.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Setup offers the first sync instead of only naming it (LLP 0200)
LLP 0101's amendment already conceded the case: on an attended
onboarding the hold blocks the demonstration that the product works,
and the user had no way to say "I have seen enough". It minted `hyp
sync` for that and stopped one step short - the wizard names the verb
in the fifth line of a six-line narration, then hands the terminal to
a client. A user with no privacy concern has to notice a sentence,
remember a command, and find a terminal setup is giving away.
So an enrolled attended run now asks: wait until the deadline, or send
now. Waiting leads and is the default; the unwanted outcome of a
mis-keyed wait is a few hours' latency, and of a mis-keyed send is
permanent.
The question is an offer, not the consent. "Send now" runs the real
`hyp sync`, which prints the destination plan, escalates its warning
because the window is open, and asks its own y/N - only that y clears
the marker. LLP 0100 R2 and LLP 0101 #no-release hold verbatim: there
is still exactly one release path, and this adds no second one.
It spawns a child rather than running in process, and that is
correctness, not convenience: `hyp init` boots all-available, which
withholds @hypaware/central because a CLI boot must not acquire a
server identity, so this process has no central sink handle. An
in-process sync would render a plan missing the destination the
release unblocks - the misleading artifact R2 requires the plan to
prevent. Whether it sent is read back from the hold marker rather than
from the exit code, which is 0 both for a release and for a user who
read the list and said no.
Position: narration, offer, first ask. The narration stays the last
thing HypAware says about privacy; the first ask stays last on screen
because it may take the terminal for good.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Review fixes round 2: a carried hidden row survives stepping back into pick
Carry-through re-tested its own precondition on every pass. A re-entry
seeds with the selection the previous pass confirmed (LLP 0191
#re-entry-seeding), which for a raw-only install holds the carried hidden
row beside whatever visible rows the user just added. Asking "does the
seed collect nothing the menu can show?" of that seed answers no, so the
carried row was dropped: `back` then `enter` deleted the gateway upstream
the install runs on, silently.
Replaces the boolean `seedIsChosen` with a `SeedOrigin` the seed carries
alongside it. `detected` never carries (unchanged), `config` carries only
when the menu can show nothing the config collects (unchanged), and
`selection` carries unconditionally: read-back never reaches that tier,
so a hidden id is in a previous answer only because this rule put it
there. LLP 0200 #carry-through records the re-entry case.
Also pins the `hidden` manifest validation branch, which shipped
untested.
Co-Authored-By: Claude <noreply@anthropic.com>
* The express gate joins the forward chain but not the back chain
Escape at the pick lane reached past the new gate to the fork, and escape
at the sync lane landed on the gate instead of the picker: the question was
inserted between the fork and the pick lane without being given a loop
level, so both of its neighbours overshot by one screen. LLP 0191
#back-edges is that escape steps exactly one screen back.
The gate now owns `atExpress` between `atFork` and `atPick`. Pick backs to
it when the pass showed one and to the fork when it did not (LLP 0201
#no-default-no-accept), and a sync back re-runs the picker without
re-asking it. Three tests pin the chain; the end-to-end walk gains the
keypress the corrected edge needs.
Also corrects three doc blocks left over from a draft where `ask` was the
default. `src/core/usage-policy/types.d.ts` and `src/core/daemon/types.d.ts`
ship in the published declaration build, so they were telling consumers the
opposite of both the code and LLP 0200 #default about a privacy-relevant
default; `usage-policy/index.js` said the same and attributed the write to
the sync lane, which LLP 0200 #wizard explicitly rejects. Its export block
had also been spliced between the client-sync comment and the export that
comment describes.
`hyp policy list` no longer drops "no machine-local entries (policy store:
...)" on a store that is empty while the folder mode is non-default; the
mode is an addition to that line, not a replacement for it.
Co-Authored-By: Claude <noreply@anthropic.com>
* An unreadable hold re-read claimed the sync happened
LLP 0200 #read-back settles the direction: "An unreadable re-read is
treated as 'still held', because claiming a sync happened is the one
wrong answer that cannot be corrected later." `readHold` said the same
thing in its own docstring and then did the opposite - it returned
`null` on a failed read, and `null` is the caller's word for "the marker
is gone, it sent". So a state dir that would not resolve ended the run
reporting `released: true`, logging `sync_now: released`, and skipping
the line that restates the deadline: setup's last word on the subject
was a release nobody observed.
A throw from the `readDeadline` seam was worse in a second way - it
escaped to the outer catch and returned `{ asked: false, reason:
'error' }` after the child had already run, so the run ended on silence
with no statement of what still held.
Both now return the deadline the step started from, which reads as
"still held" and prints the restatement.
Co-Authored-By: Claude <noreply@anthropic.com>
* Escape at the new-folder lane traps a fleet-managed machine (LLP 0200/0201)
The sync lane asks nothing when everything picked is fleet-locked, and
nothing when the client store is corrupt: it states its outcome and
returns. The new-folder lane behind it backed unconditionally into it, so
escape re-ran the statement and re-asked the same question. On a fully
managed machine that is a trap with no exit but ctrl+c.
The lane now reports `noQuestion` on both non-prompting returns and the
orchestrator steps past it to the picker, which is the last screen the user
could answer.
Same class one step earlier: the express gate lists the picker's own
confirmed defaults, so a confirmed empty selection leaves it nothing to
show. A back out of the re-entered picker landed on a gate that could not
render and fell forward into the picker again. It now reaches the fork.
Also: the pick lane re-derived `defaultRows` instead of taking the
`resolvePickSeeding` value the express gate reads, which is the second
derivation of "the defaults" LLP 0201 #gate says must not exist.
Co-Authored-By: Claude <noreply@anthropic.com>
* The sync read-back's production body was never run by a test
Every test of `runWizardSyncNow` replaced the hold re-read with the
`readDeadline` seam, so the body that actually decides the release claim -
resolve the state dir from the environment, read the marker there - had no
coverage at all. That is the body a wrong answer cannot be walked back in
(LLP 0200 #read-back), and the blind spot is what let the earlier
`catch { return null }` report a release nobody observed.
Two tests drive it end to end against a real marker on disk with no seam:
a spawn stub that clears the marker (a release) or leaves it (a declined
plan). Proven to bite: dropping `opts.env` from the state-dir resolution -
the drift that would make every run claim a release - fails only these two
and leaves all eight pre-existing tests green.
Co-Authored-By: Claude <noreply@anthropic.com>
* Renumber 674's LLP 0200 to 0202 (folder-ask kept 0200; mechanical, LLP 0156)
* Renumber 677's LLP 0200 to 0203 (mechanical, LLP 0156)
* Restore 677's per-client asset counts dropped by the confirm-copy resolution
* Add cross-PR regression test: express auto-accept honors hidden-row carry-through
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: test <test@test.com>
Co-authored-by: neutral <neutral@hyparam.com>
Co-authored-by: neutral <neutral@hyperparam.app>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

neutral:adoptForeign PR adopted into neutral's reconcile scopeneutral:adoptedAdoption completion record: merged while carrying neutral:adopt (LLP 0031)neutral:changes-requestedneutral reviewed an adopted PR and requests changes (non-binding; maintainer decides)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@bgmcmullen@philcunliffe