Skip to content

LLP 0276 + 0277: hidden rows stay off the sync gate; an answer-less config is not a reconfigure - #874

Merged
bgmcmullen merged 5 commits into
masterfrom
wizard-sync-gate-and-answerless-config
Aug 19, 2026
Merged

LLP 0276 + 0277: hidden rows stay off the sync gate; an answer-less config is not a reconfigure#874
bgmcmullen merged 5 commits into
masterfrom
wizard-sync-gate-and-answerless-config

Conversation

@bgmcmullen

@bgmcmullenbgmcmullen commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Two onboarding-wizard fixes, one commit and one LLP decision each.

LLP 0276: a hidden picker row stays off the sync gate too

On every enrolled machine the sync gate led with the two hidden raw-proxy rows (raw-anthropic / raw-openai) labelled · managed by your fleet - rows the picker had deliberately never offered (LLP 0202). They classify locked because @hypaware/ai-gateway sits in the central layer, so the only fleet-labelled rows were the two nobody had seen, and the label read as a property of the client rows beneath them.

  • runInitWizard now passes the locked descriptors through visiblePickerDescriptors, the same display filter the pick lane uses. Filtered at the screen, never at the locked set: dropping hidden ids from lockedSources upstream would re-compose the org's gateway into the local layer (the LLP 0129 collision).
  • The no-candidates short-circuit in runWizardSyncScope splits: with no visible org row to name, it prints "You picked nothing to record, so nothing syncs to your server" instead of attributing an empty list to the fleet. Both paths stay noQuestion, so the step counter is unaffected.
  • Supersedes LLP 0202's consequence line "the sync/opt-out menu is unchanged" (written before PR Onboarding wizard cleanup: defaults gate, back navigation, enrolled default sync, CLI severity colour #629 added locked rows to that gate).

LLP 0277: an answer-less config does not make a reconfigure

hyp remote add before the first hyp init (the documented team onboarding order) writes a config holding only query.remotes. The pick phase classified any readable config file as a reconfigure, so the first real onboarding run seeded from an empty read-back instead of from detection: every box arrived unchecked but still labelled · detected, no defaults/express gate rendered, and the export read-back flipped the first-run local-parquet default to keep-local.

  • The classification now keys on whether the config records a pick answer, discriminated by the plugins key (configRecordsPickAnswer): the pick composer always writes a plugins array, the side-channel writers never touch it.
  • An answer-less config seeds like no config at all - detection pre-checks, gates render, export takes the first-run default - while its keys (the added remote) still carry through the composition fold.
  • plugins: [] remains a reconfigure: an emptied install cannot be told from one, and re-seeding it from detection would re-consent on the user's behalf, which is what LLP 0183 forbids.
  • The reconfigure telemetry attribute on wizard.pick.start follows the answer-keyed classification.
  • Residual noted in the doc: the returning gate (LLP 0129/0182) still keys on file existence, so hyp init after hyp remote add still fronts the "already set up" summary; left to a follow-up.

Testing

  • node --test test/core/cli/wizard/*.test.js: 132 pass, 0 fail (new coverage for the filtered sync gate, both no-candidates lines, answer-less seeding, key carry-forward, and the plugins: [] boundary).
  • npm test: 28 failures in query/parquet/report suites, identical count on a clean master (pre-existing, unrelated).

🤖 Generated with Claude Code

bgmcmullenand others added 2 commits August 18, 2026 18:28
On every enrolled machine the sync gate led with the two hidden
raw-proxy rows (raw-anthropic / raw-openai) wearing the fleet label,
because @hypaware/ai-gateway sits in the central layer so they classify
locked - while the picker had deliberately never offered them. The lane's
locked descriptors now go through the same visiblePickerDescriptors
filter the pick lane uses, at the screen, never at the locked set (which
would re-compose the org gateway into the local layer, LLP 0129).
The no-candidates short-circuit splits accordingly: with no visible org
row to name, it says nothing syncs instead of naming the fleet as owner
of an empty list.
Extends LLP 0202 and supersedes its "sync/opt-out menu is unchanged"
consequence line.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
hyp remote add before the first hyp init writes a config holding only
query.remotes. The pick phase classified any readable config file as a
reconfigure, so that run seeded from an empty read-back instead of from
detection: every box arrived unchecked, no defaults gate rendered, and
the export default quietly flipped to keep-local.
The classification now keys on whether the config records a pick answer,
discriminated by the plugins key: the composer always writes a plugins
array, the side-channel writers never do. An answer-less config seeds
like no config at all - detection pre-checks, gates render, export takes
the first-run local-parquet default - while its own keys still carry
through the composition fold. plugins: [] stays a reconfigure: an
emptied install must not be re-consented from detection.
Extends LLP 0183.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@bgmcmullenbgmcmullen added the neutral:adopt Foreign PR adopted into neutral's reconcile scope label Aug 19, 2026
@philcunliffephilcunliffe added the neutral:adopted Adoption completion record: merged while carrying neutral:adopt (LLP 0031) label Aug 19, 2026
… the no-candidates line stops claiming nothing syncs
Two defects in the LLP 0266 half of this PR, both on the sync gate.
The locked list went through `visiblePickerDescriptors`; the candidate
list did not. A carried hidden row (LLP 0202 #carry-through) reaches
`picked.descriptors` whenever that row is not locked - a team join whose
org config has not converged, or a machine whose central layer does not
declare `@hypaware/ai-gateway` - and the gate then rendered it as an
editable checkbox for a row the picker deliberately never offered, where
unchecking it writes a `local-only` entry for a source the user never saw.
Both row lists now take the same filter, which is what makes LLP 0266's
"absent from every wizard screen" true rather than half true.
The new no-candidates line then said "nothing syncs to your server" on
exactly the machine class LLP 0266 targets: an enrolled machine whose
locked set is entirely the hidden `raw-*` pair filtered out of the
display. Those rows are still locked, still composed by the org's central
layer, and under LLP 0188 #locked they always sync and can never be opted
out - so the sentence traded LLP 0202's over-disclosure for an
affirmatively false claim about what leaves the machine. The branch now
splits three ways on `lockedHidden`, a count the lane gets so it can tell
the truth about withheld rows without being able to name them.
LLP 0266 §sync-gate, §no-candidates, and §consequences updated to match.
@philcunliffe

Copy link
Copy Markdown
Contributor

neutral review round: PR #874 @ 86ceb2e6

Verdict: approve after fixes (both fixed and pushed on this branch).

The LLP 0267 half is clean. Two medium findings in the LLP 0266 half, both on the sync gate, both fixed in 11bd103e.

Verification on the reviewed head: npm test 4489 pass / 0 fail, npm run typecheck clean. Same after the fixes: npm test 4490 pass / 0 fail / 1 skipped, npm run typecheck clean.


Finding 1 (medium, FIXED): the no-candidates line claims nothing syncs while locked rows are standing

src/core/cli/wizard/sync_scope.js:94 (reviewed head)

The new short-circuit prints "You picked nothing to record, so nothing syncs to your server." whenever opts.locked is empty. After this PR's change that branch is reached on exactly the machine class LLP 0266 targets: an enrolled machine whose only locked rows are raw-anthropic / raw-openai, which runInitWizard now strips out of opts.locked via visiblePickerDescriptors (src/core/cli/wizard/index.js:529).

Those rows are still in picked.lockedSources, are still composed by the org's central layer (@hypaware/ai-gateway carries the org listen address and proxy_mode), and per LLP 0188 §locked they always sync and can never be opted out. So something does leave the machine, and the gate says nothing does.

It is also reachable with a genuinely picked hidden row: a config that collects only raw-anthropic carries it through (carried in resolvePickSeeding, pick.js:225), the locked filter drops it from sources, so candidates is empty too. Before this PR the same screen at least listed those rows as fleet-managed. The change turned an over-disclosure into an affirmatively false statement about what leaves the machine, which is the failure LLP 0188 §never-silent exists to prevent.

Fix: the branch now splits three ways, driven by a new lockedHidden count (RunWizardSyncScopeOptions.lockedHidden, src/core/cli/wizard/types.d.ts:87) that runInitWizard computes as allLockedDescriptors.length - lockedDescriptors.length:

stateline
a visible org row"Everything you picked is managed by your fleet and always syncs." + the rows
no visible org row, lockedHidden > 0"You picked nothing to record, but capture your fleet manages directly still syncs to your server."
no locked row at all"You picked nothing to record, so nothing syncs to your server."

A count, not a list: the lane must be able to tell the truth about the withheld rows without being able to name them. All three paths stay noQuestion, so LLP 0191 §back-edges and the step counter are unaffected.

Finding 2 (medium, FIXED): the display filter was applied to locked but not to candidates

src/core/cli/wizard/index.js:539 (reviewed head)

LLP 0266's stated invariant widens LLP 0202 to "absent from every wizard screen", but only half of the screen was filtered. candidates: picked.descriptors is sources.map(id => descriptors.get(id)), and sources carries a hidden id whenever that id is not locked, via the LLP 0202 carry-through:

  • a machine configured hyp init --source raw-anthropic (config collects nothing visible, so carried = ['raw-anthropic']) that then runs hyp init on the team pathway, where the join's org-config converge has not landed yet and lockedSet is empty;
  • an enrolled machine whose central layer does not declare @hypaware/ai-gateway.

The sync gate then listed "Anthropic API" as an editable candidate and rendered it as a checkbox, and unchecking it writes a local-only policy entry for a source the picker deliberately never offered.

Fix:candidates: visiblePickerDescriptors(picked.descriptors). Filtered at the screen, not in pick.js, which keeps LLP 0266's own "filter at the screen, never at the locked set" rule and leaves configure.js's needs-setup read of picked.descriptors untouched. The policy store's editor semantics are correct under this: an existing opt-out entry for a now-unshown row is kept, never silently dropped, which is what runWizardSyncScope already documents.

No issues in the LLP 0267 half

configRecordsPickAnswer is a sound discriminator: composePickerConfig always emits a plugins array (including []), and mutateLocalConfig (the only remote add / remove writer) never touches the key. carryForwardExistingConfig handles existing.plugins === undefined safely, the reconfigure span attribute and the export read-back are consistently keyed to configured !== undefined, and expressRowsSafe reuses resolvePickSeeding so the express gate cannot drift from the pick lane. The returning-gate mismatch (configExists && configValid) is already declared a residual in LLP 0267 §consequences.


What was pushed

11bd103e on wizard-sync-gate-and-answerless-config:

  • src/core/cli/wizard/index.js: both row lists through visiblePickerDescriptors; lockedHidden passed to the lane.
  • src/core/cli/wizard/sync_scope.js: three-way no-candidates split.
  • src/core/cli/wizard/types.d.ts: lockedHidden?: number, and the doc comments on candidates / locked now say both arrive display-filtered.
  • test/core/cli/wizard/sync_scope.test.js: hidden-only locked set does not claim nothing syncs and still never names the withheld rows.
  • test/core/cli/wizard/index.test.js: a hidden picked row is not a sync candidate; lockedHidden is threaded.
  • llp/0266-hidden-rows-stay-off-the-sync-gate.decision.md: §sync-gate, §no-candidates, and §consequences updated to match. Amended in place rather than superseded because the doc is introduced by this same unmerged PR, so there is no landed decision to preserve.

neutraland others added 2 commits August 19, 2026 04:10
0266 is also claimed by fix/issue-836 (PR #850) and
update/icebird-squirreling-native-batches (PR #866); 0267 is also
claimed by fix/issue-837 (PR #849). Both of those PRs are older, so
this branch yields the numbers. 0276 and 0277 are free across master
and every open branch.
Mechanical renumber only: no content change (LLP 0156).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The LLP 0276 filter takes hidden rows off both sync-lane lists, but only
the locked list reported how many it removed. A carried hidden row (LLP
0202 #carry-through) that is not locked reaches picked.descriptors, is
composed into the local layer, and syncs unless an opt-out entry says
otherwise - and when it is the only pick, the filter empties `candidates`
with `locked` empty and `lockedHidden` 0, so the lane took the strongest
of its no-question sentences and told the user "nothing syncs to your
server" while capture was in fact leaving the machine. That is the
affirmatively false claim LLP 0276 #no-candidates ruled out for the
locked case and did not carry to the candidate case.
`runInitWizard` now passes `candidatesHidden` alongside `lockedHidden`,
one count per filtered list, and the no-question branch gains a fourth
line: with no locked row but a hidden pick standing, it states that
capture already set up on this machine still syncs, naming neither the
row nor the fleet (which does not own it).
LLP 0276 #no-candidates extended with the case and the sentence.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@philcunliffe

Copy link
Copy Markdown
Contributor

neutral review round: e613ba25 (adopted PR, healed in place)

Verdict: findings (1 actionable, fixed and pushed).

The LLP 0277 half (configRecordsPickAnswer) checks out. The plugins-key discriminator was verified against every local-config writer in the tree: remote_commands.js:mutateLocalConfig (the remote add/remove path) is the only one that never touches plugins, while composePickerConfig, carryForwardExistingConfig, client_enable.js, and gateway_proxy_enable.js all write it unconditionally. The export/reconfigure/telemetry keying off configured !== undefined is consistent, and carryForwardExistingConfig handles a missing plugins correctly. No issues.

Finding 1 (medium) - the sync gate can claim "nothing syncs" while a hidden picked row syncs

src/core/cli/wizard/sync_scope.js:97-104 (at e613ba25), fed by src/core/cli/wizard/index.js:534.

The new display filter is applied to both row lists, but only the locked list reports how many rows it removed (lockedHidden). There is no candidate counterpart. A carried hidden row (LLP 0202 #carry-through) reaches picked.descriptors whenever it is not locked - the team pathway whose org config has not converged, the case LLP 0276 §sync-gate names itself and the new test 'a hidden picked row is not a sync-scope candidate' constructs. When it is the only pick, candidates filters to [], locked is [], and lockedHidden is 0, so the no-question branch takes the strongest of its three sentences.

Confirmed end-to-end through runInitWizard (catalog holding only raw-anthropic with hidden: true, pick returning descriptors: [rawDescriptor], lockedSources: []):

CANDIDATES []
LOCKEDHIDDEN 0
SYNCLINES ["Step 3 of 5 · Choose what syncs",
"You picked nothing to record, so nothing syncs to your server."]

raw-anthropic is in sources, composes the Anthropic gateway upstream into the local layer, and has no local-only entry in the client sync store, so it syncs. This is exactly the outcome LLP 0276 §no-candidates rules out for the locked case ("Saying 'nothing syncs' here would trade LLP 0202's over-disclosure for an affirmatively false claim about what leaves the machine"). The decision applied that reasoning to hidden locked rows and did not carry it to hidden candidate rows.

Fixed in 6038b120:

  • src/core/cli/wizard/index.js computes the filtered candidate list once and passes candidatesHidden alongside lockedHidden - one count per filtered list.
  • src/core/cli/wizard/sync_scope.js gains a fourth no-question line: with no locked row but a hidden pick standing, it prints "You picked nothing to record, but capture already set up on this machine still syncs to your server." It names neither the withheld row nor the fleet, which does not own it. All four paths stay noQuestion, so the step counter is unaffected.
  • src/core/cli/wizard/types.d.ts documents candidatesHidden.
  • LLP 0276 §no-candidates extended with the fourth case and the sentence (the doc is new in this PR, so this is an amendment to an unlanded draft, not an edit to a settled record); §consequences updated.
  • New test zero visible candidates with a hidden picked row: does not claim nothing syncs, never names the fleet in test/core/cli/wizard/sync_scope.test.js; the two runInitWizard filter tests now also assert candidatesHidden. The new test was verified to fail against e613ba25's sync_scope.js and pass after the fix.

Notes (not actionable)

  • With a visible locked row present and a hidden pick, the gate prints "Everything you picked is managed by your fleet and always syncs". Left as-is: the carried hidden row was never offered, so it was not "picked", and the sentence stays defensible under that reading. The false claim was only the "so nothing syncs" clause.
  • The PR body cites LLP 0266/0267; the docs on the branch are numbered 0276/0277. Body text only, no code impact.

Checks

  • npm test: 4491 pass / 0 fail / 1 skipped (at 6038b120).
  • npm run typecheck: clean.

@philcunliffe

Copy link
Copy Markdown
Contributor

neutral triage: PR #874 @ 6038b120

The review-round cap is reached, so the residual findings were triaged at the head. Every actionable finding from both rounds (86ceb2e6, e613ba25) is fixed on the branch and verified in the head code; the remaining residuals are all non-blocking:

  1. Sync-gate wording when a visible org row and a hidden carried pick coexist (src/core/cli/wizard/sync_scope.js:114) - phrasing precision, no false claim, behaviour unaffected.
  2. Returning gate keys on config existence rather than a recorded pick answer (src/core/cli/wizard/fork.js:184) - declared residual in LLP 0277 §consequences.
  3. PR title/body still cite LLP 0266/0267 after the renumber to 0276/0277 - metadata only.

Deferred to follow-up issue #911. Verification at head: node --test test/core/cli/wizard/sync_scope.test.js test/core/cli/wizard/index.test.js passes 79/79.

@philcunliffephilcunliffe added the neutral:approved neutral reviewed this and holds it for a maintainer merge (own or adopted PR; LLP 0025/0030) label Aug 19, 2026
@bgmcmullen
bgmcmullen merged commit 60e33ea into masterAug 19, 2026
9 checks passed
@bgmcmullen
bgmcmullen deleted the wizard-sync-gate-and-answerless-config branch August 19, 2026 06:05
@philcunliffephilcunliffe changed the title LLP 0266 + 0267: hidden rows stay off the sync gate; an answer-less config is not a reconfigureLLP 0276 + 0277: hidden rows stay off the sync gate; an answer-less config is not a reconfigureAug 19, 2026
philcunliffe pushed a commit that referenced this pull request Aug 19, 2026
…nd the returning gate keys on a recorded answer
Two deferred residuals from PR #874 (issue #911), both the same shape: a
screen keyed on a proxy for the fact it states.
Finding 1: with a visible org row on the sync gate and a hidden carried
pick standing beside it (`candidatesHidden > 0`), the lane printed
"Everything you picked is managed by your fleet and always syncs". The
carried row (LLP 0202 #carry-through) composes into the local layer and
syncs, so the sentence handed the fleet an owner's claim over capture it
does not own. The branch now narrows the fleet sentence to the rows the
fleet owns and adds the machine's-own-capture line LLP 0276 already
minted. Still a count, never a name; still `noQuestion`.
Finding 2: the returning gate (LLP 0129 / 0182) keyed on
`configExists && configValid`, so the answer-less config `hyp remote add`
writes before the first `hyp init` still fronted the "already set up"
summary - the residual LLP 0277 declared. `collectHypAwareStatus` now
reports `configRecordsAnswer` off the effective config, and the gate takes
the first-run path without it. Effective, not local: a machine carried by
its central layer is set up, the fleet having answered for it.
`configRecordsPickAnswer` moves from the CLI walkthrough to
`config/schema.js` so the daemon-side reader does not close an import
cycle. One definition, two readers.
Finding 3 (PR #874's body citing LLP 0266/0267 after the renumber to
0276/0277) is metadata on a merged PR, corrected there; see LLP 0281
#not-done.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
philcunliffe added a commit that referenced this pull request Aug 19, 2026
…ing gate that keys on a recorded answer (#925)
* LLP 0281: the sync gate's fleet sentence stops at the fleet's rows, and the returning gate keys on a recorded answer
Two deferred residuals from PR #874 (issue #911), both the same shape: a
screen keyed on a proxy for the fact it states.
Finding 1: with a visible org row on the sync gate and a hidden carried
pick standing beside it (`candidatesHidden > 0`), the lane printed
"Everything you picked is managed by your fleet and always syncs". The
carried row (LLP 0202 #carry-through) composes into the local layer and
syncs, so the sentence handed the fleet an owner's claim over capture it
does not own. The branch now narrows the fleet sentence to the rows the
fleet owns and adds the machine's-own-capture line LLP 0276 already
minted. Still a count, never a name; still `noQuestion`.
Finding 2: the returning gate (LLP 0129 / 0182) keyed on
`configExists && configValid`, so the answer-less config `hyp remote add`
writes before the first `hyp init` still fronted the "already set up"
summary - the residual LLP 0277 declared. `collectHypAwareStatus` now
reports `configRecordsAnswer` off the effective config, and the gate takes
the first-run path without it. Effective, not local: a machine carried by
its central layer is set up, the fleet having answered for it.
`configRecordsPickAnswer` moves from the CLI walkthrough to
`config/schema.js` so the daemon-side reader does not close an import
cycle. One definition, two readers.
Finding 3 (PR #874's body citing LLP 0266/0267 after the renumber to
0276/0277) is metadata on a merged PR, corrected there; see LLP 0281
#not-done.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Review fix: the returning gate's answer claim reads each config layer, not the merge
`configRecordsAnswer` was computed from `merged.effective`, which cannot
express the claim it makes.
The enrolling `hyp remote login` writes a central seed carrying
`plugins: [{ name: '@hypaware/central' }]` (`enrollCentralSink`), so on
the documented team order - `hyp remote add`, `hyp remote login`,
`hyp init` - the merged config had a `plugins` array before anyone was
asked anything, `configRecordsAnswer` came back true, and the returning
gate still fronted "HypAware is already configured". That is the exact
sequence LLP 0281 #returning-gate claims to fix; it did not fire for it.
The same read also lost a deliberate `plugins: []` record-nothing pick on
a joined machine: `mergeConfigLayers` sets the key only when the merged
list is non-empty, so onboarding re-opened on a deliberately emptied
install, which LLP 0277 #answer-less forbids.
Each layer is now read on its own terms: the local layer answers when it
records a pick answer (the same discriminator the pick lane reads), and
the central layer answers when it carries capture of its own. The bare
enrollment plugin is not an answer.
Two regression tests, verified failing before this change and passing
after. `npm test`: 4540 pass / 0 fail / 1 skipped. `npm run typecheck`
clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* Review fix: a central layer answers the pick question only when it carries capture
`configRecordsAnswer`'s central half read "any plugin that is not the
enrollment seed", but the claim it stands for - stated in LLP 0281
#returning-gate, in the comment above it, and in the `HypAwareStatusReport`
doc - is "the central layer carries capture of its own". A fleet whose
central layer also pushes a sink or format plugin (`@hypaware/local-fs`,
`@hypaware/format-parquet`, `@hypaware/gascity`) satisfied the old reading
without anyone having answered anything, so the documented team order
`hyp remote add` -> `hyp remote login` -> `hyp init` still fronted the
returning summary over a machine that records nothing: the exact failure
the decision closes.
The test is now the positive one, against the picker catalog: does the
central layer name a plugin that contributes a picker row? That is the same
plugin-level test `computeCentralLockedSources` uses to decide which rows
the org owns, so the locked set and this claim cannot disagree. With no
catalog the question cannot be asked, so that path keeps the weaker
plugin-name reading rather than re-opening onboarding's consent questions.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: neutral <neutral@hyparam.dev>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: test <test@test.com>
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:approvedneutral 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

@bgmcmullen@philcunliffe