* feat(#1077): phase 5f-2 — drive the hookEvents dialect (PostToolUse/AfterTool) from the descriptor postToolEvent (bin/install.js) and preToolEvent (applySettingsJsonHooks in runtime-hooks-surface.cts) now select the event-name dialect from registry.runtimes[id].runtime.hookEvents instead of the hardcoded (runtime === 'gemini' || runtime === 'antigravity') check: hookEvents === 'gemini' → AfterTool/BeforeTool; else → PostToolUse/PreToolUse. hookEvents threaded into the applySettingsJsonHooks opts bag. Equivalence-preserving (Codex-verified): hookEvents 'gemini' is exactly {gemini, antigravity}, 'claude' the rest; undefined → claude dialect (matches the old else). The per-event SET guards (isQwen||claude → SubagentStop/Stop/PreCompact; runtime==='claude' → FileChanged; isGemini → Gemini agent-events) stay HARDCODED — hookEvents (2-value) is too coarse to drive them (the event set differs within hookEvents='claude'); per-event-set drive tracked in #1076. Registry-parity test (enh-1077): asserts BOTH post-tool (AfterTool/PostToolUse) AND pre-tool (BeforeTool/PreToolUse) dialects are a pure function of hookEvents, for gemini/antigravity/claude/augment — non-vacuous (catches a broken hookEvents thread). Closes #1077 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(#1077): build hooks/dist in before() so dialect-drive test passes in scoped CI hooks/dist is gitignored and absent in scoped/windows CI jobs that do not pre-run build:hooks. Without it, install() finds no hook files and all AfterTool/BeforeTool/PostToolUse/PreToolUse event arrays come back empty, failing every hook-presence assertion. Added an idempotent ensureHooksDist() called in a top-level before() — mirrors the pattern from bug-376-claude-js-hook-gsd-rewriter.test.cjs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(#1055): add installSurface/writesSharedSettings/permissionWriter/extendedHookEvents to runtime descriptors Purely additive: four new fields on all 16 runtime capability.json descriptors, validator extended with three new closed-vocab sets, registry regenerated. Test fixtures (VALID_RUNTIME_CAP and makeRuntimeCap) updated to include the new required fields so all 255 capability-registry tests continue to pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(#1076): drive per-event hook guards from extendedHookEvents descriptor Replace hardcoded runtime-name checks (isQwen||runtime==='claude', runtime==='claude', isGemini) in applySettingsJsonHooks with a single descriptor-driven extendedEvents array derived from the new opts field. Remove isQwen and isGemini derivations (no remaining uses after the three guard blocks are migrated). Wire extendedHookEvents from the capability registry in bin/install.js call site. Add behavioral regression test (enh-1076-extended-hook-events-drive.test.cjs) confirming the drive is purely descriptor-based and runtime-name-agnostic. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(#1055): drive resolveRuntimeConfigIntent from the runtime descriptor; retire hand-kept REGISTRY - Rewrites src/runtime-config-adapter-registry.cts to require capability-registry.cjs and read installSurface / writesSharedSettings / permissionWriter from runtimes[id].runtime; deletes the hand-kept REGISTRY const (ADR-857 phase 5g drive 2). - ALLOWED_CONFIG_RUNTIMES is now derived from descriptor entries that have installSurface. - Fixes the configFormat parity gate in scripts/gen-capability-registry.cjs to read installSurface directly from capMap descriptor bodies, breaking the require cycle (adapter now requires the generated registry; gen-script must not require the adapter). - Adds golden-master test tests/enh-1055-config-intent-descriptor-drive.test.cjs (41 tests) pinning all 16 runtimes' return shapes and the TypeError-on-unknown contract. - Updates scripts/lint-test-file-count.allowlist.json (config module, +1 file). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(#1076): make hooksSurface descriptor load-bearing for the settings-json hook-skip - Adds hooksSurface?: string to ApplySettingsJsonHooksOpts and destructuring in applySettingsJsonHooks (src/runtime-hooks-surface.cts). - Replaces the hardcoded !isOpencode && !isKilo hook-skip guard with hooksSurface !== 'none'; removes the now-unused isOpencode/isKilo derivations (ADR-857 phase 5g drive 3). - Passes hooksSurface from the runtime descriptor at the applySettingsJsonHooks call site in bin/install.js using the established _capabilityRegistry?.runtimes?.[runtime]?.runtime?.hooksSurface idiom. - Extends tests/enh-1076-extended-hook-events-drive.test.cjs with two new suites proving: (a) hooksSurface:'none' writes no hooks regardless of runtime name; (b) hooksSurface:'settings-json' writes hooks even for 'opencode' (previously hardcoded to skip). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs: record installSurface/writesSharedSettings/permissionWriter/extendedHookEvents descriptor axes in ADR-1016 Add Decision 7a documenting the four axes added in the 5f-completion pass, update axis counts from "six" to "twelve", note 5f-completion drives as done in Decision 8's ladder, update Out of scope to reflect #1055/#1076 are done and 5g (InstallPlan capstone) remains the only open phase. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#1055): parity gate must fire on configFormat↔installSurface mismatch (read installSurface at the descriptor level) The test fixture makeRuntimeCapMap did not include installSurface in the runtime object, so the gate's typeof r.installSurface !== 'string' guard always skipped the entry and never threw. Added installSurface as an optional third parameter to makeRuntimeCapMap and passed the correct installSurface values ('settings-json' for claude, 'codex-toml' for codex) to the two THROWS tests. The gate implementation already reads r.installSurface correctly from the descriptor level. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(#1076): add installSurface↔hooksSurface + extendedHookEvents↔hookEvents consistency gates with rejection tests GATE A: INSTALL_SURFACE_TO_ALLOWED_HOOKS_SURFACES map in validateRuntimeBody enforces that a runtime's hooksSurface is valid for its installSurface (e.g. profile-marker-only only allows none, codex-toml only allows codex-hooks-json). Derived from the 16 real runtime descriptors. GATE B: validateRuntimeBody checks that if extendedHookEvents contains Gemini agent-events (BeforeAgent/AfterAgent/BeforeModel), hookEvents must be 'gemini'; if it contains Claude-family events (SubagentStop/Stop/PreCompact/FileChanged), hookEvents must be 'claude'. Added 10 rejection tests in suite 27 covering each gate + each new field validator. All 16 real runtimes satisfy both gates (verified before coding). Exports: INSTALL_SURFACE_TO_ALLOWED_HOOKS_SURFACES, VALID_INSTALL_SURFACES, VALID_EXTENDED_HOOK_EVENTS, VALID_PERMISSION_WRITERS, validateRuntimeBody. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(#1076): strengthen hooksSurface-drive assertions; defensive hooksSurface fallback; drop vacuous dup 1. bin/install.js: add explicit literal fallback for hooksSurface when the committed capability registry fails to load (opencode/kilo → 'none', all others → 'settings-json'). The descriptor is always the source of truth in normal operation. 2. enh-1076 Suite 7: change SessionStart assertion from key-presence (hasOwnProperty) to at least-one-command (hasHooksFor), so the test fails if hooks are initialized-but-empty. ensureHooksDist() in before() guarantees hook files exist. 3. enh-1055 Test 2: remove vacuous duplicate suite that re-asserted intent.runtime === row.runtime already fully covered by Test 1's deepStrictEqual over all four fields. 4. capability-registry.test.cjs: fix stale comments in the grok-skip test that claimed the parity gate uses the adapter registry; gate reads purely from the descriptor (installSurface absent → typeof r.installSurface !== 'string' → soft-skip). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(#1082): materialize the InstallPlan — collect install-level descriptor axes into resolveInstallPlan; route install()/finishInstall() through it (ADR-58/5g) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs: record 5g InstallPlan materialization (ADR-58 Accepted, ADR-1016 phase-5 complete) ADR-1016 Decision 8 step 7 updated to DONE: resolveInstallPlan(runtime) in runtime-config-adapter-registry collects install-level descriptor axes into the typed InstallPlan consumed by install()/finishInstall(). Out-of-scope section updated: 5g capstone is complete, phase 5 fully materialized. ADR-1016 line ~20 updated: InstallPlan IS now materialized (both halves). ADR-58 Implementation note added (2026-06-11): realized in runtime-config-adapter-registry (co-located with adapter-selection). CONTEXT.md Runtime Config Adapter Registry entry extended to document resolveInstallPlan and both-halves realization. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(#1082): update install drift guard to the resolveInstallPlan seam (5g) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
147 lines
5.9 KiB
TypeScript
147 lines
5.9 KiB
TypeScript
'use strict';
|
|
|
|
/**
|
|
* Runtime config adapter registry — dispatch table for install-phase config
|
|
* mutations (issue #60), replacing inline `runtime === '...'` branching in
|
|
* bin/install.js.
|
|
*
|
|
* ADR-857 phase 5g drive 2: The hand-kept REGISTRY const has been retired.
|
|
* Values are now read directly from the capability-registry.cjs descriptor
|
|
* (capabilities/<id>/capability.json runtime block) so a single source of
|
|
* truth drives all surfaces.
|
|
*
|
|
* Design notes:
|
|
* - `installSurface` selects which config handler install() runs:
|
|
* 'settings-json' → fall through to the shared settings.json accumulation.
|
|
* 'codex-toml' → early-return after writing codex.toml.
|
|
* 'copilot-instructions' → early-return after writing .github/copilot-instructions.md.
|
|
* 'cline-rules' → early-return after writing .clinerules.
|
|
* 'cursor-hooks-json' → early-return after writing .cursor/hooks.json (issue #777).
|
|
* 'profile-marker-only' → early-return after writing only the profile marker.
|
|
* - `writesSharedSettings` is the finishInstall writeSettings gate:
|
|
* false for codex / copilot / kilo / cursor / windsurf / trae / cline / kimi (legacy exclusion list).
|
|
* true for all other runtimes.
|
|
* - `finishPermissionWriter` names the finishInstall-phase dedicated config writer:
|
|
* 'opencode' → writes BOTH shared settings AND its own permissions file.
|
|
* 'kilo' → writes only its own permissions file.
|
|
* null → no dedicated permission writer.
|
|
*/
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
const { runtimes } = require('./capability-registry.cjs') as { runtimes: Record<string, { runtime: Record<string, unknown> | undefined }> };
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Types
|
|
// ---------------------------------------------------------------------------
|
|
|
|
type ConfigInstallSurface =
|
|
| 'settings-json'
|
|
| 'codex-toml'
|
|
| 'copilot-instructions'
|
|
| 'cline-rules'
|
|
| 'cursor-hooks-json'
|
|
| 'profile-marker-only';
|
|
|
|
type FinishPermissionWriter = 'opencode' | 'kilo' | null;
|
|
|
|
type HooksSurface =
|
|
| 'settings-json'
|
|
| 'codex-hooks-json'
|
|
| 'cursor-hooks-json'
|
|
| 'cline-rules'
|
|
| 'copilot-inline'
|
|
| 'none';
|
|
|
|
interface RuntimeConfigIntent {
|
|
runtime: string;
|
|
installSurface: ConfigInstallSurface;
|
|
writesSharedSettings: boolean;
|
|
finishPermissionWriter: FinishPermissionWriter;
|
|
}
|
|
|
|
/**
|
|
* The full install plan for a runtime: config-intent axes PLUS the three
|
|
* hook axes that install() reads from the capability descriptor.
|
|
* ADR-857 phase 5g capstone — single seam for all install-level descriptor reads.
|
|
*/
|
|
interface InstallPlan extends RuntimeConfigIntent {
|
|
/** Hook event dialect: 'claude' | 'gemini' | undefined */
|
|
hookEvents: string | undefined;
|
|
/** Extended hook event names registered beyond the core tool events (may be empty). */
|
|
extendedHookEvents: string[];
|
|
/** Which surface owns the hook registration for this runtime. */
|
|
hooksSurface: HooksSurface;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Exports
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/** The complete set of 16 supported runtimes for config-adapter dispatch. */
|
|
const ALLOWED_CONFIG_RUNTIMES: ReadonlySet<string> = new Set(
|
|
Object.entries(runtimes)
|
|
.filter(([, cap]) => cap && cap.runtime && typeof cap.runtime['installSurface'] === 'string')
|
|
.map(([id]) => id),
|
|
);
|
|
|
|
/** All valid installSurface values. */
|
|
const INSTALL_SURFACES: ReadonlyArray<ConfigInstallSurface> = Object.freeze([
|
|
'settings-json',
|
|
'codex-toml',
|
|
'copilot-instructions',
|
|
'cline-rules',
|
|
'cursor-hooks-json',
|
|
'profile-marker-only',
|
|
]);
|
|
|
|
/**
|
|
* Resolve the config adapter intent for a given runtime.
|
|
*
|
|
* Returns a fresh object each call so callers cannot poison the registry by
|
|
* mutating the returned value.
|
|
*
|
|
* @throws {TypeError} if runtime is not a known supported runtime.
|
|
*/
|
|
function resolveRuntimeConfigIntent(runtime: string): RuntimeConfigIntent {
|
|
const entry = runtimes[runtime]?.runtime;
|
|
if (!entry) throw new TypeError(`Unknown runtime for config adapter: ${runtime}`);
|
|
const permissionWriter = entry['permissionWriter'];
|
|
return {
|
|
runtime,
|
|
installSurface: entry['installSurface'] as ConfigInstallSurface,
|
|
writesSharedSettings: entry['writesSharedSettings'] as boolean,
|
|
finishPermissionWriter: permissionWriter == null ? null : permissionWriter as FinishPermissionWriter,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Resolve the complete install plan for a given runtime.
|
|
*
|
|
* Composes the config-intent axes from resolveRuntimeConfigIntent PLUS the
|
|
* three hook axes (hookEvents / extendedHookEvents / hooksSurface) that
|
|
* install() previously read scattered from the capability registry.
|
|
*
|
|
* ADR-857 phase 5g capstone — single typed seam for all install-level
|
|
* descriptor reads. Returns a fresh object each call.
|
|
*
|
|
* @throws {TypeError} if runtime is not a known supported runtime.
|
|
*/
|
|
function resolveInstallPlan(runtime: string): InstallPlan {
|
|
const desc = runtimes[runtime]?.runtime;
|
|
if (!desc) throw new TypeError(`Unknown runtime for install plan: ${runtime}`);
|
|
const configIntent = resolveRuntimeConfigIntent(runtime);
|
|
return {
|
|
runtime,
|
|
installSurface: configIntent.installSurface,
|
|
writesSharedSettings: configIntent.writesSharedSettings,
|
|
finishPermissionWriter: configIntent.finishPermissionWriter,
|
|
hookEvents: desc['hookEvents'] as string | undefined,
|
|
extendedHookEvents: Array.isArray(desc['extendedHookEvents']) ? desc['extendedHookEvents'] as string[] : [],
|
|
hooksSurface: desc['hooksSurface'] != null
|
|
? desc['hooksSurface'] as HooksSurface
|
|
: ((runtime === 'opencode' || runtime === 'kilo') ? 'none' : 'settings-json'),
|
|
};
|
|
}
|
|
|
|
export = { resolveRuntimeConfigIntent, resolveInstallPlan, ALLOWED_CONFIG_RUNTIMES, INSTALL_SURFACES };
|