fix: third-party capability skills surface correctly (#2045)

A skills-only role:feature third-party capability installed 'active' but its skills never reached the runtime surface, capability enable/set rejected it as 'unknown capability', and capability list disagreed with capability state. Three defects, fix shape 1b (teach resolveSurface, no on-disk linking):

D1 (materialization): resolveSurface built the surfaced skills Set only from the on-disk manifest; third-party cap skills live at ~/.gsd/capabilities/<id>/skills/ and never entered the Set -> surfaced:false. Fix: union registry.capabilityClusters values into the Set in the full-profile branch (idempotent for first-party, additive for third-party; prototype-pollution guarded).

D2 (enable/set unknown): setCapabilityState validated the capId against the static first-party registry instead of the composed overlay-aware registry. Fix: validate against loadRegistry({includeInstalled}) like capability-state.cts does.

D3 (list vs state): capability list derived status purely from ledger existence, never surface composition. Fix: add a 'surfaced' field to list rows sourced from the same resolver capability state uses, so list and state agree.

Regression coverage: tests/issue-2045-third-party-skills-surface.test.cjs asserts all five acceptance criteria (resolveSurface union, state surfaced:true, enable/set not-unknown, list/state agreement, first-party + unknown-id regression). gsd-test: 23916/23916 green (linux-node22 + linux-node24).
This commit is contained in:
Tom Boucher
2026-07-07 08:11:44 -04:00
parent 2372a221eb
commit 847de596b8
21 changed files with 352 additions and 17 deletions

View File

@@ -151,8 +151,17 @@ function setCapabilityState(
const resolvedConfigDir = before.runtimeConfigDir;
// ── Load registry ─────────────────────────────────────────────────────────
// Issue #2045 (DEFECT 2): validate against the COMPOSED overlay-aware registry
// (first-party ∪ accepted overlays), mirroring capability-state.cts:547-551.
// The frozen capability-registry.cjs only knows first-party ids, so a third-
// party cap failed the membership check below → "unknown capability" even
// though resolveCapabilityRuntimeState (the `before` snapshot, line 150) already
// knew about it. loadRegistry is non-throwing and first-party-wins, so a
// malformed overlay is skipped (never crashes the writer); a truly-unknown id
// is STILL rejected because it is absent from the composed capabilities map.
// eslint-disable-next-line @typescript-eslint/no-require-imports
const registry = require('./capability-registry.cjs') as Record<string, unknown>;
const { loadRegistry } = require('./capability-loader.cjs') as { loadRegistry: (opts?: Record<string, unknown>) => Record<string, unknown> };
const registry = loadRegistry({ includeInstalled: true, cwd, gsdHome: process.env['GSD_HOME'] });
const capabilitiesMap = (
registry['capabilities'] && typeof registry['capabilities'] === 'object' && !Array.isArray(registry['capabilities'])
? registry['capabilities']