feat(install): require an explicit hook decision at the apply layer

The guided installer asks how ECC hooks should run, but that consent
lived only in the wizard path. Running install-apply directly with a
profile that includes hooks-runtime still materialized the hook runtime
with no disclosure and no decision.

Gate the apply layer instead, so every entry point is covered:

- disclose the six hook capability groups when a plan would materialize
  the hook runtime, and refuse to apply until the caller decides
- --enable-hooks confirms the hook runtime; --no-hooks installs the rest
  of the selection without it and records the reduced module closure in
  install-state
- surface the pending decision as a dry-run warning
- show the same capability disclosure in the guided installer's plan
  preview, so the wizard's hook question states what it is asking about

Plans that never materialize hooks (Kimi, --profile minimal,
--without baseline:hooks) are unaffected and need no flag. Repair and
uninstall operate on already-recorded state and stay unchanged.

The capability taxonomy and the held-materialization behavior come from
Samarjeet Singh Tomar's PR #2634, reworked to fit the single-decision
consent model that shipped with the guided installer in #2649.

Co-Authored-By: Samarjeet Singh Tomar <samar_tomar@hotmail.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
haelyra
2026-08-30 15:09:34 -04:00
co-authored by Samarjeet Singh Tomar Claude Fable 5
parent a89cec9658
commit 6aaa41e028
26 changed files with 768 additions and 58 deletions
+37
View File
@@ -169,6 +169,7 @@ function runTests() {
excludeComponents: ['component:beta'],
legacyLanguages: [],
legacyMode: false,
hookConsent: 'declined',
},
},
};
@@ -179,6 +180,42 @@ function runTests() {
'--modules', 'platform-configs',
'--with', 'component:alpha',
'--without', 'component:beta',
'--no-hooks',
]);
})) passed += 1; else failed += 1;
if (test('buildInstallApplyArgs infers enabled hooks for older install-state records', () => {
const record = {
adapter: { target: 'cursor', kind: 'project' },
state: {
target: { target: 'cursor' },
request: {
profile: 'core',
modules: [],
includeComponents: [],
excludeComponents: [],
legacyLanguages: [],
legacyMode: false,
},
resolution: {
selectedModules: ['rules-core', 'hooks-runtime'],
skippedModules: [],
},
operations: [
{
kind: 'copy-file',
moduleId: 'hooks-runtime',
sourceRelativePath: '.cursor/hooks.json',
destinationPath: '/tmp/project/.cursor/hooks.json',
},
],
},
};
assert.deepStrictEqual(buildInstallApplyArgs(record), [
'--target', 'cursor',
'--profile', 'core',
'--enable-hooks',
]);
})) passed += 1; else failed += 1;