Uh oh!
There was an error while loading. Please reload this page.
The verb registry cannot release a name: add unregister(name) that also retracts the projected CLI command - #875
Conversation
Neither kernel registry supported removal, and `registerVerb` claims a name on two surfaces at once (both verb maps plus an immediately projected CLI command). A host that ships its own implementation of a kernel verb's tool therefore had no way to displace it: hypaware-server #364 feature-detects `verbs.unregister` and degrades to the kernel's local-cache `grep_search` when it is absent. `VerbRegistry.unregister(name)` releases the name from `byName` and the tool from `byTool`, then retracts the CLI command the registration actually projected. Projection is skipped when a command already occupies the name, so the registry now tracks what it projected and retracts only that; a pre-existing same-named command survives. `CommandRegistry.unregister(name)` is the matching removal: it resolves what `get` resolves and clears every alias pointing at the command, so the name and its aliases are claimable again. Both are by-name, idempotent, and total on an unknown name, since the caller runs at daemon boot and a throw there takes boot down. Both methods are declared in `hypaware-plugin-kernel-types.d.ts` so plugins and the server see them in the published types. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`unregister` retracted the projected command only when the name was in a per-registry `projected` ledger, and on the boot path that ledger is empty for exactly the verbs a host wants to displace. `dispatch` runs `registerCoreCommands`, which pre-projects every `CORE_VERBS` command so `hyp --help` renders before boot; boot then builds the runtime over that same command registry, `commandAlreadyRegistered` is true, and the verb registry skips its own projection. Releasing the verb freed both maps and left `hyp query sql` routed at the implementation the host just displaced: archive-backed on MCP, local-cache on the CLI, silently. Retraction is now identity-based. `verbToCommand` records each command it projects in a module-level WeakSet, `isVerbProjection` reports it, and `retractCommand` retracts only a command that is one. That covers the pre-boot projection and the shared-command-registry re-creation case `register`'s own comment anticipates, while a plugin's own same-named command still survives, since it was never a projection. Two regression tests, both failing before the change: core verb retracted after `registerCoreCommands` + `createKernelRuntime` (`hyp query sql` falls back to the bare `query` group), and a projection made by a different verb registry over one shared command registry. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
philcunliffe
commented
Aug 19, 2026
VerdictThe affordance is the right shape and matches #871's contract exactly (by-name, idempotent, total on an unknown name, both maps freed, no policy). One high defect made the headline promise false on the only path that matters: the projected CLI command was not retracted for core verbs at daemon boot. Fixed and pushed as FindingsHIGH - |
Review follow-ups on the `unregister` affordance. `CommandRegistry.unregister` and `VerbRegistry.unregister` are now optional members. The kernel already feature-detects the command-registry half (`retractCommand`), and the consumer this exists for (a server host displacing the kernel-shipped verb) feature-detects the verb-registry half, because plugins declare a kernel semver *range* and can be loaded by a kernel that predates the member. Declaring them required narrowed both checks to always-true for anyone compiling against the published declarations, inviting removal of the guard and turning an older kernel into a boot failure. The concrete factories pin the member as present, so in-repo callers stay unconditional. `retractCommand`'s two tolerated fall-through branches now warn. The caller's prescribed success check is `getByTool`, which the map deletion satisfies on its own, so a half retraction read as a win while `hyp <verb>` kept routing at the run closure of the displaced verb: the silent local-cache regression LLP 0264 verb warns about. Adds a test for the registry-predates-unregister branch, which had none. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
philcunliffe
commented
Aug 19, 2026
neutral review round: |
philcunliffe
commented
Aug 19, 2026
Neutral triage at head
Both review rounds' actionable findings (the HIGH boot-path retraction miss, the required-vs-optional |
Uh oh!
There was an error while loading. Please reload this page.
Root cause
registerVerbclaims a name on two surfaces at once:byName+byToolin the verb registry, and a CLI command projected into the command registry on the spot. Neither registry supported removal, so a claimed name could never be given back.That blocks the grep-search work. hypaware-server #364 registers its own archive-backed
grep_searchand displaces a kernel-shipped twin throughverbs.unregisterwhen the kernel offers it, re-checkinggetByToolrather than trusting the call. Without the affordance, oncehyp query greplands every server host boots a kernel twin holding thegrep_searchslot and answers from the local cache only: no org BlobStores, no export ledger, no archive fan-out (LLP 0264 §verb calls shipping the kernel half alone "a regression on every server host").The fix
VerbRegistry.unregister(name)(src/core/registry/verbs.js)byNameandbyTool, so the tool slot is genuinely free and a re-register succeeds instead of throwing "already registered";register()skips projection when a command already occupies the name (commandAlreadyRegistered), so the retraction tests identity, not name:src/core/cli/verb_command.jsrecords every command it projects in a module-levelWeakSet<CommandRegistration>and exportsisVerbProjection(command), which gates the removal. A per-registryprojectedledger was the first shape and does not work: core verbs are projected before any registry-level ledger exists, so it reads empty for exactly the commands that need retracting. A pre-existing same-named command fails the identity test and is left alone;CommandRegistry.unregister(name)(src/core/registry/commands.js) is the matching removal: it resolves whatgetresolves (primary name or alias) and clears everyaliasIndexentry pointing at the removed command, so the name and its aliases are claimable again andmatch(argv)no longer routes at a command that is gone. Verb projections carry no aliases today (verbToCommandemits none), but the registry-level contract has to stay correct for commands that do.Both are declared on the
VerbRegistryandCommandRegistryinterfaces inhypaware-plugin-kernel-types.d.ts, so plugins and the server see them in the published types. A verb registry built over a command registry that predatesunregisterstill releases both maps (the projected command is the only thing left behind), mirroring howcommandAlreadyRegisteredalready tolerates a registry withouthas.No policy is added: nothing here decides who wins a contested tool name. That stays the server's call in its LLP 0178.
The tests that prove it
12 new tests, all failing on
masterbefore the change (verbs.unregister is not a function/commands.unregister is not a function) and passing after.test/core/verb-registry.test.jslist()get,has,listall clear)test/core/command-registry-unregister.test.js(new)get/has/list/sizeall clearmatch(['d'])no longer routing, and other commands' aliases untouchedgetaccepts onehyp --helpno longer lists the retracted command, driven throughdispatch(['--help'], ...)npm testis green (4495 pass, 1 skipped, 0 fail) andnpm run typecheckis clean in the worktree.Fixes#871