refactor(#1307): gate intel on isCapabilityActive + loop-resolver honors capability active (#1315)

* refactor(#1307): gate intel on isCapabilityActive + loop-resolver honors capability active

Part A: intel's command gate moves from config-only isIntelEnabled to the
shared isCapabilityActive('intel', cwd) — a consistency cutover (intel has
skills:[] so its tri-state collapses to the intel.enabled config leg; the gate
now flows through the resolver's precedence + runtime-aware resolution).
Part B: loop-resolver hook rendering now gates on capability state.active
(=== true, fail-closed) instead of state.enabled, so the capability config
gate is honored by the hook consumer, not just per-hook 'when'. active is now
required in the loop-resolver input types. Regression test proves a config-
disabled (active=false) capability's unconditional hook is not rendered.
Part of #1302.

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

* chore(#1307): add changeset for intel + loop-resolver active gate

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-06-16 00:12:54 -04:00
committed by GitHub
parent e50ead7ad2
commit 14e0709eed
10 changed files with 431 additions and 65 deletions

View File

@@ -237,7 +237,7 @@ describe('discuss:post — E2E empty envelope + synthetic resolver mechanics', (
assert.strictEqual(resolvedB.activeHooks.length, 0, 'explicit config=false must override schema default=true');
});
it('[bva] discuss:post with synthetic capability, capabilityStatesById enabled=false → hook absent even when config=true', () => {
it('[bva] discuss:post with synthetic capability, capabilityStatesById active=false → hook absent even when config=true', () => {
const reg = buildSyntheticRegistry({
targetPoint: 'discuss:post',
when: 'workflow.testcap_on',
@@ -247,9 +247,10 @@ describe('discuss:post — E2E empty envelope + synthetic resolver mechanics', (
point: 'discuss:post',
registry: reg,
config: { workflow: { testcap_on: true } },
capabilityStatesById: new Map([['future-cap', { enabled: false }]]),
// Phase 4: resolver gates on `active` (not `enabled`); pass active:false to suppress.
capabilityStatesById: new Map([['future-cap', { enabled: false, active: false }]]),
});
assert.strictEqual(resolved.activeHooks.length, 0, 'capabilityStatesById enabled=false must suppress hook even when config=true');
assert.strictEqual(resolved.activeHooks.length, 0, 'capabilityStatesById active=false must suppress hook even when config=true');
});
it('[negative] discuss:post with invalid point name "discuss:past" exits non-zero and lists valid points', () => {
@@ -372,7 +373,8 @@ describe('execute:pre — real registry empty-resolution + synthetic resolver me
point: 'execute:pre',
registry: syntheticReg,
config: {},
capabilityStatesById: new Map([['future-cap', { enabled: false }]]),
// Phase 4: resolver gates on `active` (not `enabled`); pass active:false to suppress.
capabilityStatesById: new Map([['future-cap', { enabled: false, active: false }]]),
});
assert.strictEqual(resolved.activeHooks.length, 0, 'capabilityStatesById disabled must filter unconditional hook');
});