Skip to content

CLI: inactive bundled plugin command reports unavailable + repair, not unknown (LLP 0098)#296

Merged
philcunliffe merged 2 commits into
masterfrom
fix/issue-294
Jul 14, 2026
Merged

CLI: inactive bundled plugin command reports unavailable + repair, not unknown (LLP 0098)#296
philcunliffe merged 2 commits into
masterfrom
fix/issue-294

Conversation

@philcunliffe

@philcunliffe philcunliffe commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

What

When hyp <cmd> names a command that a bundled/installed plugin declares in its manifest but the effective config does not activate, the dispatcher used to print the generic hyp: unknown command '<cmd>'. The command is not unknown, it is unavailable: the process already knows which plugin provides it (the same manifest catalog --help synthesis reads) and knows it was left inactive.

This adds findInactivePluginForCommand to src/core/cli/dispatch.js so a dispatch miss on a known-but-inactive plugin command reports an unavailable state naming the providing plugin, plus a repair line, instead of unknown command. A genuine typo still gets the generic unknown-command message, and commands whose plugin is active are unaffected. Rationale is captured in llp/0098-inactive-not-unknown-dispatch-miss.decision.md (annotated via @ref LLP 0098#decision [implements]).

Test

test/core/dispatch-inactive-plugin.test.js gates the fix. Verified under ground-truth discipline: with the source change stashed the test FAILS (asserts on the old unknown command 'graph' output); with the fix in place it PASSES. Full npm test is green (2146 pass, 0 fail, 1 skipped).

Fixes #294

🤖 Generated with Claude Code

philcunliffe and others added 2 commits July 10, 2026 12:26
…+ repair, not unknown (LLP 0098)

Fixes #294

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…296)

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

Copy link
Copy Markdown
Contributor Author

Dual-agent review - approve

  • Verdict: approve
  • Risk class: low
  • Auto-merge advisory: 👎 thumbs down - own neutral PR; requires human merge gate

Advisory only: no merge was attempted. Both reviewers ran (Codex + Claude); one shared minor finding, no blocker/major.

Risk capstone

Single-file behavior change on the CLI top-level dispatch-miss error path (src/core/cli/dispatch.js). New findInactivePluginForCommand + computeBootSelection helper (extracted from the existing collectPluginHelpCommands, which is refactored to reuse it) run the same cheap manifest discovery --help already uses, only when a command fails to match. No change to the success dispatch path, boot activation, or config resolution. Well covered by three new tests (inactive/typo/active). Blast radius: low.

Codex review

Fix Validations

Inactive bundled plugin command was reported as unknown

  • Status: correct
  • Evidence: src/core/cli/dispatch.js:245, src/core/cli/dispatch.js:248, test/core/dispatch-inactive-plugin.test.js:51
  • Assessment: The miss path now checks the inactive plugin manifest pool after registry/group matching and emits the unavailable-provider message with exit code 2. The new regression test covers the issue shape and asserts the generic unknown command text is not emitted.

Genuine typos must remain unknown

  • Status: correct
  • Evidence: src/core/cli/dispatch.js:720, src/core/cli/dispatch.js:735, test/core/dispatch-inactive-plugin.test.js:91
  • Assessment: The lookup is exact on the first declared command word, so grahp still falls through to the generic unknown-command path.

Findings

2) Contract & Interface Fidelity

  • Severity: minor
  • Confidence: medium
  • Evidence: src/core/cli/dispatch.js:730, src/core/cli/dispatch.js:732, src/core/cli/dispatch.js:251, src/core/runtime/boot.js:551, src/core/runtime/boot.js:552, src/core/config/validate.js:261
  • Why it matters: A plugin explicitly present as { name, enabled: false } is “not selected,” so the new path will tell the user to add the same plugin to plugins[], which can create a duplicate entry that the config validator rejects.
  • Suggested fix: Distinguish absent-from-config from present-but-disabled before rendering the repair line; for disabled entries, suggest setting enabled to true or removing enabled: false.

No Finding

  1. Behavioral Correctness
  2. Change Impact / Blast Radius
  3. Concurrency, Ordering & State Safety
  4. Error Handling & Resilience
  5. Security Surface
  6. Resource Lifecycle & Cleanup
  7. Release Safety
  8. Test Evidence Quality
  9. Architectural Consistency
  10. Debuggability & Operability

Evidence Bundle

  • Changed hot paths: dispatch() unknown-command branch at src/core/cli/dispatch.js:217, inactive lookup at src/core/cli/dispatch.js:719, boot-selection reuse at src/core/cli/dispatch.js:679, help synthesis at src/core/cli/dispatch.js:636.
  • Impacted callers: top-level help calls runHelp with discovery at src/core/cli/dispatch.js:143 and src/core/cli/dispatch.js:152; ordinary command dispatch boots config-profile plugins at src/core/cli/dispatch.js:170; unknown command/group paths at src/core/cli/dispatch.js:217.
  • Impacted tests: new inactive/typo/active tests at test/core/dispatch-inactive-plugin.test.js:51, test/core/dispatch-inactive-plugin.test.js:91, test/core/dispatch-inactive-plugin.test.js:120; existing disabled-help contract at test/core/command-dispatch.test.js:231; existing generic unknown test at test/core/command-dispatch.test.js:575.
  • Unresolved uncertainty: I did not run the test suite; review is based on the supplied diff plus targeted caller/contract tracing.
Claude review

Claude review

Independent scan (guidance-compliance, shallow-bug, contract/callers, tests): no
blocker/major issues. Verified helpDiscovery/env are in scope at the miss path
(dispatch.js:121,136), selectBootPlugins return shape (pool/selected/selectedManifests/
shadowing) matches every consumer, pool ordering (bundled→excluded→installed) matches the
documented first-writer precedence, @ref targets LLP 0005/0009/0031/0098#decision all
resolve, and the three new tests cover inactive/typo/active. No semicolons, no em dashes.

Corroborates the one Codex minor below (present-but-disabled repair line); no additional
findings.

Repair line is wrong for a present-but-disabled plugin entry

  • Severity: minor
  • Confidence: 85
  • Evidence: src/core/cli/dispatch.js:245-252 (findInactivePluginForCommand + repair render); reachable because pool is discovery-based while selected honors enabled:false (boot.js:460-470); duplicate rejected at src/core/config/validate.js:271 (duplicate_plugin)
  • Why it matters: A plugin present as { name, enabled: false } is in pool but not selected, so the miss path prints "add {name} to plugins[]" while it is already there disabled; following the hint literally creates a duplicate entry the validator rejects.
  • Suggested fix: classify absent vs present-but-disabled (needs the effective config's plugins[] names) and, for disabled, advise setting "enabled": true instead of adding a second entry. Judgment-heavy: LLP 0098 is Accepted and pins the exact repair wording, so this likely wants a follow-up request rather than an in-place edit.

@philcunliffe

Copy link
Copy Markdown
Contributor Author

neutral PR-review round - PR #296 (own neutral PR, not adopted)

Path: dual-review (Codex + Claude, both ran). Verdict: approve - one shared minor, no blocker/major. Risk class: low (single-file logic on the CLI dispatch-miss error path; three new tests cover inactive/typo/active).

Findings

1. [minor] Repair line is wrong for a present-but-disabled plugin entry - src/core/cli/dispatch.js:245-252

  • The miss path matches any plugin that is in the boot pool but not selected. Pool membership is discovery-based (bundled/installed on disk), while selected honors enabled:false (src/core/runtime/boot.js:460-470). So a plugin present in config as { "name": "...", "enabled": false } is matched, and the repair prints add {"name": "..."} to plugins[] - but it is already in plugins[], disabled. Following the hint literally produces a duplicate entry the validator rejects (src/core/config/validate.js:271, duplicate_plugin).
  • Correct repair for that sub-case: set "enabled": true on the existing entry, not add a second one.
  • Not fixed this tick (judgment-heavy). The motivating issue CLI: commands of inactive bundled plugins should report 'unavailable' with a repair line, not 'unknown command' #294 (plugin absent from config) is fully and correctly handled; only the explicitly-disabled sub-case is misleading. A fix needs new user-facing wording for a case LLP 0098 (Status: Accepted, immutable per house rules) did not contemplate - its decision text pins the exact two-line add ... to plugins[] output. Cleanest as a follow-up request (new issue/spec @ref-ing 0098) rather than an in-place edit by a review worker. Left noted per review policy.

Verified clean (independent Claude scan)

  • helpDiscovery and env are both in scope at the miss path (dispatch.js:121,136); displayConfigPath(helpDiscovery.configPath, env) resolves.
  • selectBootPlugins return shape (pool / selected: Set / selectedManifests / shadowing) matches every consumer; pool order (bundled -> excluded -> installed) matches the documented first-writer precedence used for head-token collisions.
  • Exact-match-only + startsWith('-') guard: genuine typos and flags fall through to the generic message (test 2 confirms).
  • @ref targets LLP 0005 / 0009 / 0031 / 0098#decision all resolve; ## Decision anchor present in 0098. Code style OK (no semicolons, no em dashes).

Nothing pushed; head unchanged at 9e2332b. Marker = findings (real minor found, left noted, not fixed).

@philcunliffe

Copy link
Copy Markdown
Contributor Author

Round 2 review (independent second pass) — verdict: approve with one deferred minor

Independent read of the diff on head 9e2332b (unchanged since round 1): src/core/cli/dispatch.js (findInactivePluginForCommand, the collectPluginHelpCommandscomputeBootSelection refactor, displayConfigPath, and the miss-path branch), llp/0098-*.decision.md, and test/core/dispatch-inactive-plugin.test.js. Affected test re-run green (3/3). No new blocker or major found; the diff is correct, safe, and matches LLP 0098.

Confirmations:

  • Refactor is behavior-preserving. collectPluginHelpCommands(discovery) now delegates to the extracted computeBootSelection; same discovery + catalog + resolveLayeredConfigFromDisk + selectBootPlugins('config') path, same shadow-collision → [] short-circuit. The !selection guard is defensive-only (the helper always returns an object or throws into the outer try). No functional change vs. round 1.
  • Exact-match narrowness holds. cmd.name.split(' ')[0] === token with the leading--/empty-token guard; typos fall through to the generic unknown command message (test 2 proves it). Active-plugin commands never reach the miss path (test 3). Pool-order walk (bundled → excluded → installed) matches boot first-writer precedence.
  • No security/contract issue. token/plugin are echoed only to stderr (no shell); displayConfigPath correctly collapses $HOME~; helpDiscovery/env are in scope; exit code stays 2. Extra discovery cost is on the error path only.

Confirmed round-1 minor (real, non-blocking, deferred)

src/core/cli/dispatch.js miss path (repair line ~L256) + findInactivePluginForCommand (~L700-720): a plugin present in config but disabled ({ "name": "...", "enabled": false }) lands in pool but not selected (computeSelectedPlugins skips entry.enabled === false, boot.js ~L556), so it matches the inactive lookup and the repair advises add {"name": "..."} to plugins[] — but it is already there, disabled. Correct advice for that sub-case is set "enabled": true on the existing entry. Empirically reproduced this round (disabled entry → repair: add {"name": ...}). The absent-plugin case (issue #294's actual scope) is handled correctly and is well tested.

Non-blocking because: (a) it is outside #294's scope (absent plugin, not disabled entry); (b) the advised duplicate entry would still re-enable the plugin (the merge adds on the enabled !== false entry), so it is misleading wording, not a broken repair; (c) the exact repair wording is pinned by LLP 0098 (Status: Accepted, immutable per house rules), so the correct fix is a follow-up request @ref LLP 0098, not an in-place edit here. Triage will file the follow-up issue; this PR should ship.

New findings

None actionable. Recording findings solely to preserve the deferred round-1 minor above.

@philcunliffe

Copy link
Copy Markdown
Contributor Author

Round 3 (light confirmation review, per maxReviewRounds=4). Head is byte-identical to rounds 1-2 (9e2332b) — no new commits to re-review.

Confirmed: the diff to src/core/cli/dispatch.js (miss-path findInactivePluginForCommand/computeBootSelection/displayConfigPath refactor) is unchanged and still correct for issue #294's scope. No new blocker/major findings on the error-path branch or the collectPluginHelpCommands refactor.

The one known minor still stands (unresolved, deliberately deferred, non-blocking):

src/core/cli/dispatch.js, findInactivePluginForCommand (~line 730) walks selection.pool and treats any plugin not in selection.selected as "inactive," including one that's present in config.plugins with "enabled": falsecomputeSelectedPlugins in src/core/runtime/boot.js (for (const entry of config.plugins) { if (entry.enabled === false) continue ... }) skips disabled entries from selected the same way it skips absent ones. So for a disabled (not absent) plugin, the repair line says add {"name": "<plugin>"} to plugins[], which is misleading — the entry is already there; the correct fix is set "enabled": true. Non-blocking and out of scope for #294 (which only covers the absent-plugin case, handled correctly); blocked on LLP 0098 immutability, to be addressed via a follow-up request (@ref LLP 0098).

No other findings.

@philcunliffe

Copy link
Copy Markdown
Contributor Author

Round 4 review (final round, maxReviewRounds=4) — verdict: approve with one deferred non-blocking minor

Independent final read of the full diff on head 9e2332b (byte-identical to rounds 1-3; no commits since). Reviewed src/core/cli/dispatch.js (the miss-path branch, findInactivePluginForCommand, the collectPluginHelpCommandscomputeBootSelection extraction, displayConfigPath), llp/0098-*.decision.md, and test/core/dispatch-inactive-plugin.test.js.

Confirmed correct and safe for issue #294's scope (absent plugin):

  • Miss-path lookup walks selection.pool, skips selection.selected, and exact-matches cmd.name.split(' ')[0] === token; the typeof/length/startsWith('-') guard sends genuine typos and flags to the generic unknown command message. Shadow-collision → no suggestion (matches boot throwing).
  • computeBootSelection extraction is behavior-preserving: same discovery + catalog + resolveLayeredConfigFromDisk + selectBootPlugins('config') path; collectPluginHelpCommands delegates through it with a defensive !selection guard.
  • displayConfigPath collapses $HOME~ on a correct path.sep boundary; token/plugin echoed only to stderr; exit code stays 2; extra discovery cost is on the error path only.
  • @ref LLP 0098#decision [implements] resolves; code style clean (no semicolons, no em dashes).

Known non-blocking minor (unchanged, deferred by design): a plugin present in config but disabled ({"name":"...","enabled":false}) lands in pool but not selected, so it matches the inactive lookup and the repair advises add {"name":"..."} to plugins[] — misleading, since the entry already exists (correct advice: set "enabled": true). Out of #294's scope (absent plugin, handled correctly and well-tested) and blocked on LLP 0098 immutability, which pins the exact two-line repair output — so the fix is a follow-up request @ref LLP 0098, not an in-place edit by a review worker.

No new findings. Review rounds now exhausted (4/4) with only the one non-blocking, out-of-scope minor open → the PR ships; triage files the follow-up. Nothing pushed; head unchanged at 9e2332b. Marker = findings.

@philcunliffe

Copy link
Copy Markdown
Contributor Author

Triage (rounds exhausted 4/4): ships. The one residual review finding was independently classified non-blocking (judgment-tier) — a misleading-but-not-broken repair hint on the dispatch-miss error path, out of issue #294's scope, blocked from an in-PR fix by immutable LLP 0098. No production risk (no crash, data loss, security, or perf; no boot path hard-fails).

Deferred to follow-up issue #297 (labelled neutral:fix). This PR is reviewed and clear to proceed to ready-hold.

@philcunliffe philcunliffe added the neutral:approved neutral reviewed this and holds it for a maintainer merge (own or adopted PR; LLP 0025/0030) label Jul 14, 2026
@philcunliffe philcunliffe merged commit b9409ca into master Jul 14, 2026
4 checks passed
@philcunliffe philcunliffe deleted the fix/issue-294 branch July 14, 2026 19:28
philcunliffe added a commit that referenced this pull request Jul 14, 2026
…099, issue #297) (#298)

* Dispatch miss on inactive bundled plugin command: report unavailable + repair, not unknown (LLP 0098)

Fixes #294

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

* Fix JSDoc @param after collectPluginHelpCommands refactor (fix-ci for #296)

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

* Dispatch-miss repair line branches on why a plugin is inactive (LLP 0099, issue #297)

The dispatch-miss availability hint (LLP 0098) always advised
`add {"name": "<plugin>"} to plugins[]`. That is correct only when the
plugin is absent from plugins[]. When the effective config already contains
`{"name": "<plugin>", "enabled": false}` the entry lands in the boot pool but
not the selected set, so the miss path fires and told the user to add an entry
that already exists (issue #297, deferred from PR #296).

Classify the inactive plugin into absent / disabled-local / disabled-central
(the disabled entry belongs to central iff central declares its name, per the
additive merge model in LLP 0031) and emit a case-appropriate repair:
- absent: unchanged, byte-identical to LLP 0098.
- disabled-local: set "enabled": true on the existing entry.
- disabled-central: the fleet config disables it; it cannot be enabled locally.

computeBootSelection now also returns the resolved layered config so the miss
path can read the effective + central documents it already resolved (no extra
I/O). Mint LLP 0099 extending LLP 0098's repair wording; add an Extended-by
forward-ref on 0098.

Regression tests (fail on the old single-line output, pass now) cover the
disabled-local and disabled-central cases alongside the existing absent case.

@ref LLP 0099, LLP 0098

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

* PR #298 review: inline InactiveState union (drop @typedef), byte-exact absent-case test assertion

CLAUDE.md forbids @typedef in JSDoc; inline the 'absent'|'disabled-local'|
'disabled-central' union literal at each of its three use sites in
dispatch.js instead of naming it. Also tighten the absent-plugin repair-line
assertion in dispatch-inactive-plugin.test.js to a byte-exact full-line
equality check, so any drift in the LLP-0098-pinned wording (issue #294)
fails the test instead of slipping past a prefix/regex match.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

neutral:approved neutral 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.

CLI: commands of inactive bundled plugins should report 'unavailable' with a repair line, not 'unknown command'

1 participant