* 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:
@@ -1038,3 +1038,81 @@ describe('Phase 4 regression: capabilityStatesById gates on active (not enabled)
|
||||
assert.strictEqual(result.activeHooks[0].capId, 'test-cap');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── ADR-1244 D2 fail-closed gate injection ────────────────────────────────────
|
||||
|
||||
describe('ADR-1244 D2: fail-closed gate injection for skipped overlay caps with gates', () => {
|
||||
// Verifies that cmdLoopRenderHooks injects a BLOCKING synthetic gate at the
|
||||
// declared point when an overlay capability that declares a gate is skipped at
|
||||
// load time due to an incompatible engines.gsd version constraint.
|
||||
//
|
||||
// Fixture: overlay cap declares a gate at execute:wave:post with engines.gsd: ">=99.0.0"
|
||||
// → loadRegistry skips it → records it in _overlay.blockedGates
|
||||
// → cmdLoopRenderHooks injects a blocking=true, onError=halt gate at execute:wave:post
|
||||
|
||||
test('skipped gate-kind overlay cap → BLOCKING synthetic gate at its declared point', (t) => {
|
||||
const overlayHome = fs.mkdtempSync(path.join(os.tmpdir(), 'loop-fail-closed-'));
|
||||
t.after(() => cleanup(overlayHome));
|
||||
|
||||
// Write an overlay capability that:
|
||||
// - declares a gate at execute:wave:post
|
||||
// - has engines.gsd: ">=99.0.0" (incompatible → will be skipped at load)
|
||||
const capId = 'fail-closed-gate-cap';
|
||||
const capDir = path.join(overlayHome, '.gsd', 'capabilities', capId);
|
||||
fs.mkdirSync(capDir, { recursive: true });
|
||||
const capManifest = {
|
||||
id: capId,
|
||||
role: 'feature',
|
||||
version: '1.0.0',
|
||||
title: 'Fail Closed Gate Cap',
|
||||
description: 'ADR-1244 D2 fail-closed wiring test',
|
||||
tier: 'standard',
|
||||
requires: [],
|
||||
engines: { gsd: '>=99.0.0' }, // intentionally incompatible → always skipped
|
||||
runtimeCompat: { supported: ['*'], unsupported: [] },
|
||||
skills: [], agents: [], hooks: [], config: {}, steps: [], contributions: [],
|
||||
gates: [{ point: 'execute:wave:post', check: 'always-pass', blocking: true, onError: 'halt' }],
|
||||
};
|
||||
fs.writeFileSync(path.join(capDir, 'capability.json'), JSON.stringify(capManifest), 'utf8');
|
||||
|
||||
// Invoke gsd-tools via subprocess so stdout is the real fd-1 (io.cjs writes via writeSync).
|
||||
// Set GSD_HOME to the overlay home so loadRegistry picks up the incompatible cap.
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
[GSD_TOOLS, 'loop', 'render-hooks', 'execute:wave:post', '--cwd', overlayHome],
|
||||
{
|
||||
cwd: ROOT,
|
||||
encoding: 'utf8',
|
||||
env: { ...process.env, GSD_HOME: overlayHome },
|
||||
},
|
||||
);
|
||||
|
||||
assert.strictEqual(result.status, 0, 'Expected exit 0. stderr: ' + (result.stderr || ''));
|
||||
|
||||
let envelope;
|
||||
try {
|
||||
envelope = JSON.parse(result.stdout.trim());
|
||||
} catch {
|
||||
assert.fail('loop render-hooks output must be valid JSON; got: ' + result.stdout.slice(0, 300));
|
||||
}
|
||||
|
||||
// The synthetic blocking gate must be present in activeHooks
|
||||
const syntheticGate = Array.isArray(envelope.activeHooks)
|
||||
? envelope.activeHooks.find((h) => h.capId === capId && h.kind === 'gate')
|
||||
: undefined;
|
||||
assert.ok(
|
||||
syntheticGate !== undefined,
|
||||
`activeHooks must contain a synthetic gate attributed to ${capId} (fail-closed injection). ` +
|
||||
'Got: ' + JSON.stringify(envelope.activeHooks),
|
||||
);
|
||||
assert.strictEqual(syntheticGate.blocking, true, 'synthetic gate must be blocking=true');
|
||||
assert.strictEqual(syntheticGate.onError, 'halt', 'synthetic gate must have onError=halt');
|
||||
|
||||
// The rendered markdown must also reference the gate cap
|
||||
assert.ok(
|
||||
typeof envelope.rendered === 'string' && envelope.rendered.includes(capId),
|
||||
'rendered output must reference the fail-closed gate cap. Got: ' + envelope.rendered,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user