Skip to content

Plugin-owned command groups render a header, and the gate now covers them (#1005) - #1222

Merged
philcunliffe merged 2 commits into
masterfrom
fix/issue-1005
Sep 2, 2026
Merged

Plugin-owned command groups render a header, and the gate now covers them (#1005)#1222
philcunliffe merged 2 commits into
masterfrom
fix/issue-1005

Conversation

@philcunliffe

Copy link
Copy Markdown
Contributor

What was wrong

Verified on origin/master at c0daf4e2 by running the real CLI against an
install with both plugins active, not by reading code:

$ hyp client claude-desktop --help
usage: hyp client claude-desktop <subcommand> [args...]

Subcommands:
  install         Configure Claude Desktop end to end: ...

No header, no description. hyp client claude-account --help did the same.
Neither group has a bare command, and neither plugin registered a
CommandGroupRegistration, so renderGroupHelp skipped the header line
(LLP 0214 #d2) and the reader was never told what the group is for.

PR #1004 fixed the three core groups in this state and added a gate, but the
gate sweeps coreRegistry() only, so it was green the whole time these two
were headerless.

What changed

1. The two groups get a voice. @hypaware/claude-desktop and
@hypaware/claude-account each register a group description. --help now
opens with the header and paragraph every core group's does. registerGroup
is metadata only, so nothing new appears in list() and no manifest entry
changes.

2. The gate covers plugin-contributed groups. Rather than a parallel
gate, the existing sweep body in test/core/cli-consistency-gate.test.js
moves into assertGroupHeaders(registry, run) and runs a second time over a
registry built from core plus every bundled plugin's contributions. The
plugin half comes from dryRunActivate (the doctor's own isolated pass:
throwaway paths, inert source registry, nothing binds), which
test/plugins/bundled-command-manifest-agreement.test.js already runs over
this same workspace. The new sweep adds ~0.4s.

Building that merged registry needs each registration's aliases: vector is
an alias of query vector, so hyp vector --help renders correctly today,
and a sweep blind to aliases would report it as a fourth headerless group.
RegisteredSnapshot.commandDetails therefore now carries aliases, read
straight off the registration.

3. Three fixtures. plugin-command-visibility,
claude-desktop-install-helper and claude-desktop-install hand-roll an
activation context; each gained the registerGroup stub the plugins now
call.

Failing before, passing after

Before (source reverted, test kept):

ok 17 - every reachable core group renders a header line, bare command or not
not ok 18 - every reachable bundled-plugin group renders a header line too
  error: 'hyp client claude-account: no bare command and no registered group
          description, so its help has no header'
# pass 25  # fail 1

After:

ok 17 - every reachable core group renders a header line, bare command or not
ok 18 - every reachable bundled-plugin group renders a header line too
# tests 26  # pass 26  # fail 0

Revert proof: git stash of the two plugin files alone reproduces the
failure above; git stash pop restores 26/26.

npm test 5796 pass / 0 fail, npm run typecheck clean,
npm run smoke -- cli_bundled_plugins_activated ok.

Contracts checked

docs/CLI_REFERENCE.md documents both command groups but does not quote the
--help header, and nothing generates or diffs it against CLI output. No
--json shape changes: registerGroup feeds group help only.

No LLP: this realizes LLP 0214 #d2 (Accepted) rather than changing it, and
the new @refs point at it.

Not addressed

hyp claude-desktop --help (the alias prefix) still exits 2 with "unknown
command". Alias prefixes are not group-navigable anywhere in the CLI, core
included, and the core sweep enumerates canonical names for the same reason.
Out of scope here.

Fixes #1005

philcunliffe and others added 2 commits September 2, 2026 16:15
…them (#1005)

`hyp client claude-desktop --help` and `hyp client claude-account --help`
opened on a naked `usage:` line: neither group has a bare command, and
neither plugin registered a `CommandGroupRegistration`, so `renderGroupHelp`
skipped the header and the reader was never told what the group is for.

Both plugins now register a group description (LLP 0214 #d2), so the header
and paragraph render the way every core group's does.

The durable half is the gate. `test/core/cli-consistency-gate.test.js` swept
core group prefixes only, so it was green while these two were headerless.
The sweep body moves into `assertGroupHeaders` and runs a second time over a
registry built from core plus every bundled plugin's dry-run contributions,
so the next plugin cannot repeat this. Building that registry needs the
registration's aliases (`vector` is an alias of `query vector`, and without
it the sweep would report a group that renders correctly), so the doctor's
dry-run snapshot now carries them.

Three test fixtures that hand-roll an activation context gained the
`registerGroup` stub the two plugins now call.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…nd its group summary is platform-honest

Three review findings on the gate extension, all in the sweep's fidelity to
what a real install does.

The merged replay skipped a command whenever its name OR any alias was
already registered. That first-wins skip is right for the `session` trio
(ai-gateway and opencode each guard with `ctx.commands.get`, and a per-plugin
dry run cannot see the other's registry), but wrong for an alias collision:
`CommandRegistry.register` throws on one, the loader records
`activate_failed`, and every registration after it in that plugin's
`activate()` is lost. Swallowing it here would leave the sweep green over a
CLI half of which never registered. The skip now covers the name case only
and asserts on the alias case, naming the plugin, the command, and the alias.

`assert.ok(dry.ok, ...)` dropped `dry.error`, so an unrelated activation
failure surfaced in the group-header test with no cause. It now carries the
kind and message.

The claude-desktop group summary said "on this Mac", rendered unconditionally
on every platform. `install` and `verify` refuse anything but darwin, so the
group help now says "on macOS" and is true wherever it renders.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@philcunliffe

Copy link
Copy Markdown
Contributor Author

Verdict: approve after fixes. 5 findings, all low. 3 fixed and pushed as 0627adea; 2 left as out-of-scope observations.

Reviewed in a detached worktree at f42ccf88f9ef60a6fa4dcd322e9222dd56adc895 with a fresh
npm install, against origin/master at c0daf4e2 (the local master ref is stale).
The substance of the PR is sound and every load-bearing claim in the description holds; the
findings are all in the new gate's fidelity to what a real install does, plus one string.


Part 1: the claims, verified

The defect and the fix, through the real CLI

Reproduced, not read off the diff. A throwaway HYP_HOME with @hypaware/ai-gateway,
@hypaware/claude-account, @hypaware/claude-desktop in plugins[].

At origin/master (c0daf4e2) both groups open on a naked usage line, exit 0:

$ node bin/hypaware.js client claude-desktop --help
usage: hyp client claude-desktop <subcommand> [args...]

Subcommands:
  install         Configure Claude Desktop end to end: ...

client claude-account --help the same. At this head both open with header and paragraph.

registerGroup really is metadata only: hyp --help and hyp client --help are
byte-identical between c0daf4e2 and this head (diff exit 0 on both), and
hyp dev plugin doctor <plugin> --json is byte-identical too. DoctorReport
(src/core/plugin_doctor/types.d.ts:41-51) carries no registered field, so the aliases
addition cannot reach any --json surface. The "no --json shape changes" claim holds.
hyp dev plugin doctor on both plugins: 0 errors, 0 warnings - in particular the
command_help_drift group warning at src/core/plugin_doctor/diagnose.js:381-391 does not
fire, because both manifests declare commands under the described prefix.

The gate extension is non-vacuous, and it does cover every bundled plugin

Three mutation proofs, each run and each restored:

  1. Delete the registerGroup call from claude-desktop/src/index.js (source only,
    test untouched):
    ok 17 - every reachable core group renders a header line, bare command or not
    not ok 18 - every reachable bundled-plugin group renders a header line too
      error: 'hyp client claude-desktop: no bare command and no registered group
              description, so its help has no header'
    
  2. Coverage is not limited to the two plugins this PR touches. Injecting two commands
    under a fresh, undescribed prefix (probe alpha, probe beta) into
    vector-search/src/index.js - untouched by this PR - yields
    error: 'hyp probe: no bare command and no registered group description, ...'.
    bundledRegistry() reads every directory under hypaware-core/plugins-workspace
    (23 today, central and gascity included) and assert.ok(dirs.length > 0, ...) guards
    the empty case, so the sweep cannot go vacuous by finding no plugins.
  3. The vector false-positive the aliases field prevents is real. query vector
    carries aliases: ['vector'] (vector-search/src/index.js:76-85) and vector status
    contributes the prefix vector. Replaying with aliases: [] fails test 18 with
    'hyp vector: no bare command and no registered group description, ...'; reverting
    aliases out of snapshotRegistry (src/core/plugin_doctor/dry_run.js:175-180) fails it
    too. The RegisteredSnapshot change is load-bearing, not incidental.

Test 18 costs 136ms (test 17: 3.9ms); the whole gate file runs in 382ms.

The aliases addition is read correctly and breaks nothing

aliases: [...(c.aliases ?? [])] (dry_run.js:178) reads straight off the registration,
copies rather than aliases the live array, and normalizes the absent case to [], matching
the aliases: string[] the interface now declares (types.d.ts:60-61). The only other
producer, emptySnapshot() (dry_run.js:652-665), already yields commandDetails: []. Both
consumers read only name, summary, hidden: checkCommandHelp (diagnose.js:322) and
registersOnlyHidden (diagnose.js:406). Additive, and typecheck is clean.

Reuse, not duplication

The existing sweep body was extracted to assertGroupHeaders(registry, run) and both tests
call it. No parallel gate, no second harness. The plugin half reuses dryRunActivate +
loadManifest rather than booting - the same isolated pass
test/plugins/bundled-command-manifest-agreement.test.js already runs over this workspace,
whose FATAL assertion (that file, lines 95-97) is the same "a dry run that never finished
cannot pass by registering nothing" guard. So no platform coupling the suite did not already
have (CI is ubuntu-latest only).

Fixes #1005 is the right trailer

Issue #1005 asks for exactly two things: a description for each of the two named plugin
groups, and extending the gate to plugin-contributed groups. Both are delivered and both
are verified above. The issue names only hyp client claude-desktop --help and
hyp client claude-account --help; it never mentions the alias prefix. The deliberately
unaddressed hyp claude-desktop --help is out of that issue's scope, and the justification
checks out: at this head and at origin/master, both hyp claude-desktop --help and
hyp claude-account --help exit 2 with "unknown command", which is how alias prefixes behave
CLI-wide, core included. Closing #1005 on this is honest; Refs would understate it.
closingIssuesReferences reports #1005 and nothing else.

Docs and house style

docs/CLI_REFERENCE.md documents both groups (lines 577-584, 626-666) and quotes only the
usage line, never the header. Nothing generates or diffs it against CLI output - the sole
in-tree mention is a prose comment at src/core/cli/remote_commands.js:1018. No doc edit owed,
as claimed. No semicolons, no em dashes; types stay in JSDoc/.d.ts; package.json
untouched, so no new dependency. LLP 0214#d2 exists
(llp/0214-verbs-and-plugin-groups-carry-long-help.decision.md:70) and both refs are accurate
against what it settled - this realizes the Accepted decision rather than changing it, so no
new LLP is owed. Both group summaries are derived from the manifests' own descriptions.


Part 2: findings

FIXED (pushed as 0627adea)

F1 - low - test/core/cli-consistency-gate.test.js:145. The first-wins guard
if ([command.name, ...command.aliases].some((name) => registry.get(name))) continue
swallowed every collision, including one that is fatal in production. CommandRegistry.register
throws on an alias collision (src/core/registry/commands.js:96-102); the loader lets that
escape, records activate_failed, and abandons the rest of that plugin's activate()
(src/core/runtime/loader.js:107-118) - so every command it would have registered after the
throw vanishes from the real CLI while this sweep stayed green over all of them. The session
case the comment cites is a name collision (ai-gateway/src/index.js:65-85 and
opencode/src/index.js:115, neither carrying aliases), so narrowing the skip keeps it working.

Fixed: the skip now covers the name case only and asserts on the alias case. Proved
non-vacuous by giving opencode's session ignore an aliases: ['status']:

not ok 18 - every reachable bundled-plugin group renders a header line too
  error: |-
    @hypaware/opencode: 'session ignore' claims a name already registered
    (session ignore, status); the real loader aborts the rest of this plugin's
    activate() on an alias collision

F2 - low - test/core/cli-consistency-gate.test.js:138.
assert.ok(dry.ok, '...did not complete...') dropped dry.error, which carries
{ kind, message } (src/core/plugin_doctor/types.d.ts:100-101). An unrelated activation
failure therefore surfaced in the group-header test with no cause, sending the reader down
the wrong path while bundled-command-manifest-agreement (which the comment says owns the
finding) reported the truth. Fixed by including kind and message. Proved by making
opencode's activate throw:

error: @hypaware/opencode: activate() did not complete, so it contributes no group
       to sweep (activate_threw: Error: synthetic activate failure ...)

F3 - low - hypaware-core/plugins-workspace/claude-desktop/src/index.js:92.
The new group summary read "Set up Claude Desktop capture on this Mac", rendered
unconditionally. activate() has no platform gate and the manifest declares no platform
restriction, so on Linux - a first-class target per the release checklist's "one macOS host
and one Linux host" - the help opened by telling the reader they were on a Mac. I confirmed
this by rendering it on Linux. install and verify do refuse anything but darwin
(claude-desktop/src/install.js:132-134, src/verify.js:57-59), so the claim is macOS-specific
but the sentence was not. Now "Set up Claude Desktop capture on macOS", true wherever it
renders.

NOT FIXED (out of scope, recorded)

F4 - low - src/core/cli/group_help.js:33-58, pre-existing and equally true of core.
listGroupChildren synthesizes a child summary from subcommand names and never consults
registry.getGroup, so the parent listing and the group's own help give two different
descriptions of the same group:

$ hyp client --help
  claude-desktop  Subcommands: install, install-helper, profile, status, verify
$ hyp client claude-desktop --help
hyp client claude-desktop - Set up Claude Desktop capture on macOS

This PR adds the summary data that makes the divergence concrete, but it does not cause
it: core's client history registers the summary "Import past sessions from AI clients on
this machine" (src/core/cli/core_commands.js:115-116) and hyp client --help still prints
history Subcommands: import, plan, providers. Same shape, shipped in #1004. Fixing it
means changing listGroupChildren for core and plugins alike - a different change, against a
different contract (LLP 0009 #layered-help), and outside issue #1005. Left for a follow-up
rather than folded in, exactly the way #1005 itself was recorded off #1004.

F5 - low - src/core/plugin_doctor/dry_run.js:181. commandGroups snapshots only
{ name, summary }, dropping help, so the replayed group never carries the paragraph. I
confirmed the consequence: deleting the entire help: array from claude-desktop's
registerGroup leaves both gate tests green while the CLI loses its whole explanatory
paragraph. Not fixed, for two reasons. First it is symmetric and pre-existing: the core sweep
asserts only out.startsWith(header) too, so the paragraph half of LLP 0214#d2 is ungated
for core groups exactly as it is for plugin ones - the tests are named "renders a header
line" and do what they say. Second, carrying help through RegisteredSnapshot without
also widening the assertion would add a schema field nothing reads, which the repo guidance
rules out. Closing it properly means extending assertGroupHeaders to assert the paragraph
for both halves, which is a wider contract change than issue #1005 asks for.


Checks run in the worktree, after the fixes

  • npm test: 5796 pass / 0 fail / 1 skipped
  • npm run typecheck: clean
  • npm run smoke -- cli_bundled_plugins_activated: ok (also walkthrough_picker_to_first_query, status_diagnostics)
  • node --test test/core/cli-consistency-gate.test.js: 26/26
  • the three touched fixture suites plus bundled-command-manifest-agreement: 62/62
  • hyp client claude-desktop --help and hyp client claude-account --help still render their headers; hyp dev plugin doctor on both: 0 errors, 0 warnings

New head: 0627adea174659f64c60ebc7b4ede2761de05741.

@philcunliffe

philcunliffe commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

Verdict: approve. Round 2 found 0 actionable findings; nothing changed, nothing pushed.

Round 2's brief was the safety net itself: round 1 edited the gate, so this round re-proves
the gate rather than re-reading the feature. Reviewed in a fresh detached worktree at
0627adea174659f64c60ebc7b4ede2761de05741 with its own npm install, against origin/master
at c0daf4e2 (the local master ref is stale). Every claim below was executed, not inferred
from the diff; every mutation was restored and the tree verified clean (git diff --quiet)
before the next one.


1. The gate is still non-vacuous after round 1's edit

Round 1 replaced an unconditional continue with an assertion plus a narrowed continue.
The risk it created is that the original defect stopped being caught while the new
assertion looked like progress. It did not.

The original defect, re-caught at this head. Deleted the whole ctx.commands.registerGroup({...})
block from hypaware-core/plugins-workspace/claude-desktop/src/index.js:89-102 (source only,
test untouched):

ok 17 - every reachable core group renders a header line, bare command or not
not ok 18 - every reachable bundled-plugin group renders a header line too
  error: 'hyp client claude-desktop: no bare command and no registered group
          description, so its help has no header'

Restored; back to 26/26. So the narrowing cost the gate nothing on the defect it was built for.

The new alias assertion fires, with a message that names the culprit. The interesting
case is the one that is only an alias collision - a fresh command name whose alias is
already taken - because that is the case the old first-wins continue swallowed. Replaced
aliases: ['claude-desktop install'] on client claude-desktop install
(claude-desktop/src/index.js:158-159) with aliases: ['status'], colliding with core's hyp status:

not ok 18 - every reachable bundled-plugin group renders a header line too
  error: |-
    @hypaware/claude-desktop: 'client claude-desktop install' claims a name already
    registered (status); the real loader aborts the rest of this plugin's activate()
    on an alias collision

Plugin, command, and the colliding token, all three. Restored.

And the production consequence round 1 asserted is real. Confirmed at source, not
assumed: CommandRegistry.register throws on an alias collision
(src/core/registry/commands.js:96-102, a distinct throw from the duplicate-name one at :93-95),
and src/core/runtime/loader.js:107-118 catches whatever escapes await mod.activate(ctx),
records activate_failed, and pushes { ok: false } - every registration the plugin would
have made after the throw is simply gone from the real CLI. Fatal, silent, and previously
invisible to this sweep. Round 1's diagnosis holds.

2. The name-only narrowing lost no coverage

First-wins existed to absorb the duplicate session registrations, and I checked that the
narrowed skip is still live rather than quietly dead. Instrumented the branch and ran the
gate:

SKIP-BRANCH @hypaware/opencode ["session ignore"]
SKIP-BRANCH @hypaware/opencode ["session status"]
SKIP-BRANCH @hypaware/opencode ["session unignore"]
# pass 26  # fail 0

Hit exactly three times, all @hypaware/opencode, all pure name collisions with what
@hypaware/ai-gateway registered first (ai-gateway/src/index.js:65,75,85 each guard with
if (!ctx.commands.get(name)); opencode/src/index.js:115 guards the whole block with
if (ctx.commands.get('session ignore')) return). Neither side carries aliases, so the
narrowing kept the only case it was ever protecting. Zero false positives: the full sweep
stays green, and npm test is 5796/0.

And the replay can't be tightened further without breaking something else. I checked
whether the cleaner fix - seed each dry run with the accumulated registry so the plugins'
own guards fire naturally and no replay heuristic is needed - was available.
dryRunActivate builds its own createCommandRegistry() internally
(src/core/plugin_doctor/dry_run.js:82-88) with no seed option, and that isolation is the
doctor's contract: it diagnoses one plugin against nothing else. So a merged replay with a
first-wins rule is the right shape here, and round 1 narrowed it as far as a per-plugin dry
run can see.

3. Round 1's other two fixes landed correctly

dry.error now reaches the reader. Made @hypaware/opencode's activate() throw:

error: @hypaware/opencode: activate() did not complete, so it contributes no group to
       sweep (activate_threw: Error: synthetic activate failure
           at Module.activate (.../opencode/src/index.js:28:9)
           at dryRunActivate (.../plugin_doctor/dry_run.js:135:17))

Kind and message both present, with the stack as a bonus. Restored.

"on macOS" is accurate. Verified what the group actually does rather than taking the
wording on trust. The manifest's own description targets
/Library/Managed Preferences/com.anthropic.claudefordesktop.plist, and install and
verify both refuse a non-darwin platform outright
(claude-desktop/src/install.js:133-134, src/verify.js:57-59) - only darwin is supported.
The summary now describes the group's domain instead of asserting a fact about the reader's
machine, which is the actual defect "on this Mac" had: it rendered unconditionally on Linux.
grep -rn "this Mac" over src/ and the workspace now returns nothing. claude-account's
summary was already platform-neutral and needed no change.

hyp dev plugin doctor on both plugin directories: 0 errors, 0 warnings.

4. The two deliberate omissions are genuinely pre-existing

Both re-checked against origin/master directly, since "pre-existing" is the whole
justification for leaving them.

listGroupChildren never consults getGroup. hyp client --help is byte-identical
between c0daf4e2 and this head (diff exit 0). The divergence is visible on master already,
in core: master prints history Subcommands: import, plan, providers while
hyp client history --help on master opens hyp client history - Import past sessions from AI clients on this machine. Structurally it cannot be otherwise here - the function's
parameter is typed Pick<CommandRegistry, 'list'> (src/core/cli/group_help.js:29), so it has
no access to the group registry at all. Closing it means widening that signature for core and
plugins alike, against LLP 0009's help contract. Correctly left.

commandGroups drops help. The snapshot line in
src/core/plugin_doctor/dry_run.js:181 is byte-identical to master's - this PR added
aliases to commandDetails on the line above and did not touch commandGroups. Round 1's
reasoning stands: the assertion is out.startsWith(header) for both halves, so the
paragraph is ungated symmetrically, and carrying help through RegisteredSnapshot without
widening the assertion would add a field nothing reads. Correctly left.

5. Fixes #1005 is honest

closingIssuesReferences reports #1005 and nothing else. The alias-prefix case is not a
regression and not in scope: hyp claude-desktop --help and hyp claude-account --help exit
2 with unknown command at both c0daf4e2 and this head, identical output, which is how
alias prefixes behave CLI-wide. The two things #1005 asks for - a description on each of the
two named groups, and the gate extended to plugin-contributed groups - are both delivered and
both proven above. Reproduced the before/after through the real CLI with a throwaway
HYP_HOME: at master client claude-desktop --help opens on a naked usage: line; at this
head it opens on hyp client claude-desktop - Set up Claude Desktop capture on macOS followed
by the paragraph.

LLP 0214#d2 names plugin-owned groups explicitly and settles registerGroup/getGroup
(llp/0214-...decision.md:69-85), so this realizes an Accepted decision rather than changing
one: no new LLP owed. House style clean - no em dashes, no NUL bytes, no semicolons, no
package.json change. The three registerGroup() {} stubs added to existing fake contexts
are the minimum needed to keep those fakes from throwing.


Findings

None actionable. Two observations, both recorded rather than fixed, both consistent with
how round 1 handled #1005's own out-of-scope items:

O1 - informational - test/core/cli-consistency-gate.test.js:145-155. When a command's
name and an alias both collide, the replay reports a fatal alias collision that production
would not have. Confirmed by giving opencode's session ignore an aliases: ['status']:
test 18 fails with claims a name already registered (session ignore, status), while the
real CLI is fine - opencode's if (ctx.commands.get('session ignore')) return returns before
registering anything, and hyp session --help renders normally. So the assertion message's
"the real loader aborts the rest of this plugin's activate()" is wrong for that one shape. It
over-reports rather than under-reports, no instance exists today, and an alias that can never
resolve is arguably worth failing on anyway. Not worth branching a test message for a case
that has never occurred.

O2 - informational - same block. A name collision is still skipped on the assumption
that the losing plugin guards with ctx.commands.get. Every bundled plugin that duplicates a
name does guard today (verified: the only three duplicates are the session trio), but an
unguarded one would throw in production and stay green here. This is inherent to the
per-plugin dry run rather than to round 1's edit - see the dryRunActivate isolation note in
section 2 - and it is strictly better than before, when the alias case was swallowed too.


A note on tooling

The /code-review skill was launched against this PR at high effort at the start of this
round. It had not returned by the time this comment was first posted (~35 minutes, two
follow-up nudges), and this section originally said so. It has since reported, and this
paragraph is the correction.
Everything above was written before its output existed and
stands unchanged: every claim in this comment comes from a command I ran in the worktree, with
mutation output quoted verbatim.

Its result is independent corroboration, not new work. It ran its own clean worktree, got the
same npm test (5796/0/1) and clean typecheck, reproduced the revert proof from the other
direction (reverting both plugin files, failing on client claude-account where I failed
on client claude-desktop), rendered both help pages through real dispatch, and added one
check I had not run: test/plugins/bundled-command-manifest-agreement.test.js 24/24, so the
doctor's "group with no declared command under it" warning is not tripped.

It filed four findings, none blocking, and all four are items already in this comment:

its finding severity where it lands here
name-collision skip cannot tell a guarded re-registration from an unguarded one medium O2, above
[name, alias] both-collide case reports a collision production never has low O1, above
commandGroups drops help, so the sweep proves the header and never the paragraph low section 4, second item
the new descriptions are invisible in hyp client --help low section 4, first item

It rates the name-collision gap medium where I recorded it as informational, and its
reasoning is worth having on the record: a plugin claiming a name core already owns
activates cleanly in the dry run (which has no core commands), so claimed comes back as
exactly [command.name], the replay skips, and the sweep stays green while the real loader
throws and drops that plugin entirely. That is a sharper statement of the hazard than mine.
It does not change the verdict, for the reason given in O2: it is strictly pre-existing and
strictly improved by this PR (before round 1 the alias case was swallowed too), it is latent
rather than live (the only names skipped today are opencode's three genuinely guarded
session * commands, which we both verified independently), and a per-plugin dry run cannot
distinguish the two cases even in principle - a plugin's ctx.commands.get guard would not
fire in the dry run but would fire for real. Closing it needs a different mechanism, not a
tighter assertion.

So: no finding from either reviewer asks anything of this PR, and the marker stays clean.


Checks run in the worktree

  • npm test: 5796 pass / 0 fail / 1 skipped
  • npm run typecheck: clean (exit 0)
  • npm run smoke -- cli_bundled_plugins_activated: ok
  • test/core/cli-consistency-gate.test.js alone: 26/26 in 378ms (test 18 costs ~140ms)
  • hyp dev plugin doctor on claude-desktop and claude-account: 0 errors, 0 warnings

No commit, no push: the head under review is unchanged at
0627adea174659f64c60ebc7b4ede2761de05741.

@philcunliffe

Copy link
Copy Markdown
Contributor Author

Ship risk: low

Who could be affected: Anyone who runs the help pages for the two Claude Desktop setup command groups. No other part of the product is reachable from this change.

What could happen: The intended effect is the only effect found. Two help pages that previously opened on a bare "usage:" line now open with a title and a short paragraph explaining what the subcommands are for. Nothing about capture, recorded data, credentials, configuration, or the background service is touched, and no command changes what it does or what it returns.

Why this level: The new descriptions are labels, not behaviour. They live in a separate description list that the help screen reads and that command routing never consults, so no command can be added, hidden, renamed, or redirected by them. The remaining edits are internal bookkeeping and test-suite coverage that is not part of the published package.

What was checked: The three unrelated help and diagnostic surfaces were run on both the current release base and this change and came back identical word for word, with identical results. The two affected groups were loaded for real and the command list, name lookups, and routing decisions were identical before and after. The full test suite (5,796 passing) and the type check both passed, and the published-package file list was confirmed to contain no test files.

@philcunliffe
philcunliffe added this pull request to the merge queue Sep 2, 2026
Merged via the queue into master with commit 2c0f2c9 Sep 2, 2026
8 checks passed
@philcunliffe
philcunliffe deleted the fix/issue-1005 branch September 2, 2026 18:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Plugin-owned command groups render headerless --help, and the new core gate does not cover them

1 participant