Skip to content

A picker row can be hidden from the menu without ceasing to be a source (LLP 0200) - #674

Closed
bgmcmullen wants to merge 3 commits into
masterfrom
hidden-picker-rows
Closed

A picker row can be hidden from the menu without ceasing to be a source (LLP 0200)#674
bgmcmullen wants to merge 3 commits into
masterfrom
hidden-picker-rows

Conversation

@bgmcmullen

@bgmcmullenbgmcmullen commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Drops the raw-anthropic / raw-openai rows from the setup wizard, via a new hidden field on the picker contract.

Why

The two rows cost every first-run user a question and earn it back for almost nobody:

  • Beside a client row they compose nothing.claude already contributes the anthropic upstream, codex contributes openai + chatgpt, and the picker fold dedupes upstreams by name. LLP 0183 already named the consequence: the row reads back as configured whenever its upstream is, so its checked state is cosmetic.
  • Alone they capture nothing. Projection into ai_gateway_messages is adapter-owned - registerExchangeProjector is called only by the claude, codex, and openclaw plugins, never by the gateway itself. The default boot profile is config (boot.js:571), which activates only what the config names. So a raw-only install proxies traffic to the provider correctly, counts aigw.exchange_bytes, warns aigw.message_projection_skipped / no_projector_match per exchange, and writes zero rows.

Confirmed with hyp init --yes --source raw-anthropic --dry-run: composes gateway + local-fs + format-parquet, no adapter.

So the rows are redundant in the common case and non-functional in the case they exist for.

What changed

contributes.picker rows may set hidden: true. A hidden row is absent from the interactive menu and the defaults gate, and is otherwise a picker source in every respect: --source <id> still composes it, configuredPickerSources still reads it back, and its id keeps its opt-out/sync identity.

  • hypaware-plugin-kernel-types.d.ts, src/core/types.d.ts - hidden?: boolean
  • src/core/manifest.js - rejects a non-boolean
  • src/core/plugin_catalog.js - carries it onto the descriptor
  • ai-gateway/hypaware.plugin.json - both raw rows set it
  • walkthrough.js - visiblePickerDescriptors(), the single display filter

Adding a manifest field rather than a row-id list in core is deliberate: a hardcoded list would undo what LLP 0130 settled about who owns the picker table.

Two things worth reviewer attention

Hiding is a display filter, never a catalog deletion.datasetOwnedSourceIdsFromCatalog folds picker descriptors into the dataset-owner map that arms LLP 0192's fail-closed withholding. Deleting the two contributes.picker blocks - which looks like the same change - would give ai_gateway_messages an empty owner list, and both withhold rules read that as "never withhold". A privacy guard would switch off under cover of a UI cleanup. That's written into the visiblePickerDescriptors doc comment and into LLP 0200 so the next person doesn't finish the cleanup.

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). My first version carried any seeded hidden row, and the existing unchecking a row still removes its plugin and its gateway upstream test caught it: 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.

Not fixed here

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, polluting Claude-attributed queries and reports. Both are projector behaviour, independent of any picker row, and belong to LLP 0192's deferred attribution decision. Filed as #673.

Docs

LLP 0200 records the decision; Extended-by: forward-refs added to LLP 0130 and LLP 0012.

Checks

CI green: test and typecheck on Node 22 and 24, plus duplicate-numbers.

Locally: npm run typecheck clean; smokes green for walkthrough_picker_to_first_query, cli_bundled_plugins_activated, status_diagnostics. Two tests fail on this machine and pass in CI - resolve emits a hashed usage_policy.fold_tightened... and the client_attach_idempotent smoke - both of which also fail on an unmodified master checkout here, so they look environment-specific rather than related to this change.

Four new tests cover hidden-row behaviour: absent from gate and menu, raw-only config survives a reconfigure, derivative seeding does not resurrect an unchecked upstream, and --source still composes.

Note for #647

Rebased onto master after #647, keeping its new raw-row copy ("starts a local gateway listener") alongside the hidden flag - the manifest conflict was only that the two edits landed on adjacent lines. Worth flagging to that PR's author: a hidden row's summary no longer renders anywhere, since the picker menu was its only consumer. The copy stays accurate and costs nothing, but the disclosure it adds is now unreachable for these two rows specifically.

🤖 Generated with Claude Code

…ce (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>
@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
…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>
@philcunliffe

Copy link
Copy Markdown
Contributor

neutral review round: PR #674 @ 13f1680

Verdict: findings (3) — all fixed and pushed as 27bf6c0.

The core design is right and well argued. hidden as a manifest field rather than a core row-id list is the correct read of LLP 0130, and it matches an existing convention in this codebase (core_commands.js:652 sets hidden: true on a command; dispatch.js:726 and group_help.js:40 filter on it). Keeping the descriptors in the catalog rather than deleting the contributes.picker blocks is the load-bearing call — source_withhold.js:262 folds picker descriptors into the dataset-owner map, and an empty owner list reads as "never withhold" in both rules, so the deletion would have disarmed LLP 0192's fail-closed guard. That is correctly identified, correctly avoided, and documented in three places.


1. [Medium] Carry-through could fire off a detection seed, not just the config on disk

src/core/cli/wizard/pick.js:430-433 (pre-fix)

constseededVisible=visibleList.some((d)=>seed.has(d.id))constcarried=seededVisible
? []
: descriptorList.filter((d)=>d.hidden===true&&seed.has(d.id)).map((d)=>d.id)

carried is derived from seed, but seed is three-tiered (pick.js:145-147): initialSelection ?? configured ?? detected. On a first run there is no config, so seedis the detection result.

The comment directly above (pick.js:417-421) and LLP 0200 §carry-through both scope this to "the config on disk collects nothing the menu can show". The code says something wider: "the seed has no visible row". When those diverge, a hidden row carrying a detect probe gets composed into the config having never been rendered and with no way for the user to uncheck it — detection forcing a source on. That is exactly what pick.js:120's own @ref LLP 0011#autodetect-vs-default promises never happens.

Unreachable against the bundled catalog only because neither raw row declares a detect probe. But hidden lands on PluginPickerContribution (hypaware-plugin-kernel-types.d.ts:286) as a general kernel-contract field, so any plugin can set it beside a detect block, and the failure mode is silent capture.

Fixed:pick.js now computes seedIsChosen (Boolean(opts.initialSelection) || configured !== undefined) beside seed and threads it into promptPickSelection; carried is gated on it. This makes the code match the sentence LLP 0200 already settled, so no LLP change was needed. Pinned by a new test that drives detect directly (test/core/cli/wizard/pick.test.js:1078); verified it fails on the pre-fix pick.js and passes after.

2. [Medium] User-facing docs still advertise the two rows as menu choices

  • README.md:51-58 — "On a TTY this launches the interactive walkthrough: 1. Pick the sources to capture. Any subset of: … Raw Anthropic API traffic (raw-anthropic) / Raw OpenAI API traffic (raw-openai)". After this PR the TTY menu never renders either.
  • CONTEXT.md:11-13 — "In the first-run wizard the user-facing sources are claude, codex, raw-anthropic, raw-openai, and otel."

The LLP forward-refs (0012, 0130) were added, but CLAUDE.md's "Living docs … don't leave stale guidance" covers the prose docs too, and README is the first thing a user reads.

Fixed: README step 1 now lists the three rendered rows and adds a short paragraph on why the raw rows are --source-only; CONTEXT.md's Source glossary distinguishes picker sources from menu rows and the Raw proxy source bullet says how to reach them. Left hypaware-reference/SKILL.md and docs/PRIVACY.md alone — both talk about hyp init and data classes generally, which stay accurate.

3. [Low] picker-disclosure.test.js now guards copy nothing renders, under a rationale that stopped being true

test/plugins/picker-disclosure.test.js:87-97 justifies the raw-row assertions with "these two rows carry no adapter to disclose it anywhere else" — but the picker menu was summary's only consumer, so hiding the rows makes the listener disclosure unreachable for exactly these two. The PR body flags this and leaves it. The test still passes; the comment is now false.

Fixed: comment corrected to record that the copy currently reaches nobody and why the assertions are kept anyway (hiding is a display filter, so un-hiding a row must not be when the disclosure is discovered missing). No assertion changed.


Not fixed — needs an author/maintainer call

[Low] A fleet-locked hidden row disappears from the defaults gate.pick.js:437 narrowed defaultRows from descriptorList to visibleList, so a hidden row in lockedSet is no longer listed under "HypAware will record:". Functionally contained (locked ids are filtered from composition at pick.js:215 and reported via lockedSources regardless), but it is a disclosure gap against LLP 0031#status-provenance, which says a locked row renders with the fleet-managed label "rather than silently". Deliberately not patched: LLP 0200 §hidden-rows is Accepted and settles that a hidden row is absent from the gate, and per CLAUDE.md an accepted doc is changed by a new LLP, not by contradicting code. Unreachable today (nothing locks the raw rows). Flagging so the author can decide whether "hidden" should yield to "locked".

[Info, pre-existing, out of scope]README.md step 2 still describes an interactive export question, but export has not been asked interactively for a while (walkthrough.js:659-665, pick.js:194-200). Not caused by this PR; noting it since the neighbouring step was being edited.

Checked and clean

  • Style: no semicolons, no U+2014 anywhere in the touched files, JSDoc-only types, no inline import('...'), no @typedef. The two .d.ts additions are interface members, per convention.
  • @ref discipline: pick.js:82 is a proper @ref LLP 0200#hidden-rows [implements]: with a gloss. walkthrough.js:1928 embeds the ref in prose without a gloss, which is below the CLAUDE.md form but matches existing practice in the same file (walkthrough.js:604 does the same with LLP 0130) — left as-is.
  • Manifest validation (manifest.js:214), catalog carry-through (plugin_catalog.js:120), and the --source enum (init.js:187) are consistent; nothing narrowed the non-interactive path.
  • visiblePickerDescriptors is applied at both render sites and nowhere else; validation filters (walkthrough.js:656, pick.js:489) correctly still use the full descriptor set, so a hidden id stays acceptable.

Test result

npm test: 3749 passing, 0 failing, 6 skipped (3755 total) — includes the one test added by this round.
npm run typecheck: clean.
npm run smoke -- walkthrough_picker_to_first_query: ok.

Note on the PR body's "two tests fail on this machine and pass in CI": both pass here. The suite is green from a clean worktree checkout once dependencies are installed, so that looks like local environment state rather than anything in this branch.

No verdict label or approval posted — the terminal verdict rung owns that.

…o 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>
@philcunliffe

Copy link
Copy Markdown
Contributor

neutral review round 2: PR #674 @ 27bf6c0

Verdict: findings (2) — both fixed and pushed as f6ae0c3.

Round 1's three fixes all hold up on re-read: the README/CONTEXT rewrites are accurate, the picker-disclosure.test.js comment now says the true thing, and visiblePickerDescriptors is still applied at exactly the two render sites and nowhere else. I re-verified the load-bearing claim independently — datasetOwnedSourceIdsFromCatalog (src/core/runtime/source_withhold.js:262-266) folds pickerDescriptors.values() with no hidden awareness, so the two raw ids still reach the ai_gateway_messages owner list and LLP 0192's fail-closed withholding stays armed. The design is right.

The one thing round 1 introduced is also where the remaining bug was.


1. [Medium] A carried hidden row is dropped by stepping back into pick — silently deleting the upstream the install runs on

src/core/cli/wizard/pick.js:448-451 (pre-fix)

constseededVisible=visibleList.some((d)=>seed.has(d.id))constcarried=seedIsChosen&&!seededVisible
? descriptorList.filter((d)=>d.hidden===true&&seed.has(d.id)).map((d)=>d.id)
: []

Round 1 correctly narrowed the gate from "the seed has no visible row" to "a chosen seed has no visible row". But seedIsChosen (pick.js:155, pre-fix) collapses two very different tiers — opts.initialSelection and configured — and the !seededVisible test is wrong for the first of them.

initialSelection is a re-entry seed: wizard/index.js:353 sets pickSeed = picked.sourcesPicked and re-enters pick with it when the user steps back from the sync lane. For the exact install carry-through exists to protect, sourcesPicked from the previous pass holds the carried hidden row plus whatever visible row the user just added. Re-asking "does this seed collect nothing the menu can show?" of that seed answers no, so carried comes back empty and the row is dropped.

Reproduced against the bundled catalog on 27bf6c0 (a --source raw-openai install being reconfigured):

pass 1 (menu, user adds claude): sourcesPicked = [ 'claude', 'raw-openai' ]
pass 2 (back, gate, bare enter): sourcesPicked = [ 'claude' ]
pass 2 written upstreams: [ 'anthropic' ] <- 'openai' is gone

back then enter deletes the gateway upstream the whole install runs on, with nothing rendered about it either way. That is the precise failure mode #carry-through was written to prevent, reintroduced through the back-navigation door, and it also contradicts LLP 0191 §re-entry-seeding ("stepping back into pick shows the answer previously confirmed").

Not caught by the round-1 test because that test drives a single pass.

Fixed:seedIsChosen: boolean is replaced by a SeedOrigin union ('selection' | 'config' | 'detected', new in src/core/cli/wizard/types.d.ts:54), computed at pick.js:157. The carry rule is now seedOrigin === 'selection' || (seedOrigin === 'config' && !seededVisible) (pick.js:463):

  • detected never carries — unchanged, still what LLP 0011 §autodetect-vs-default demands, still pinned by round 1's test.
  • config carries only when the menu can show nothing the config collects — unchanged, still what stops derivative read-back seeding from resurrecting an unchecked upstream.
  • selection carries unconditionally. Safe precisely because read-back cannot reach that tier: a hidden id is in a previous answer only because this rule already put it there, so re-testing the precondition can only undo a decision, never make a better one.

Pinned by test/core/cli/wizard/pick.test.js:1102, which drives both passes and asserts the written upstreams. Verified it fails on the pre-fix pick.js (not ok 44) and passes after.

On LLP 0200. §carry-through's normative sentence said the row rides through "in exactly one case". Fixing this meant that sentence needed a second case, so I amended §carry-through and §consequences in the same commit rather than leaving the doc stale. I want to be explicit that this is not the same move round 1 declined to make for the fleet-locked row: that was a settled design choice the doc states deliberately, whereas re-entry is a case the doc never considered and whose current behaviour contradicts LLP 0191. Reviewer/author is free to disagree with the amendment; the code change stands on 0191 either way.

2. [Low] The new hidden manifest validation branch shipped untested

src/core/manifest.js:214-216 adds the reject-a-non-boolean rule, but test/core/manifest-semver.test.js has a case for every neighbouring picker field (summary, detect variants, detect.path, needs_setup) and none for hidden. A silently-removed guard here is how a non-boolean hidden reaches plugin_catalog.js:120's typeof check and gets quietly dropped instead of failing the manifest.

Fixed: two cases added at test/core/manifest-semver.test.js:143 (rejects hidden: 'yes' with the exact message; accepts hidden: true).


Checked and clean

  • The round-1 fixes. README step 1 and the new raw-proxy paragraph match what the menu now renders; CONTEXT.md's Source glossary correctly separates "picker sources" from "menu rows"; the picker-disclosure.test.js comment is accurate and the assertions are rightly kept.
  • --source is untouched.src/core/commands/init.js:187 still enumerates both raw ids, and the opts.picks path never reaches a prompt. Pinned by the PR's own test.
  • Withholding stays armed.source_withhold.js:66 and :129 filter picker keys by provenance, not by visibility; :262 folds every descriptor. No hidden read anywhere in that file, which is the correct answer.
  • The legacy runPickerWalkthrough path (walkthrough.js:648) filters the menu but has no config read-back at all, so it needs no carry-through. Correctly left alone.
  • Style: no semicolons, no U+2014 in any touched file, JSDoc-only types, no inline import('...'), no @typedef. SeedOrigin is a union alias in a .d.ts, matching the existing WizardPathway / WizardStepName / ReturningGateAction convention, imported by root-anchored .js specifier.
  • @ref discipline: split the single LLP 0200#hidden-rows ref on runWizardPick into #hidden-rows (the two screens) and #carry-through (the ride-through rule), since the gloss on the old one no longer described what the code does. Both anchors exist.

Not re-raised

The fleet-locked hidden row vanishing from the defaults gate stands as round 1 left it: LLP 0200 §hidden-rows settles it, it is unreachable against the bundled catalog, and it is the author's call to make in a new LLP.

Test result

npm test: 3752 passing, 0 failing, 6 skipped (3758 total) — up from 3749 by the three tests this round added. Baseline on 27bf6c0 was 3749/0 from the same clean worktree, so nothing regressed.
npm run typecheck: clean.
npm run smoke -- walkthrough_picker_to_first_query: ok.

No verdict label or approval posted - the terminal verdict rung owns that.

@philcunliffephilcunliffe added the neutral:changes-requested neutral reviewed an adopted PR and requests changes (non-binding; maintainer decides) label Aug 7, 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 f6ae0c3 carries changes that have not themselves been reviewed. Handing the remainder back rather than deferring it - this is your PR, so residual items are yours to decide, not neutral's to defer into a follow-up issue.

What the two rounds did. Round 1 (head 13f1680) found 3 findings and fixed them. Round 2 (head 27bf6c0) found 2 more and fixed them - including a real bug that round 1's own fix had introduced: collapsing two seed tiers into one boolean meant stepping back into the picker lane dropped a carried hidden row, silently deleting the gateway upstream the install runs on. Reproduced end-to-end against the bundled catalog, and pinned by test/core/cli/wizard/pick.test.js:1102 (verified failing before the fix, passing after). Suite at the current head: 3752 passing, 0 failing; typecheck clean.

What needs your decision:

  1. The LLP 0200 amendment. Round 2 edited an Accepted doc (§carry-through and §consequences) because its normative "in exactly one case" sentence needed a second case once re-entry was handled. The stated reasoning is that re-entry was never considered by the doc and its current behaviour contradicts LLP 0191, which is a different situation from re-litigating settled intent. That reasoning looks sound, but this repo's convention is that an Accepted LLP changes via a new LLP, so the call is yours: keep the amendment, or split it into a new doc. The code change stands on LLP 0191 either way.

  2. A fleet-locked hidden row vanishing from the defaults gate. Deliberately left unfixed across both rounds: LLP 0200 §hidden-rows settles the current behaviour, and it is unreachable against the bundled catalog. If "locked" should beat "hidden", that is a new LLP, not a code patch.

  3. The current head is unreviewed.f6ae0c3 is neutral's own round-2 work. Given round 2 caught a defect in round 1's fix, that head deserves your eyes before merge rather than an automatic pass.

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, not a blocking finding.

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