Skip to content

Deferred #874 residuals: the sync gate's fleet sentence, and a returning gate that keys on a recorded answer - #925

Merged
philcunliffe merged 3 commits into
masterfrom
fix/issue-911
Aug 19, 2026
Merged

Deferred #874 residuals: the sync gate's fleet sentence, and a returning gate that keys on a recorded answer#925
philcunliffe merged 3 commits into
masterfrom
fix/issue-911

Conversation

@philcunliffe

@philcunliffephilcunliffe commented Aug 19, 2026

Copy link
Copy Markdown
Contributor

Closes the deferred review findings triaged out of PR #874. Two behaviour fixes with regression tests, one metadata correction made on the merged PR itself. New decision doc LLP 0281, with Extended-by: forward-refs appended to LLP 0276 §no-candidates and LLP 0277 §consequences (both Accepted, so neither is edited on the parts it settled).

Finding 1: the sync gate's fleet sentence spoke for a row the fleet does not own

src/core/cli/wizard/sync_scope.js

With a visible org row on the no-candidates branch the lane printed "Everything you picked is managed by your fleet and always syncs." over the org rows. When candidatesHidden > 0 a carried hidden row (LLP 0202 §carry-through) is also standing: it is in sources, composes into the local layer, and absent an opt-out entry it syncs. The sentence quantified over it and attributed it to the fleet.

LLP 0276 §no-candidates split the empty-locked cases four ways for exactly this reason and left the non-empty one; this applies the same rule to it. With a hidden pick standing beside the org rows:

Your fleet manages these and they always sync:
Claude Code
Capture already set up on this machine also syncs to your server.

The second line is the sentence LLP 0276 already minted for a machine's own capture, unchanged. Neither line names the withheld row - candidatesHidden stays a count. With no hidden pick standing, the exhaustive sentence is true and is kept verbatim, so the common enrolled run is untouched. The path stays noQuestion, so LLP 0191 §back-edges and the step counter are unaffected.

Finding 2: the returning gate keyed on file existence, not on a recorded answer

src/core/cli/wizard/fork.js, src/core/daemon/status.js, src/core/config/schema.js

LLP 0277 moved the pick phase onto "the config records a pick answer" and declared the returning gate a residual: it still read configExists && configValid, so hyp init after a bare hyp remote add (the documented team onboarding order) fronted the "HypAware is already configured" summary over a machine that collects nothing.

  • collectHypAwareStatus now reports configRecordsAnswer, computed from the effective (merged) config. Effective rather than local is deliberate: a machine carried entirely by its central layer is set up, the fleet having answered on its behalf, and demoting it to the first-run path would re-ask questions the org owns (LLP 0129 §join-before-picker).
  • evaluateReturningGate's guard becomes !configExists || !configValid || !configRecordsAnswer. managed is still read before the early return, for the same reason the invalid-config branch pins it.
  • configRecordsPickAnswer moves from src/core/cli/walkthrough.js to src/core/config/schema.js. The daemon-side reader cannot import the CLI walkthrough without closing an import cycle (walkthrough.js already imports status.js), and two copies of the discriminator could disagree.

Finding 3: PR #874 cites LLP 0266/0267

Repository metadata on a merged PR, not code: corrected on PR #874's title and body (0266 → 0276, 0267 → 0277, neutral-triage marker preserved). Recorded in LLP 0281 §not-done. Nothing in the tree to change.

Testing

Every behavioural finding has a test verified to fail before the fix and pass after.

testfilebeforeafter
zero visible candidates with an org row and a hidden picked row: the fleet sentence covers only its own rowstest/core/cli/wizard/sync_scope.test.jsfailspasses
zero visible candidates with an org row and no hidden pick: keeps the exhaustive fleet sentencesamepasses (boundary pin)passes
evaluateReturningGate: a valid config that records no pick answer is the first-run pathtest/core/cli/wizard/fork.test.jsfailspasses
evaluateReturningGate: an answer-less config on a managed machine is first-run and still managedsamefailspasses
evaluateReturningGate: a config that records a pick answer still fronts the returning gatesamepasses (boundary pin)passes
5 cases in test/core/status-config-records-answer.test.js (remote-add-only, composed, plugins: [], no config, central-layer-answers)new fileall failall pass

Failing-before was checked by reverting each production file in place (sync_scope.js, fork.js, status.js) and re-running the suite.

  • npm test: 4538 pass / 0 fail / 1 skipped.
  • npm run typecheck: clean.

Fixes#911

@philcunliffe

Copy link
Copy Markdown
ContributorAuthor

Review round: e21497b1 -> fixed at 6b211dc6

Verdict: changes requested, and applied on the branch. Three findings; one high (the PR's headline fix did not fire for its own headline scenario), one low with the same root cause, one low reported and deliberately not fixed. The two behaviour findings are fixed in 6b211dc6, pushed to fix/issue-911. npm test 4540 pass / 0 fail / 1 skipped, npm run typecheck clean at the new head.


Finding 1 (high, FIXED): src/core/daemon/status.js:951 - configRecordsAnswer read the merge, so the returning gate still fronted "already configured" on the documented team order

configRecordsAnswer was config !== null && configRecordsPickAnswer(config), where config is merged.effective (status.js:924).

The enrolling hyp remote login writes a central seed that always carries a plugins array: enrollCentralSink writes literally { version: 2, plugins: [{ name: '@hypaware/central' }], sinks: { central: ... } } (src/core/commands/central.js:236-240), and runJoin seeds the same way (central.js:95). mergeConfigLayers puts that array into effective, so configRecordsPickAnswer(effective) was true on every enrolled machine, asked or not.

Reproduced by running collectHypAwareStatus against exactly those two files on disk (remote-add local layer + enrollment seed), pre-fix:

hasCentral true configExists true configValid true configRecordsAnswer true

So hyp remote add -> hyp remote login -> hyp init still took the returning branch. That is the sequence LLP 0277 §consequences names as the residual and LLP 0281 §consequences (added by this PR) asserts is now closed: "hyp remote add -> hyp remote login -> hyp init now presents the fork, not the returning summary." It did not. The fix only bit for a remote add with no login behind it, which is not the documented order.

The existing test a central layer answers for a machine whose local layer does not did not catch it: its fixture central layer carries @hypaware/ai-gateway, i.e. a converged org config that really did answer for the machine, not the bare enrollment seed that exists between remote login and the first org-config pull.

It also left the gate and the pick lane keying on two different discriminators for one run: src/core/cli/wizard/pick.js:127 reads the local config via readLocalConfig, so on an enrolled machine the gate said "returning/Reconfigure" while the pick lane said "answer-less" and seeded from detection.

Finding 2 (low, FIXED): same line - a deliberate plugins: [] was erased by the layer merge

mergeConfigLayers sets effective.plugins only when the merged list is non-empty (src/core/config/merge.js:56; likewise merge.js:137). So a joined machine whose central layer contributes no plugins and whose local layer holds plugins: [] (the deliberate record-nothing pick) yielded effective.plugins === undefined and configRecordsAnswer === false. Reproduced pre-fix: configRecordsAnswer false. The gate then re-opened onboarding on a deliberately emptied install, which configRecordsPickAnswer's own JSDoc and LLP 0277 §answer-less say must not happen.

The fix (6b211dc6)

Each layer is read on its own terms instead of off the merge:

constcentralAnswersPick=[...centralPluginNames].some((name)=>name!==CENTRAL_ENROLLMENT_PLUGIN)constconfigRecordsAnswer=(localConfig!==null&&configRecordsPickAnswer(localConfig))||centralAnswersPick
  • Local layer answers when it records a pick answer - the same discriminator pick.js reads, so gate and lane can no longer classify one file two ways.
  • Central layer answers when it carries capture of its own, preserving LLP 0281's load-bearing "the fleet answered on its behalf" case (LLP 0129 §join-before-picker).
  • The bare @hypaware/central enrollment seed is not an answer: it is on disk before anyone is asked anything.

src/core/daemon/types.d.ts and LLP 0281 §returning-gate updated to describe the rule the code actually implements (the old text asserted "effective (merged)" and a consequence that did not hold). LLP 0276/0277 forward-refs untouched.

Post-fix, the same two reproductions:

A enrolled-remote-add-only configRecordsAnswer false (was true)
B empty-plugins-with-central configRecordsAnswer true (was false)

Two regression tests added to test/core/status-config-records-answer.test.js, each verified failing at e21497b1 and passing at 6b211dc6:

testat e21497bat 6b211dc
an enrolled machine that has not run init yet records no pick answerfailspasses
an empty plugins array survives beside a central layer that adds nonefailspasses

Finding 3 (low, NOT fixed - reported): src/core/cli/wizard/sync_scope.js:127 asserts the hidden pick syncs without consulting the policy store

Capture already set up on this machine also syncs to your server. prints unconditionally on candidatesHidden > 0. optedOutBefore is computed only over candidateIds, i.e. the visible candidates, so a hidden picked source with a standing local-only entry is invisible to that check, and the screen claims capture ships when the export seam will withhold it.

Left as-is deliberately: the branch cannot do better, because candidatesHidden is a count by LLP 0276's own design ("the lane must be able to tell the truth about them without being able to name them") - there is no id to look up. The identical property already holds for the sentence LLP 0276 minted at sync_scope.js:107, which this branch reuses verbatim, so this is a pre-existing property of that sentence rather than something this PR introduces. Fixing it would mean handing the sync lane the hidden row ids that LLP 0276 withholds, which is a design change and belongs in its own LLP. Flagged here so it is on the record.


Checked and clean

  • The configRecordsPickAnswer move to src/core/config/schema.js is complete: no stale importers anywhere in the tree, walkthrough.js -> status.js import cycle genuinely avoided, JSDoc @ref LLP 0277#answer-less carried across intact.
  • managed is still read before the new early return in evaluateReturningGate, matching the invalid-config branch.
  • The new sync_scope branch is ordered correctly after the locked.length === 0 branch, returns noQuestion: true, writes no policy store entry, and never names the withheld row (asserted).
  • HypAwareStatusReport.configRecordsAnswer has exactly one producer; hyp status's own rendering (src/core/commands/status.js:133,413) still keys on configExists and is unchanged.
  • test/core/init-configured-entry.test.js end-to-end dispatch cases are unaffected (their configs carry plugins).
  • Style: no em dashes, no semicolons, no inline import('...') types, @import specifier is repo-root-anchored.

Not done: no merge, no ready-flip, PR body untouched.

@philcunliffe

Copy link
Copy Markdown
ContributorAuthor

Neutral review record

Reviewed:6b211dc6ce5bf94e048c83fe05c34aebf3745dc8 (fix/issue-911)
Verdict: findings (2) - 1 actionable, fixed and pushed; 1 non-actionable, left.
Verification:npm test 4541 pass / 0 fail / 1 skipped, npm run typecheck clean, on the pushed head.

Finding 1 - medium - FIXED

src/core/daemon/status.js:973 (pre-fix)

constcentralAnswersPick=[...centralPluginNames].some((name)=>name!==CENTRAL_ENROLLMENT_PLUGIN)

The claim this computes is stated three times as "the central layer carries capture of its own" - LLP 0281 #returning-gate, the comment directly above the line, and HypAwareStatusReport.configRecordsAnswer in src/core/daemon/types.d.ts:406. The predicate did not test that: any plugin other than @hypaware/central satisfied it, including pure sink/format plugins (@hypaware/local-fs, @hypaware/format-parquet) and @hypaware/gascity, none of which record a pick answer.

Verified empirically against 6b211dc6: a central layer of plugins: [@hypaware/central, @hypaware/local-fs, @hypaware/format-parquet] plus the central sink, beside a local layer holding only query.remotes, returned configExists=true configValid=true configRecordsAnswer=true. So for a fleet that pushes a sink or format plugin, a new hire running the documented hyp remote add -> hyp remote login -> hyp init still gets fronted with the "HypAware is already configured" summary over a machine with zero capture configured - the exact failure LLP 0281 #returning-gate sets out to close. Not a regression against master (that path already showed the gate), but the fix silently did not fire for those fleets.

Fix (8768eea7): the test is now the positive one, against the picker catalog collectHypAwareStatus already builds - does the central layer name a plugin that contributes a picker row? That is the same plugin-level test computeCentralLockedSources (src/core/cli/wizard/join.js:137) uses to decide which rows the org owns, so the locked set and this claim cannot disagree. With no catalog available the question cannot be asked, so that path keeps the weaker plugin-name reading rather than re-opening onboarding's consent questions (LLP 0183 direction-of-cost).

Positively verified: new test a central layer carrying only non-capture plugins records no pick answer (test/core/status-config-records-answer.test.js) fails on 6b211dc6 (not ok 8) and passes on 8768eea7; git show HEAD:src/core/daemon/status.js contains capturePluginNames at lines 986-990, absent from the pre-fix tree.

Finding 2 - low - LEFT (settled by precedent)

src/core/cli/wizard/sync_scope.js:126

opts.stdout.write('Capture already set up on this machine also syncs to your server.\n')

The branch fires on candidatesHidden > 0, which is only a count of display-filtered picked rows. A hidden row such as raw-anthropic is addressable by hyp policy client raw-anthropic local-only, and the lane deliberately preserves store entries for sources outside candidateIds, so a user with a standing opt-out has a row that does not sync while the sentence says it does - on a privacy-facing screen (LLP 0188 #never-silent). The lane receives no ids for hidden rows and cannot check.

Left deliberately: this is the identical exposure carried by the pre-existing sibling branch at sync_scope.js:107, settled by LLP 0276 #no-candidates, and LLP 0281 #visible-org-row explicitly reuses that sentence "unchanged". Narrowing it here would make the two branches disagree and would edit what an Accepted LLP settled. It belongs in a follow-up that gives the lane hidden-row ids, covering both branches at once.

Checked and clean

configRecordsPickAnswer's move from walkthrough.js to config/schema.js (no stale importers, no re-export surface, breaks no import cycle); evaluateReturningGate's single caller (src/core/cli/wizard/index.js:108); every collectStatus stub and the sole HypAwareStatusReport producer against the new required field; all @ref anchors (#visible-org-row, #returning-gate, #not-done) resolve.

New head:8768eea7d7b321f35e4da810c405d4c6c493e9f8

@philcunliffe

Copy link
Copy Markdown
ContributorAuthor

Triage at head 8768eea7: the review-round cap was reached with one finding still open (the hidden-pick sync sentence at src/core/cli/wizard/sync_scope.js:126, reported in both review rounds). Verified against the diff: it is a pre-existing property of the sentence master already carries at sync_scope.js:107 (Accepted LLP 0276 #no-candidates), not a defect this PR introduces, and the fix requires a design change (hidden-row ids for the sync lane) in its own LLP. Judged non-blocking; the PR can merge safely.

Deferred to follow-up issue #935.

@philcunliffe
philcunliffe marked this pull request as ready for review August 19, 2026 11:10
@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
neutraland others added 3 commits August 19, 2026 12:02
…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>
…, 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>
…rries 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>
@philcunliffe
philcunliffe merged commit d1f1395 into masterAug 19, 2026
10 checks passed
@philcunliffe
philcunliffe deleted the fix/issue-911 branch August 19, 2026 19:07
philcunliffe pushed a commit that referenced this pull request Aug 20, 2026
Master landed PR #925 (LLP 0281 #visible-org-row), which added the sibling
no-candidates branch this PR's LLP 0289 #not-done anticipated. Both sides
touch the same few lines of the sync lane, so the merge is where the two
branches either agree on one fact or drift apart.
Resolution:
- `src/core/cli/wizard/sync_scope.js`: master's org-row branch gated on
`candidatesHidden > 0`, the count this PR replaces with
`candidatesHiddenIds`. It now reads the same `hiddenCandidateSyncs`
boolean its no-locked sibling reads, which is exactly what LLP 0289
#not-done said it should do when #925 landed. A hidden pick the store
already withholds does not ship, so the exhaustive fleet sentence is true
again and the two branches turn on one question rather than disagreeing
about it. Both branches of that fork now record `hidden_picks_syncing` on
the span.
- `llp/0276-...decision.md`: both sides appended an `Extended-by:` note to
#no-candidates. Both are kept, master's (LLP 0281) first.
- `test/core/cli/wizard/sync_scope.test.js`: both sides' tests are kept,
master's two ported to `candidatesHiddenIds`, plus a new pin for the case
the merge creates - an org row beside a hidden pick already opted out
keeps the exhaustive sentence. That pin fails on the mechanical rename
and passes on this resolution.
- `llp/0281-...decision.md`: forward-ref to LLP 0289 #ask-the-store on
#visible-org-row, since its branch now carries that `@ref`.
- `llp/0289-...decision.md`: #not-done records that LLP 0281 has landed and
the residual is closed.
npm test: 5017 pass / 0 fail / 1 skipped. npm run typecheck: clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
philcunliffe pushed a commit that referenced this pull request Aug 20, 2026
…e it is
Self-review of the master merge found one finding, fixed here.
medium: the merge re-gated LLP 0281 #visible-org-row's branch on
`hiddenCandidateSyncs`, a question about shipping, when what that
Accepted decision settled was a question about ownership. It says so
itself: "no clause here is affirmatively false about *what* leaves the
machine - which is why round 2 left it", and the rule it adopted is "a
sentence names only the rows whose owner it names."
So on an enrolled machine with a visible org row, a hidden carried pick
(LLP 0202 #carry-through), and a standing `hyp policy client
raw-anthropic local-only`, the merged lane printed "Everything you
picked is managed by your fleet and always syncs." over a row the fleet
does not manage and that does not sync - the exact claim #925 had just
removed from master, restored by this branch's merge and pinned by a new
test.
The branch now splits the two claims:
- the fleet sentence narrows whenever a hidden pick exists, withheld or
not, which is what LLP 0281 settled and what master does today
- the second line, the one that promises an export, is the store's
answer and prints only when a hidden pick is not already withheld,
the same question the no-locked sibling asks
So the two branches still agree about what leaves the machine, without
this one taking back an owner's claim it had given up. The withheld row
is still never named.
LLP 0289 #not-done and #consequences record which half of that branch
the store governs; the `Extended-by:` blockquote this PR appends to
LLP 0281 #visible-org-row is corrected to match. Nothing LLP 0281
settled is edited.
The reworked test fails on the pre-fix lane ("Everything you picked is
managed by your fleet and always syncs." where none of it is the
fleet's) and passes after.
npm test: 5016 pass / 0 fail / 1 skipped (the one failure in this
sandbox, `hyparquet-floor-pin`, reproduces unmodified at 9549f38 and is
a shared-node_modules artifact). npm run typecheck: clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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.

Follow-up: deferred review findings from PR #874

1 participant

@philcunliffe