Skip to content

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

Merged
philcunliffe merged 5 commits into
masterfrom
fix/issue-297
Jul 14, 2026
Merged

Dispatch-miss repair line branches on why a plugin is inactive (LLP 0099, issue #297)#298
philcunliffe merged 5 commits into
masterfrom
fix/issue-297

Conversation

@philcunliffe

Copy link
Copy Markdown
Contributor

What

Follow-up to PR #296 (deferred review finding). Fixes the dispatch-miss repair line when a plugin entry exists but is disabled.

LLP 0098 (shipping in #296) made a dispatch miss on a known-but-inactive plugin command print add {"name": "<plugin>"} to plugins[]. That advice 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 (computeSelectedPlugins skips enabled === false), so the miss path fires and tells the user to add an entry that already exists (issue #297).

Fix

findInactivePluginForCommand now classifies the inactive plugin into three states and emits a case-appropriate repair:

The disabled entry belongs to central iff central declares its name, because the additive merge model keys plugins[] by name and drops a local entry colliding with a central one (collides_with_central), so a fleet-disabled plugin cannot be enabled locally (@ref LLP 0031#merge-model). computeBootSelection now also returns the resolved layered config, so the miss path reads the effective + central documents it already resolved - no extra I/O. The first output line and LLP 0098's exact-match / inactive-only / boot-selection-reuse constraints are unchanged.

Ground-truth (fail -> pass)

Two regression tests added alongside test/core/dispatch-inactive-plugin.test.js. On the unmodified src they fail; with the fix they pass.

Assertion (disabled-local): /^ {2}repair: set "enabled": true on the \{"name": "@hypaware\/context-graph"\} entry in plugins\[\] in /m

  • Old output: repair: add {"name": "@hypaware/context-graph"} to plugins[] in <path> (wrong: entry already exists)
  • New output: repair: set "enabled": true on the {"name": "@hypaware/context-graph"} entry in plugins[] in <path>

npm test: 2148 pass, 0 fail, 1 skipped. tsc -p tsconfig.build.json clean.

LLP

Mints LLP 0099 (decision, Accepted) extending LLP 0098's repair wording (@ref LLP 0098 / @ref LLP 0031); adds an Extended-by: LLP 0099 forward-ref on immutable LLP 0098. Code annotated with @ref LLP 0099#decision [implements].

Stacking

Stacked on fix/issue-294 (PR #296), which is not yet merged and carries the LLP 0098 code this change extends. Retarget to master (or merge after #296 lands) - base will collapse to just this commit once #296 merges.

Fixes #297

philcunliffe and others added 4 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>
…099, 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>
…t 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.
@philcunliffe

Copy link
Copy Markdown
Contributor Author

neutral PR-review round 1 — PR #298 (own neutral PR; stacked on #296)

Path: dual-review (Codex + Claude, both ran — not a Codex fallback). Verdict: approve. Risk class: low (single-file CLI dispatch-miss error-path logic + a new decision LLP + regression tests). Reviewed head 8f91f91.

Verified (not trusting self-report)

  • Central-vs-local classification is sound. classifyInactiveState decides "disabled by central" iff centralConfig.plugins declares the name — an exact discriminator given the additive, name-keyed merge model (src/core/config/merge.js, @ref LLP 0031#merge-model: a local entry colliding with a central-declared name is dropped). All layer combinations check out (central-disabled → disabled-central; local-disabled → disabled-local; central-enabled + local-disabled → plugin is active, never reaches classification; no config → absent). Shadow/excluded-skeleton cases are gated out by the shadowing.length > 0 return and the selected.has(name) skip.
  • Absent-plugin output is byte-identical to LLP 0098's pinned wording (issue CLI: commands of inactive bundled plugins should report 'unavailable' with a repair line, not 'unknown command' #294) — confirmed empirically by reverting the branching.
  • computeBootSelection change is I/O-neutral (returns the already-resolved layered config; other caller collectPluginHelpCommands unaffected via the spread).
  • Tests genuinely gate the fix: both new disabled-case tests fail on the old single-branch output and pass on the new code; central-disabled sub-case covered; 5/5 pass. @ref anchors (0099#decision, 0098, 0031#merge-model) resolve.

Findings (both minor; both fixed this round)

  1. [minor] @typedef in src/core/cli/dispatch.js — CLAUDE.md prohibits @typedef in JSDoc. Fixed (870887e): the InactiveState typedef removed, the union 'absent' | 'disabled-local' | 'disabled-central' inlined at both use sites (L740, L784). Verified in the committed tree (grep @typedef → none).
  2. [minor] absent-case test used a prefix/regex assertion — wouldn't catch drift on the LLP-0098-pinned repair line. Fixed (870887e): tightened to a byte-exact line equality (test L89). The two disabled-case tests untouched.

npm test green (2148 pass, 0 fail, 1 skipped), tsc -p tsconfig.build.json clean at the fix head. Head moved 8f91f91870887e; next tick re-reviews the new head (expected clean → ready-hold).

Note for the human merger: this PR is stacked on PR #296 (base fix/issue-294). Merge #296 first; GitHub then auto-retargets #298 to master.

@philcunliffe

Copy link
Copy Markdown
Contributor Author

neutral PR-review round 2 (confirmation) — PR #298 — clean

Light confirmation review of the moved head 870887e. The only delta since the round-1 reviewed head 8f91f91 is exactly the two round-1 fixes, verified against the committed tree — no new logic:

  1. src/core/cli/dispatch.js: the @typedef InactiveState block removed and the union 'absent' | 'disabled-local' | 'disabled-central' inlined at both use sites (findInactivePluginForCommand return, classifyInactiveState return). Resolves round-1 finding [codex] Add durable cache spool #1 (CLAUDE.md @typedef prohibition).
  2. test/core/dispatch-inactive-plugin.test.js: the absent-case repair assertion tightened from prefix/regex to a byte-exact assert.equal against the LLP-0098-pinned line. Resolves round-1 finding [codex] Remove OpenTelemetry npm dependencies #2. The two disabled-case tests untouched.

CI green on 870887e (typecheck + test SUCCESS), MERGEABLE. Both round-1 findings fixed; nothing new actionable. Marker = clean → reviewed rung satisfied at the current head; PR is terminal (mergeable ∧ green ∧ reviewed) and holds for a human.

Merge order: this PR is stacked on PR #296 (base fix/issue-294). Merge #296 first; GitHub then auto-retargets #298 to master.

@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
Base automatically changed from fix/issue-294 to master July 14, 2026 19:28
# Conflicts:
#	llp/0098-inactive-not-unknown-dispatch-miss.decision.md
#	src/core/cli/dispatch.js
#	test/core/dispatch-inactive-plugin.test.js
@philcunliffe philcunliffe removed 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 ece58a8 into master Jul 14, 2026
4 checks passed
@philcunliffe philcunliffe deleted the fix/issue-297 branch July 14, 2026 19:42
@philcunliffe

Copy link
Copy Markdown
Contributor Author

Neutral review round — verdict: approve (clean)

Reviewed head b3e0c5b (CI green, 4/4). Method: dual-review — Codex plus two independent Claude review subagents (bug-scan/contract and guidance/LLP/history), run in an isolated worktree at the head SHA. Codex ran to completion this round (no gateway interception). No actionable findings from any reviewer; nothing to fix, nothing pushed.

Scope: src/core/cli/dispatch.js (+ new helpers findInactivePluginForCommand, classifyInactiveState, computeBootSelection, displayConfigPath) and the new test/core/dispatch-inactive-plugin.test.js, plus decision docs LLP 0098/0099. Blast radius: low — the change is confined to the top-level dispatch-miss error path; active-command dispatch and --help synthesis reuse the same computeBootSelection refactor with no behavior change.

What was verified (each held up):

  • Classification soundness under the additive-merge model (LLP 0031). classifyInactiveState reads layered.effective.plugins and layered.centralConfig.plugins. Because the merge keys plugins[] by name and drops a local entry colliding with a central name, centralConfig membership is an exact central-vs-local discriminator. The central-enabled + local-disabled case cannot misclassify: central-enabled wins the merge, the plugin activates, and it never reaches the miss path.
  • No non-config mislabeling. Under bootProfile:'config', a pool-but-unselected plugin is unselected only for a config reason (absent or enabled:false); shadow collisions early-return undefined before classification, and excluded-skeleton plugins correctly map to absent (whose "add it" advice does activate them).
  • findInactivePluginForCommand preserves LLP 0098's exact head-token match and pool-order first-writer precedence; active plugins' commands match earlier in dispatch (selected.has(name)continue) and never reach here. Leading - / empty tokens are rejected; all failure modes degrade to "no suggestion".
  • displayConfigPath handles undefined/empty HOME (env.HOME ?? os.homedir(), guarded by home &&); os is imported; env is in scope at both call sites.
  • @refs resolve. LLP 0099#decision, LLP 0031#merge-model, and LLP 0098 [constrained-by] all point at real anchors. PluginName is @imported. Code's three-way branch and each repair: string match LLP 0099's Decision section byte-for-byte.
  • LLP metadata clean. Both docs **Type:** decision / .decision.md / **Status:** Accepted; LLP 0098 gained the **Extended-by:** LLP 0099 forward-ref; no surviving [inferred] claims.
  • Guidance compliance. No em dashes (U+2014) in any changed file (byte-grep), no semicolons introduced, JSDoc-only types, no inline import() types, root-anchored .js type specifiers.
  • Tests. All three states (absent / disabled-local / disabled-central) plus typo and active-plugin cases are exercised; the central-layer test's seed path matches resolveCentralLayerPath so it genuinely loads a central layer; assertions are byte-meaningful (exact-equality on the absent repair line, anchored full-line regexes plus negative assertions elsewhere). Ran the new file locally: 5/5 pass.

No changes were made to the PR; head remains b3e0c5b.

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.

Follow-up: deferred review findings from PR #296

1 participant