* feat(#1431): runtime capability registry overlay (ADR-1244 Phase 2) Promote the registry from a frozen data file to loadRegistry({includeInstalled}), composing the first-party registry with a validated installed overlay (ADR-1244 D2): - Extract the conformance validator to a shared runtime-callable module (gsd-core/bin/lib/capability-validator.cjs); the generator re-exports it verbatim, guarded by a generative-parity test (no build-time/runtime drift). - capability-loader.cts: loadRegistry({includeInstalled}) composes first-party ∪ validated overlay from $GSD_HOME/.gsd/capabilities (global) and <root>/.gsd/capabilities (project) via the canonical buildRegistry. First-party always wins (id/skill/agent/config/command-family + reserved gsd-/anthropic- prefixes); full merged-set cross-capability validation; engines.gsd load-time re-gate (skip-with-warning); gate-kind capabilities FAIL CLOSED; fragment-path escapes rejected. - semverSatisfies (hand-written, no dep) for the engines.gsd gate, fail-closed. - Wire surface/state + loop to the overlay; loop injects a blocking gate for each skipped gate-kind overlay (fail-closed). - cwd-aware overlay config-key federation: config-loader _federatedConfigSchema(cwd) + config-schema isValidConfigKey(key, cwd) compose the overlay per loadConfig/ config-set call (never eager at module load, never wrong-cwd); first-party path unchanged with no cwd. - run-tests.cjs sandboxes GSD_HOME (idempotent — nested spawns reuse it) for test hermeticity; capability-loader.cjs git+eslint-ignored (tsc artifact); capability-validator.cjs stays linted (#551 migration coverage). Closes #1431 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(#1431): add changeset for runtime capability registry overlay Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(#1431): kill config-schema cwd-aware federation mutants (Stryker ≥52) The cwd-aware overlay config-key federation added to config-schema.cts (_capabilityConfigSchema(cwd) + isCapabilityConfigKey/isValidConfigKey cwd threading) introduced mutable surface uncovered by config-schema's mutation test set, dropping its score to 39.58% (below the 52 break threshold). Add a real-overlay-fixture describe block exercising every branch (cwd guard, overlay loadRegistry, found-branch, first-party fallback, cwd threading); local Stryker score 39.58% -> 77.08%. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -492,9 +492,11 @@ function cmdLoopRenderHooks(
|
||||
warnings?: string[];
|
||||
capabilities: Array<{ id: string; enabled?: boolean; active: boolean }>;
|
||||
};
|
||||
// Registry is the static generated module — same object capability-state uses internally.
|
||||
// Load overlay-aware registry (ADR-1244 D2 wiring) so installed third-party
|
||||
// capabilities are visible to loop rendering exactly like first-party ones.
|
||||
// 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 });
|
||||
const capabilityStatesById = new Map<string, { enabled?: boolean; active: boolean }>();
|
||||
for (const cap of state.capabilities || []) {
|
||||
capabilityStatesById.set(cap.id, cap);
|
||||
@@ -509,6 +511,27 @@ function cmdLoopRenderHooks(
|
||||
return;
|
||||
}
|
||||
|
||||
// ── ADR-1244 D2 fail-closed gate injection ────────────────────────────────────
|
||||
// For every skipped overlay capability that declared a gate at this point,
|
||||
// inject a synthetic BLOCKING gate into the resolved output so the loop HALTS
|
||||
// rather than silently proceeding as if the gate had passed. step/contribution
|
||||
// overlays that were skipped are left open (skip-open is correct for them).
|
||||
const overlayMeta = (registry as { _overlay?: { blockedGates?: Array<{ point: string; capId: string; reason: string }> } })['_overlay'];
|
||||
if (overlayMeta && Array.isArray(overlayMeta.blockedGates)) {
|
||||
for (const blocked of overlayMeta.blockedGates) {
|
||||
if (blocked.point === point) {
|
||||
const syntheticGate: ActiveHook = {
|
||||
capId: blocked.capId,
|
||||
kind: 'gate',
|
||||
blocking: true,
|
||||
onError: 'halt',
|
||||
check: `capability "${blocked.capId}" was skipped at load (${blocked.reason}); its gate at ${point} cannot be evaluated — failing closed`,
|
||||
};
|
||||
resolved.activeHooks.push(syntheticGate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --active-cap mode: print exactly 'true' or 'false' with no envelope
|
||||
if (activeCapId !== undefined) {
|
||||
const isActive = resolved.activeHooks.some((h) => h.capId === activeCapId);
|
||||
|
||||
Reference in New Issue
Block a user