Uh oh!
There was an error while loading. Please reload this page.
Enforce plugin manifest and runtime command agreement - #849
Conversation
…#853) `test/core/repo-scratch-hygiene.test.js` has been failing on `master` since it merged: `x/npm-test.log` and `x/typecheck.log` are tracked, and the test's first half asserts no `.log` is. Both came in on `adb448ab` (#785) via the `git add -A` sweep that #786 wrote this test to catch; the files predate the test, so it was red on arrival. Every branch cut since inherits it, currently blocking #833, #849, #850 and #851 for a reason none of them caused. The transcripts are deleted rather than the test relaxed, which is what its message asks for. `.gitignore` needs nothing: `*.log` is already committed and the rule test already passes, since an ignore rule cannot reach a path that is already tracked. That asymmetry is the whole reason the file carries two tests. Scope is exactly the two `.log` paths. The other eight files under `x/` are untouched: the hygiene test forbids tracked transcripts and nothing else, and sweeping up scratch it does not name would be a judgement this fix has no authority to make. Fixes#852 Co-authored-by: test <test@test.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review of |
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>
…ff two false reports Three defects found reviewing #849. - `dryRunActivate` left `ctx.env` defaulting to `process.env`, so a plugin that reads `HYP_HOME` during `activate()` was pointed at the caller's real install. `@hypaware/local-fs` mkdirs `<HYP_HOME>/exports` from `activate()`, so merely diagnosing it wrote into the home directory the function's own contract promises not to touch, and the new bundled agreement test did it for the whole workspace on every `npm test`. `HYP_HOME` now points at the throwaway root the rest of the dry run already uses. - The inert source registry returned a `StartedSource` the underlying registry never recorded, so a plugin that starts one of its own sources from `activate()` and then reloads it got `source 'x' is not started` and the doctor reported `activate_threw` against a plugin that works. The no-op is now swapped in at `register()` time and routed through the real lifecycle, which keeps the bookkeeping intact and still runs nothing. A malformed contribution passes through untouched so `register()` still rejects it. - The `registerGroup` warning contradicted LLP 0267 #d3: a group whose registered commands are all hidden is correctly absent from the manifest, yet was warned as describing a group nothing lists, and the bundled gate counts warnings as failures. Such a group is now exempt; a group with a visible command under it still warns. Also drops the "its manifest would not load" route from the `findInactivePluginForCommand` doc: boot names an unloadable plugin by its rootDir, not its plugin name, and an unreadable manifest declares no command to match against, so that route is not (and cannot be) served here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
philcunliffe
commented
Aug 19, 2026
Review round: |
philcunliffe
commented
Aug 19, 2026
Triage at head |
…onfig is not a reconfigure (#874) * LLP 0266: 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) 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> * LLP 0267: an answer-less config does not make a reconfigure 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> * Review round: hidden rows off the sync gate's candidate list too, and 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. * Renumber LLP 0266/0267 to 0276/0277 to clear number collisions 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> * Sync gate must not claim nothing syncs while a hidden picked row stands 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> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: neutral <neutral@hyperparam.app> Co-authored-by: test <test@example.com>
Plugin command help has two sources that nothing compared: `hyp --help` renders before boot and reads `contributes.commands` out of the manifest, while group and leaf help read the registry `activate()` filled. `@hypaware/context-graph-enrich` shipped two of its commands with different summaries at the two levels. - `hyp plugin doctor` gains `command_help_drift`: a declared command whose manifest summary and registered summary differ, a declared command registered `hidden`, or a group description no declared command sits under. Verb-projected commands are covered for free, since a verb registers its CLI command into the same registry. - A new bundled contract test runs that diff over every plugin in the workspace, so the bundled set is held to the check a plugin author runs. - A `hidden` command no longer trips `contribution_undeclared`: omitting it from the manifest is how it stays out of pre-boot help. - The doctor's dry run stops running sources (`@hypaware/otel` bound a real port from `activate()`) and stops handing back a capability stub that cannot be converted to a string. - The dispatch miss path distinguishes a plugin the config selects but the boot did not get, which previously fell through to "unknown command". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
LLP 0262 (#818) updated hyp session ignore to post to every local recorder (claude listener + gateway proxy), not just the gateway, and updated the activate() registration summary accordingly, but left the hypaware.plugin.json manifest summary describing the old gateway-only behavior. The new manifest/activate() agreement test caught the drift; this brings the manifest summary in line with the runtime text.
… dispatch miss The fourth miss state (LLP 0267 #d5) derived "selected but this run did not get it" by subtracting `activePlugins` from the config-selected manifests. `dispatch` only fills `activePlugins` when it boots the kernel itself; a caller that injects one (`opts.kernel`, forwarded by the integration API's `run()`) leaves it empty, so every config-selected plugin looked like a failed activation and a plain typo answered with "your config selects but this run could not activate". `bootKernel` already publishes that set as `unavailablePlugins`: the same four routes D5 names, and empty when dispatch did not boot. Read it instead of re-deriving it, and cover the injected-kernel case with a test. Also fold the seam's duplicated head-token match into `declaresCommandHead`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…the bundled gate
Three follow-ups on the new command_help_drift check.
- `summary` is optional on a manifest command entry, so an entry without
one is the shape authors most often land on. It is still drift (top
level help lists the command with no description), but reporting it as
"two different summaries: the manifest says ''" sends the author
looking for a second wording that does not exist. Name the blank and
hand back the exact line to paste.
- A group prefix is not always one token: `resolveGroupHelp` walks every
leading prefix, so `ctx.commands.registerGroup({ name: 'query cache' })`
is correct and renders for `hyp query cache --help`. Comparing only the
head token of each declared command warned about it forever. Match the
whole prefix.
- The bundled contract test treated `activate_threw` as non-fatal, so a
bundled plugin that throws before registering anything and declares no
commands passed the gate vacuously - exactly what LLP 0267 #d2 says it
must not do.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>…ff two false reports Three defects found reviewing #849. - `dryRunActivate` left `ctx.env` defaulting to `process.env`, so a plugin that reads `HYP_HOME` during `activate()` was pointed at the caller's real install. `@hypaware/local-fs` mkdirs `<HYP_HOME>/exports` from `activate()`, so merely diagnosing it wrote into the home directory the function's own contract promises not to touch, and the new bundled agreement test did it for the whole workspace on every `npm test`. `HYP_HOME` now points at the throwaway root the rest of the dry run already uses. - The inert source registry returned a `StartedSource` the underlying registry never recorded, so a plugin that starts one of its own sources from `activate()` and then reloads it got `source 'x' is not started` and the doctor reported `activate_threw` against a plugin that works. The no-op is now swapped in at `register()` time and routed through the real lifecycle, which keeps the bookkeeping intact and still runs nothing. A malformed contribution passes through untouched so `register()` still rejects it. - The `registerGroup` warning contradicted LLP 0267 #d3: a group whose registered commands are all hidden is correctly absent from the manifest, yet was warned as describing a group nothing lists, and the bundled gate counts warnings as failures. Such a group is now exempt; a group with a visible command under it still warns. Also drops the "its manifest would not load" route from the `findInactivePluginForCommand` doc: boot names an unloadable plugin by its rootDir, not its plugin name, and an unreadable manifest declares no command to match against, so that route is not (and cannot be) served here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
5c6bd00 to
6460a9cCompareUh oh!
There was an error while loading. Please reload this page.
What was wrong
Plugin command help has two independent sources and nothing compared them.
hyp --helprenders beforebootKerneland readscontributes.commandsoutof
hypaware.plugin.json(LLP 0009: booting to populate the registry wouldimport every entrypoint and bind listeners). Group and leaf help render after
boot and read the command registry
activate()filled.@hypaware/context-graph-enrichshipped the drift:hyp --help)hyp enrich --help)enrichContext-graph enrichment (subcommands: propose, curate, backfill, status)Context-graph enrichmentenrich statusShow enrichment watermarks and prospect/committed countsShow enrichment watermarks and countsSeparately, a plugin the config selects whose
activate()fails isadvertised in top-level help and then reports
hyp: unknown commandwhen run:LLP 0153/0154 only classify plugins the config does not select.
What changed
hyp plugin doctorgainscommand_help_drift(src/core/plugin_doctor/diagnose.js):a declared command whose manifest summary and registered summary differ
(error), a declared command registered
hidden(error), and aregisterGroupdescription no declared command sits under (warn).Verb-projected commands need no extra machinery: a verb registers its CLI
command into the same registry, so
graph neighborsis compared like anyother command.
test/plugins/bundled-command-manifest-agreement.test.js)runs that diff over every plugin in
hypaware-core/plugins-workspace, so thebundled set is held to the check a plugin author gets. Reusing the doctor
rather than writing a second harness is deliberate (LLP 0267 #d2).
hiddencommands are exempt fromcontribution_undeclared. Manifestomission is how an internal command stays out of pre-boot help; the warning
was nagging
@hypaware/claudeand@hypaware/codexabout their hooks.@hypaware/otelstarts itsOTLP source from
activate(), so diagnosing a plugin bound127.0.0.1:4318for real, and failed outright where the daemon already held it. The
capability stub also answered
Symbol.toPrimitive/valueOf/toStringwithitself, so a plugin logging a value read off a required capability blew up in
string conversion and the doctor blamed the plugin.
@hypaware/context-graph-enrichnow registersthe manifest's wording (LLP 0009 says a bare group command's summary should
name its headline subcommands).
src/core/cli/dispatch.js),a fourth miss state beside LLP 0154's three. With it, top-level help's
epilogue ("run it anyway: hyp names the plugin that provides it") is true for
this case too, so the wording did not need hedging.
CommandRegistryimplementations gainlistGroups();RegisteredSnapshotgains
commandDetailsandcommandGroups.Regression tests
test/plugins/bundled-command-manifest-agreement.test.jsfails on thepre-fix tree with two
command_help_drifterrors againstcontext-graph-enrich, and passes after.test/core/dispatch-inactive-plugin.test.jsgains "dispatch miss on aselected plugin whose activate() threw reports unavailable, not unknown",
which prints
hyp: unknown command 'gascity'before the dispatch change.test/plugins/plugin-doctor.test.jsgains six unit tests: summary drift,verb-projected drift, the hidden exemption, declared-but-hidden, the group
warning, and the no-source-start guarantee.
Local
npm test(4287 pass, 0 fail) andnpm run typecheckare green.Design
New:
llp/0267-manifest-and-runtime-command-agreement.decision.md. It extendsLLP 0009 (#d1) and LLP 0154 (#d5); forward-refs added to both.
Fixes#837