@nerviq/cli 1.29.0 → 1.29.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1527 -1493
- package/README.md +550 -538
- package/SECURITY.md +82 -82
- package/bin/cli.js +2562 -2558
- package/docs/api-reference.md +356 -356
- package/docs/audit-fix.md +109 -0
- package/docs/autofix.md +3 -62
- package/docs/getting-started.md +1 -1
- package/docs/index.html +592 -592
- package/docs/integration-contracts.md +287 -287
- package/docs/maintenance.md +128 -128
- package/docs/new-platform-guide.md +202 -202
- package/docs/release-process.md +63 -0
- package/docs/shallow-risk.md +244 -244
- package/docs/why-nerviq.md +82 -82
- package/package.json +67 -67
- package/src/aider/activity.js +226 -226
- package/src/aider/context.js +162 -162
- package/src/aider/freshness.js +123 -123
- package/src/aider/techniques.js +3465 -3465
- package/src/audit/layers.js +180 -180
- package/src/audit.js +1032 -1032
- package/src/benchmark.js +299 -299
- package/src/codex/activity.js +324 -324
- package/src/codex/freshness.js +142 -142
- package/src/codex/techniques.js +4895 -4895
- package/src/context.js +326 -326
- package/src/continuous-ops.js +11 -1
- package/src/convert.js +340 -340
- package/src/copilot/config-parser.js +280 -280
- package/src/copilot/context.js +218 -218
- package/src/copilot/freshness.js +177 -177
- package/src/copilot/patch.js +238 -238
- package/src/copilot/techniques.js +3578 -3578
- package/src/cursor/freshness.js +194 -194
- package/src/cursor/patch.js +243 -243
- package/src/cursor/techniques.js +3735 -3735
- package/src/doctor.js +201 -201
- package/src/fix-engine.js +511 -8
- package/src/formatters/csv.js +86 -86
- package/src/formatters/junit.js +123 -123
- package/src/formatters/markdown.js +164 -164
- package/src/formatters/otel.js +151 -151
- package/src/freshness.js +156 -156
- package/src/gemini/activity.js +402 -402
- package/src/gemini/context.js +290 -290
- package/src/gemini/freshness.js +183 -183
- package/src/gemini/patch.js +229 -229
- package/src/gemini/techniques.js +3811 -3811
- package/src/governance.js +533 -533
- package/src/harmony/audit.js +306 -306
- package/src/i18n.js +63 -63
- package/src/insights.js +119 -119
- package/src/integrations.js +134 -134
- package/src/locales/en.json +33 -33
- package/src/locales/es.json +33 -33
- package/src/migrate.js +354 -354
- package/src/opencode/activity.js +286 -286
- package/src/opencode/freshness.js +137 -137
- package/src/opencode/techniques.js +3450 -3450
- package/src/setup/analysis.js +12 -12
- package/src/setup.js +7 -6
- package/src/shallow-risk/index.js +56 -56
- package/src/shallow-risk/patterns/agent-config-cross-platform-drift.js +50 -50
- package/src/shallow-risk/patterns/agent-config-dangerous-autoapprove.js +46 -46
- package/src/shallow-risk/patterns/agent-config-deprecated-keys.js +46 -46
- package/src/shallow-risk/patterns/agent-config-missing-file.js +317 -317
- package/src/shallow-risk/patterns/agent-config-secret-literal.js +49 -49
- package/src/shallow-risk/patterns/agent-config-stack-contradiction.js +34 -34
- package/src/shallow-risk/patterns/hook-script-missing.js +70 -70
- package/src/shallow-risk/patterns/mcp-server-no-allowlist.js +52 -52
- package/src/shallow-risk/shared.js +648 -648
- package/src/source-urls.js +295 -295
- package/src/state-paths.js +85 -85
- package/src/supplemental-checks.js +805 -805
- package/src/telemetry.js +160 -160
- package/src/windsurf/context.js +359 -359
- package/src/windsurf/freshness.js +194 -194
- package/src/windsurf/patch.js +231 -231
- package/src/windsurf/techniques.js +3779 -3779
package/src/governance.js
CHANGED
|
@@ -2,108 +2,108 @@ const { DOMAIN_PACKS } = require('./domain-packs');
|
|
|
2
2
|
const { MCP_PACKS, mergeMcpServers, normalizeMcpPackKeys } = require('./mcp-packs');
|
|
3
3
|
const { getCodexGovernanceSummary } = require('./codex/governance');
|
|
4
4
|
const { formatTerminologyLines } = require('./terminology');
|
|
5
|
-
|
|
6
|
-
const PERMISSION_PROFILES = [
|
|
7
|
-
{
|
|
8
|
-
key: 'read-only',
|
|
9
|
-
label: 'Read-Only',
|
|
10
|
-
risk: 'low',
|
|
11
|
-
defaultMode: 'plan',
|
|
12
|
-
useWhen: 'Security review, discovery, and first contact with a mature repo.',
|
|
13
|
-
behavior: 'No file writes. Safe for audits, workshops, and approval flows.',
|
|
14
|
-
deny: ['Write(**)', 'Edit(**)', 'MultiEdit(**)', 'Bash(rm -rf *)', 'Bash(git reset --hard *)'],
|
|
15
|
-
},
|
|
16
|
-
{
|
|
17
|
-
key: 'suggest-only',
|
|
18
|
-
label: 'Suggest-Only',
|
|
19
|
-
risk: 'low',
|
|
20
|
-
defaultMode: 'acceptEdits',
|
|
21
|
-
useWhen: 'Teams want structured proposals and exported plans without automatic apply.',
|
|
22
|
-
behavior: 'Generates plans and proposal bundles, but no source changes are applied.',
|
|
23
|
-
deny: ['Bash(rm -rf *)', 'Bash(git reset --hard *)', 'Bash(git clean *)', 'Read(./.env*)'],
|
|
24
|
-
},
|
|
25
|
-
{
|
|
26
|
-
key: 'safe-write',
|
|
27
|
-
label: 'Safe-Write',
|
|
28
|
-
risk: 'medium',
|
|
29
|
-
defaultMode: 'acceptEdits',
|
|
30
|
-
useWhen: 'Starter repos or tightly scoped apply flows with visible rollback.',
|
|
31
|
-
behavior: 'Allows creation of missing Claude artifacts while preserving existing files.',
|
|
32
|
-
deny: ['Read(./.env*)', 'Read(./secrets/**)', 'Bash(rm -rf *)', 'Bash(git push --force *)'],
|
|
33
|
-
},
|
|
34
|
-
{
|
|
35
|
-
key: 'power-user',
|
|
36
|
-
label: 'Power-User',
|
|
37
|
-
risk: 'medium',
|
|
38
|
-
defaultMode: 'acceptEdits',
|
|
39
|
-
useWhen: 'Experienced maintainers who understand the repo and want faster iteration.',
|
|
40
|
-
behavior: 'Broader local automation with fewer prompts, still without bypass defaults.',
|
|
41
|
-
deny: ['Read(./.env*)', 'Bash(rm -rf *)'],
|
|
42
|
-
},
|
|
43
|
-
{
|
|
44
|
-
key: 'internal-research',
|
|
45
|
-
label: 'Internal-Research',
|
|
46
|
-
risk: 'high',
|
|
47
|
-
defaultMode: 'bypassPermissions',
|
|
48
|
-
useWhen: 'Internal experiments only, never as a product-facing default.',
|
|
49
|
-
behavior: 'Maximum autonomy for research workflows, suitable only with explicit human oversight.',
|
|
50
|
-
deny: [],
|
|
51
|
-
},
|
|
52
|
-
];
|
|
53
|
-
|
|
54
|
-
const HOOK_REGISTRY = [
|
|
55
|
-
{
|
|
56
|
-
key: 'protect-secrets',
|
|
57
|
-
file: '.claude/hooks/protect-secrets.sh',
|
|
58
|
-
triggerPoint: 'PreToolUse',
|
|
59
|
-
matcher: 'Read|Write|Edit|Bash',
|
|
60
|
-
purpose: 'Blocks direct access to secret or credential files before a tool runs.',
|
|
61
|
-
filesTouched: [],
|
|
62
|
-
sideEffects: ['Stops the action and returns a block decision when a secret path is targeted.'],
|
|
63
|
-
risk: 'low',
|
|
64
|
-
riskLevel: 'high',
|
|
65
|
-
dryRunExample: 'Attempt to read `.env` and confirm the hook blocks the request.',
|
|
66
|
-
rollbackPath: 'Remove the PreToolUse registration from settings.json.',
|
|
67
|
-
},
|
|
68
|
-
{
|
|
69
|
-
key: 'on-edit-lint',
|
|
70
|
-
file: '.claude/hooks/on-edit-lint.sh',
|
|
71
|
-
triggerPoint: 'PostToolUse',
|
|
72
|
-
matcher: 'Write|Edit',
|
|
73
|
-
purpose: 'Runs the repo linter or formatter after file edits when tooling is available.',
|
|
74
|
-
filesTouched: ['Working tree files targeted by eslint/ruff fixes'],
|
|
75
|
-
sideEffects: ['May auto-fix formatting or lint issues.', 'Can modify the same files that were just edited.'],
|
|
76
|
-
risk: 'medium',
|
|
77
|
-
riskLevel: 'medium',
|
|
78
|
-
dryRunExample: 'Edit a JS or Python file and inspect whether eslint or ruff would run.',
|
|
79
|
-
rollbackPath: 'Remove the PostToolUse hook entry or delete the script.',
|
|
80
|
-
},
|
|
81
|
-
{
|
|
82
|
-
key: 'log-changes',
|
|
83
|
-
file: '.claude/hooks/log-changes.sh',
|
|
84
|
-
triggerPoint: 'PostToolUse',
|
|
85
|
-
matcher: 'Write|Edit',
|
|
86
|
-
purpose: 'Appends a durable file-change log under `.claude/logs/` for later review.',
|
|
87
|
-
filesTouched: ['.claude/logs/file-changes.log'],
|
|
88
|
-
sideEffects: ['Creates the logs directory on first use.', 'Adds a timestamped audit line per file change.'],
|
|
89
|
-
risk: 'low',
|
|
90
|
-
riskLevel: 'low',
|
|
91
|
-
dryRunExample: 'Edit one file and verify the log entry is appended.',
|
|
92
|
-
rollbackPath: 'Remove the PostToolUse hook entry and delete the log file if desired.',
|
|
93
|
-
},
|
|
94
|
-
{
|
|
95
|
-
key: 'duplicate-id-check',
|
|
96
|
-
file: '.claude/hooks/check-duplicate-ids.sh',
|
|
97
|
-
triggerPoint: 'PostToolUse',
|
|
98
|
-
matcher: 'Write|Edit',
|
|
99
|
-
purpose: 'Detects duplicate IDs in catalog or structured data files after edits.',
|
|
100
|
-
filesTouched: [],
|
|
101
|
-
sideEffects: ['Returns a systemMessage warning if duplicates are found.'],
|
|
102
|
-
risk: 'low',
|
|
103
|
-
riskLevel: 'low',
|
|
104
|
-
dryRunExample: 'Edit a catalog file and verify duplicate check runs without blocking.',
|
|
105
|
-
rollbackPath: 'Remove the PostToolUse hook entry from settings.',
|
|
106
|
-
},
|
|
5
|
+
|
|
6
|
+
const PERMISSION_PROFILES = [
|
|
7
|
+
{
|
|
8
|
+
key: 'read-only',
|
|
9
|
+
label: 'Read-Only',
|
|
10
|
+
risk: 'low',
|
|
11
|
+
defaultMode: 'plan',
|
|
12
|
+
useWhen: 'Security review, discovery, and first contact with a mature repo.',
|
|
13
|
+
behavior: 'No file writes. Safe for audits, workshops, and approval flows.',
|
|
14
|
+
deny: ['Write(**)', 'Edit(**)', 'MultiEdit(**)', 'Bash(rm -rf *)', 'Bash(git reset --hard *)'],
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
key: 'suggest-only',
|
|
18
|
+
label: 'Suggest-Only',
|
|
19
|
+
risk: 'low',
|
|
20
|
+
defaultMode: 'acceptEdits',
|
|
21
|
+
useWhen: 'Teams want structured proposals and exported plans without automatic apply.',
|
|
22
|
+
behavior: 'Generates plans and proposal bundles, but no source changes are applied.',
|
|
23
|
+
deny: ['Bash(rm -rf *)', 'Bash(git reset --hard *)', 'Bash(git clean *)', 'Read(./.env*)'],
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
key: 'safe-write',
|
|
27
|
+
label: 'Safe-Write',
|
|
28
|
+
risk: 'medium',
|
|
29
|
+
defaultMode: 'acceptEdits',
|
|
30
|
+
useWhen: 'Starter repos or tightly scoped apply flows with visible rollback.',
|
|
31
|
+
behavior: 'Allows creation of missing Claude artifacts while preserving existing files.',
|
|
32
|
+
deny: ['Read(./.env*)', 'Read(./secrets/**)', 'Bash(rm -rf *)', 'Bash(git push --force *)'],
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
key: 'power-user',
|
|
36
|
+
label: 'Power-User',
|
|
37
|
+
risk: 'medium',
|
|
38
|
+
defaultMode: 'acceptEdits',
|
|
39
|
+
useWhen: 'Experienced maintainers who understand the repo and want faster iteration.',
|
|
40
|
+
behavior: 'Broader local automation with fewer prompts, still without bypass defaults.',
|
|
41
|
+
deny: ['Read(./.env*)', 'Bash(rm -rf *)'],
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
key: 'internal-research',
|
|
45
|
+
label: 'Internal-Research',
|
|
46
|
+
risk: 'high',
|
|
47
|
+
defaultMode: 'bypassPermissions',
|
|
48
|
+
useWhen: 'Internal experiments only, never as a product-facing default.',
|
|
49
|
+
behavior: 'Maximum autonomy for research workflows, suitable only with explicit human oversight.',
|
|
50
|
+
deny: [],
|
|
51
|
+
},
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
const HOOK_REGISTRY = [
|
|
55
|
+
{
|
|
56
|
+
key: 'protect-secrets',
|
|
57
|
+
file: '.claude/hooks/protect-secrets.sh',
|
|
58
|
+
triggerPoint: 'PreToolUse',
|
|
59
|
+
matcher: 'Read|Write|Edit|Bash',
|
|
60
|
+
purpose: 'Blocks direct access to secret or credential files before a tool runs.',
|
|
61
|
+
filesTouched: [],
|
|
62
|
+
sideEffects: ['Stops the action and returns a block decision when a secret path is targeted.'],
|
|
63
|
+
risk: 'low',
|
|
64
|
+
riskLevel: 'high',
|
|
65
|
+
dryRunExample: 'Attempt to read `.env` and confirm the hook blocks the request.',
|
|
66
|
+
rollbackPath: 'Remove the PreToolUse registration from settings.json.',
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
key: 'on-edit-lint',
|
|
70
|
+
file: '.claude/hooks/on-edit-lint.sh',
|
|
71
|
+
triggerPoint: 'PostToolUse',
|
|
72
|
+
matcher: 'Write|Edit',
|
|
73
|
+
purpose: 'Runs the repo linter or formatter after file edits when tooling is available.',
|
|
74
|
+
filesTouched: ['Working tree files targeted by eslint/ruff fixes'],
|
|
75
|
+
sideEffects: ['May auto-fix formatting or lint issues.', 'Can modify the same files that were just edited.'],
|
|
76
|
+
risk: 'medium',
|
|
77
|
+
riskLevel: 'medium',
|
|
78
|
+
dryRunExample: 'Edit a JS or Python file and inspect whether eslint or ruff would run.',
|
|
79
|
+
rollbackPath: 'Remove the PostToolUse hook entry or delete the script.',
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
key: 'log-changes',
|
|
83
|
+
file: '.claude/hooks/log-changes.sh',
|
|
84
|
+
triggerPoint: 'PostToolUse',
|
|
85
|
+
matcher: 'Write|Edit',
|
|
86
|
+
purpose: 'Appends a durable file-change log under `.claude/logs/` for later review.',
|
|
87
|
+
filesTouched: ['.claude/logs/file-changes.log'],
|
|
88
|
+
sideEffects: ['Creates the logs directory on first use.', 'Adds a timestamped audit line per file change.'],
|
|
89
|
+
risk: 'low',
|
|
90
|
+
riskLevel: 'low',
|
|
91
|
+
dryRunExample: 'Edit one file and verify the log entry is appended.',
|
|
92
|
+
rollbackPath: 'Remove the PostToolUse hook entry and delete the log file if desired.',
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
key: 'duplicate-id-check',
|
|
96
|
+
file: '.claude/hooks/check-duplicate-ids.sh',
|
|
97
|
+
triggerPoint: 'PostToolUse',
|
|
98
|
+
matcher: 'Write|Edit',
|
|
99
|
+
purpose: 'Detects duplicate IDs in catalog or structured data files after edits.',
|
|
100
|
+
filesTouched: [],
|
|
101
|
+
sideEffects: ['Returns a systemMessage warning if duplicates are found.'],
|
|
102
|
+
risk: 'low',
|
|
103
|
+
riskLevel: 'low',
|
|
104
|
+
dryRunExample: 'Edit a catalog file and verify duplicate check runs without blocking.',
|
|
105
|
+
rollbackPath: 'Remove the PostToolUse hook entry from settings.',
|
|
106
|
+
},
|
|
107
107
|
{
|
|
108
108
|
key: 'injection-defense',
|
|
109
109
|
file: '.claude/hooks/injection-defense.js',
|
|
@@ -117,188 +117,188 @@ const HOOK_REGISTRY = [
|
|
|
117
117
|
dryRunExample: 'Run a WebFetch or MCP-backed tool call and verify suspicious content is logged for review.',
|
|
118
118
|
rollbackPath: 'Remove the PostToolUse hook entry from settings.',
|
|
119
119
|
},
|
|
120
|
-
{
|
|
121
|
-
key: 'trust-drift-check',
|
|
122
|
-
file: '.claude/hooks/trust-drift-check.sh',
|
|
123
|
-
triggerPoint: 'PostToolUse',
|
|
124
|
-
matcher: 'Write|Edit',
|
|
125
|
-
purpose: 'Runs trust drift validation after file changes to catch metric/docs inconsistencies.',
|
|
126
|
-
filesTouched: [],
|
|
127
|
-
sideEffects: ['Returns a systemMessage warning if drift is detected.'],
|
|
128
|
-
risk: 'low',
|
|
129
|
-
riskLevel: 'low',
|
|
130
|
-
dryRunExample: 'Edit a product-facing file and verify drift check runs.',
|
|
131
|
-
rollbackPath: 'Remove the PostToolUse hook entry from settings.',
|
|
132
|
-
},
|
|
133
|
-
{
|
|
134
|
-
key: 'session-init',
|
|
135
|
-
file: '.claude/hooks/session-start.sh',
|
|
136
|
-
triggerPoint: 'SessionStart',
|
|
137
|
-
matcher: null,
|
|
138
|
-
purpose: 'Rotates large log files and loads workspace context at session start.',
|
|
139
|
-
filesTouched: ['tools/change-log.txt', 'tools/failure-log.txt'],
|
|
140
|
-
sideEffects: ['Archives logs over 500KB.', 'Returns a systemMessage with workspace info.'],
|
|
141
|
-
risk: 'low',
|
|
142
|
-
riskLevel: 'low',
|
|
143
|
-
dryRunExample: 'Start a new session and verify log rotation runs.',
|
|
144
|
-
rollbackPath: 'Remove the SessionStart hook entry from settings.',
|
|
145
|
-
},
|
|
146
|
-
];
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
* Classify the risk level of a hook based on its event type and characteristics.
|
|
150
|
-
* - high: PreToolUse hooks that can block operations (exit 2)
|
|
151
|
-
* - medium: PostToolUse hooks that modify files or warn (exit 1 or write-only)
|
|
152
|
-
* - low: Informational hooks (PostToolUse notification/logging, SessionStart)
|
|
153
|
-
* @param {Object} hook - A hook entry from HOOK_REGISTRY or a custom hook.
|
|
154
|
-
* @returns {string} Risk level: 'high', 'medium', or 'low'.
|
|
155
|
-
*/
|
|
156
|
-
function classifyHookRiskLevel(hook) {
|
|
157
|
-
// PreToolUse hooks can block operations (exit code 2 blocks the tool call)
|
|
158
|
-
if (hook.triggerPoint === 'PreToolUse') {
|
|
159
|
-
return 'high';
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// SessionStart hooks are informational — they run once at session init
|
|
163
|
-
if (hook.triggerPoint === 'SessionStart') {
|
|
164
|
-
return 'low';
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// PostToolUse hooks that touch files are medium risk (they can modify working tree)
|
|
168
|
-
if (hook.triggerPoint === 'PostToolUse') {
|
|
169
|
-
if (Array.isArray(hook.filesTouched) && hook.filesTouched.length > 0) {
|
|
170
|
-
// Hooks that only write to log files are low risk
|
|
171
|
-
const onlyLogs = hook.filesTouched.every(f =>
|
|
172
|
-
/\blog|\.log\b/i.test(f) || f.includes('logs/')
|
|
173
|
-
);
|
|
174
|
-
if (onlyLogs) return 'low';
|
|
175
|
-
return 'medium';
|
|
176
|
-
}
|
|
177
|
-
return 'low';
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// Default: unknown trigger points get medium risk
|
|
181
|
-
return 'medium';
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
const POLICY_PACKS = [
|
|
185
|
-
{
|
|
186
|
-
key: 'baseline-engineering',
|
|
187
|
-
label: 'Baseline Engineering',
|
|
188
|
-
modules: ['CLAUDE.md baseline', 'commands', 'rules', 'safe-write profile'],
|
|
189
|
-
useWhen: 'General product teams that want a pragmatic default.',
|
|
190
|
-
},
|
|
191
|
-
{
|
|
192
|
-
key: 'security-sensitive',
|
|
193
|
-
label: 'Security-Sensitive',
|
|
194
|
-
modules: ['read-only profile', 'suggest-only mode', 'protect-secrets hook', 'approval checklist'],
|
|
195
|
-
useWhen: 'Auth, payments, customer data, or regulated surfaces.',
|
|
196
|
-
},
|
|
197
|
-
{
|
|
198
|
-
key: 'oss-friendly',
|
|
199
|
-
label: 'OSS-Friendly',
|
|
200
|
-
modules: ['suggest-only profile', 'minimal commands', 'light rules', 'manual merge expectations'],
|
|
201
|
-
useWhen: 'Open-source repos with many external contributors.',
|
|
202
|
-
},
|
|
203
|
-
{
|
|
204
|
-
key: 'regulated-lite',
|
|
205
|
-
label: 'Regulated-Lite',
|
|
206
|
-
modules: ['suggest-only or safe-write profile', 'activity artifacts', 'rollback manifests', 'benchmark evidence'],
|
|
207
|
-
useWhen: 'Teams that need auditable change paths before broader adoption.',
|
|
208
|
-
},
|
|
209
|
-
];
|
|
210
|
-
|
|
211
|
-
const PILOT_ROLLOUT_KIT = {
|
|
212
|
-
recommendedScope: [
|
|
213
|
-
'Pick 1-2 repos with active maintainers and low blast radius.',
|
|
214
|
-
'Run discover and suggest-only first; avoid direct writes on mature repos.',
|
|
215
|
-
'Choose one permission profile before any pilot starts.',
|
|
216
|
-
'Define success metrics before the first benchmark run.',
|
|
217
|
-
],
|
|
218
|
-
approvals: [
|
|
219
|
-
'Engineering owner approves scope and rollback expectations.',
|
|
220
|
-
'Security owner approves the selected permission profile and hooks.',
|
|
221
|
-
'Pilot owner records the benchmark baseline and acceptance criteria.',
|
|
222
|
-
],
|
|
223
|
-
successMetrics: [
|
|
224
|
-
'Score delta and organic score delta',
|
|
225
|
-
'Number of recommendations accepted',
|
|
226
|
-
'Time to first useful Claude workflow',
|
|
227
|
-
'Rollback-free apply rate',
|
|
228
|
-
],
|
|
229
|
-
rollbackExpectations: [
|
|
230
|
-
'Every apply batch must emit a rollback artifact.',
|
|
231
|
-
'If a created artifact is rejected, delete the files listed in the rollback manifest.',
|
|
232
|
-
'Record the rollback event in the activity log for auditability.',
|
|
233
|
-
],
|
|
234
|
-
};
|
|
235
|
-
|
|
236
|
-
function clone(value) {
|
|
237
|
-
return JSON.parse(JSON.stringify(value));
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
function mergeUnique(existing = [], additions = []) {
|
|
241
|
-
return [...new Set([...(Array.isArray(existing) ? existing : []), ...additions])];
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
function mergeHooks(existingHooks = {}, nextHooks = {}) {
|
|
245
|
-
const merged = clone(existingHooks || {});
|
|
246
|
-
|
|
247
|
-
for (const [stage, blocks] of Object.entries(nextHooks)) {
|
|
248
|
-
const targetBlocks = Array.isArray(merged[stage]) ? clone(merged[stage]) : [];
|
|
249
|
-
for (const incoming of blocks) {
|
|
250
|
-
const index = targetBlocks.findIndex(block => block.matcher === incoming.matcher);
|
|
251
|
-
if (index === -1) {
|
|
252
|
-
targetBlocks.push(clone(incoming));
|
|
253
|
-
continue;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
const current = targetBlocks[index];
|
|
257
|
-
const existingCommands = new Set((current.hooks || []).map(hook => `${hook.type}:${hook.command}:${hook.timeout || ''}`));
|
|
258
|
-
const mergedHooks = [...(current.hooks || [])];
|
|
259
|
-
for (const hook of incoming.hooks || []) {
|
|
260
|
-
const signature = `${hook.type}:${hook.command}:${hook.timeout || ''}`;
|
|
261
|
-
if (!existingCommands.has(signature)) {
|
|
262
|
-
mergedHooks.push(clone(hook));
|
|
263
|
-
existingCommands.add(signature);
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
targetBlocks[index] = { ...current, hooks: mergedHooks };
|
|
267
|
-
}
|
|
268
|
-
merged[stage] = targetBlocks;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
return merged;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
function getPermissionProfile(key = 'safe-write') {
|
|
275
|
-
return PERMISSION_PROFILES.find(profile => profile.key === key) ||
|
|
276
|
-
PERMISSION_PROFILES.find(profile => profile.key === 'safe-write');
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
function isWritableProfile(key = 'safe-write') {
|
|
280
|
-
return ['safe-write', 'power-user', 'internal-research'].includes(getPermissionProfile(key).key);
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
function ensureWritableProfile(key = 'safe-write', commandName = 'apply', dryRun = false) {
|
|
284
|
-
const profile = getPermissionProfile(key);
|
|
285
|
-
if (!dryRun && !isWritableProfile(profile.key)) {
|
|
286
|
-
throw new Error(`${commandName} requires a writable profile. Use --profile safe-write or --dry-run.`);
|
|
287
|
-
}
|
|
288
|
-
return profile;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
function buildHookConfig(hookFiles, profileKey) {
|
|
292
|
-
const profile = getPermissionProfile(profileKey);
|
|
293
|
-
if (!isWritableProfile(profile.key)) {
|
|
294
|
-
return {};
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
const uniqueFiles = [...new Set(hookFiles)].sort();
|
|
298
|
-
if (uniqueFiles.length === 0) {
|
|
299
|
-
return {};
|
|
300
|
-
}
|
|
301
|
-
|
|
120
|
+
{
|
|
121
|
+
key: 'trust-drift-check',
|
|
122
|
+
file: '.claude/hooks/trust-drift-check.sh',
|
|
123
|
+
triggerPoint: 'PostToolUse',
|
|
124
|
+
matcher: 'Write|Edit',
|
|
125
|
+
purpose: 'Runs trust drift validation after file changes to catch metric/docs inconsistencies.',
|
|
126
|
+
filesTouched: [],
|
|
127
|
+
sideEffects: ['Returns a systemMessage warning if drift is detected.'],
|
|
128
|
+
risk: 'low',
|
|
129
|
+
riskLevel: 'low',
|
|
130
|
+
dryRunExample: 'Edit a product-facing file and verify drift check runs.',
|
|
131
|
+
rollbackPath: 'Remove the PostToolUse hook entry from settings.',
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
key: 'session-init',
|
|
135
|
+
file: '.claude/hooks/session-start.sh',
|
|
136
|
+
triggerPoint: 'SessionStart',
|
|
137
|
+
matcher: null,
|
|
138
|
+
purpose: 'Rotates large log files and loads workspace context at session start.',
|
|
139
|
+
filesTouched: ['tools/change-log.txt', 'tools/failure-log.txt'],
|
|
140
|
+
sideEffects: ['Archives logs over 500KB.', 'Returns a systemMessage with workspace info.'],
|
|
141
|
+
risk: 'low',
|
|
142
|
+
riskLevel: 'low',
|
|
143
|
+
dryRunExample: 'Start a new session and verify log rotation runs.',
|
|
144
|
+
rollbackPath: 'Remove the SessionStart hook entry from settings.',
|
|
145
|
+
},
|
|
146
|
+
];
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Classify the risk level of a hook based on its event type and characteristics.
|
|
150
|
+
* - high: PreToolUse hooks that can block operations (exit 2)
|
|
151
|
+
* - medium: PostToolUse hooks that modify files or warn (exit 1 or write-only)
|
|
152
|
+
* - low: Informational hooks (PostToolUse notification/logging, SessionStart)
|
|
153
|
+
* @param {Object} hook - A hook entry from HOOK_REGISTRY or a custom hook.
|
|
154
|
+
* @returns {string} Risk level: 'high', 'medium', or 'low'.
|
|
155
|
+
*/
|
|
156
|
+
function classifyHookRiskLevel(hook) {
|
|
157
|
+
// PreToolUse hooks can block operations (exit code 2 blocks the tool call)
|
|
158
|
+
if (hook.triggerPoint === 'PreToolUse') {
|
|
159
|
+
return 'high';
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// SessionStart hooks are informational — they run once at session init
|
|
163
|
+
if (hook.triggerPoint === 'SessionStart') {
|
|
164
|
+
return 'low';
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// PostToolUse hooks that touch files are medium risk (they can modify working tree)
|
|
168
|
+
if (hook.triggerPoint === 'PostToolUse') {
|
|
169
|
+
if (Array.isArray(hook.filesTouched) && hook.filesTouched.length > 0) {
|
|
170
|
+
// Hooks that only write to log files are low risk
|
|
171
|
+
const onlyLogs = hook.filesTouched.every(f =>
|
|
172
|
+
/\blog|\.log\b/i.test(f) || f.includes('logs/')
|
|
173
|
+
);
|
|
174
|
+
if (onlyLogs) return 'low';
|
|
175
|
+
return 'medium';
|
|
176
|
+
}
|
|
177
|
+
return 'low';
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Default: unknown trigger points get medium risk
|
|
181
|
+
return 'medium';
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const POLICY_PACKS = [
|
|
185
|
+
{
|
|
186
|
+
key: 'baseline-engineering',
|
|
187
|
+
label: 'Baseline Engineering',
|
|
188
|
+
modules: ['CLAUDE.md baseline', 'commands', 'rules', 'safe-write profile'],
|
|
189
|
+
useWhen: 'General product teams that want a pragmatic default.',
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
key: 'security-sensitive',
|
|
193
|
+
label: 'Security-Sensitive',
|
|
194
|
+
modules: ['read-only profile', 'suggest-only mode', 'protect-secrets hook', 'approval checklist'],
|
|
195
|
+
useWhen: 'Auth, payments, customer data, or regulated surfaces.',
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
key: 'oss-friendly',
|
|
199
|
+
label: 'OSS-Friendly',
|
|
200
|
+
modules: ['suggest-only profile', 'minimal commands', 'light rules', 'manual merge expectations'],
|
|
201
|
+
useWhen: 'Open-source repos with many external contributors.',
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
key: 'regulated-lite',
|
|
205
|
+
label: 'Regulated-Lite',
|
|
206
|
+
modules: ['suggest-only or safe-write profile', 'activity artifacts', 'rollback manifests', 'benchmark evidence'],
|
|
207
|
+
useWhen: 'Teams that need auditable change paths before broader adoption.',
|
|
208
|
+
},
|
|
209
|
+
];
|
|
210
|
+
|
|
211
|
+
const PILOT_ROLLOUT_KIT = {
|
|
212
|
+
recommendedScope: [
|
|
213
|
+
'Pick 1-2 repos with active maintainers and low blast radius.',
|
|
214
|
+
'Run discover and suggest-only first; avoid direct writes on mature repos.',
|
|
215
|
+
'Choose one permission profile before any pilot starts.',
|
|
216
|
+
'Define success metrics before the first benchmark run.',
|
|
217
|
+
],
|
|
218
|
+
approvals: [
|
|
219
|
+
'Engineering owner approves scope and rollback expectations.',
|
|
220
|
+
'Security owner approves the selected permission profile and hooks.',
|
|
221
|
+
'Pilot owner records the benchmark baseline and acceptance criteria.',
|
|
222
|
+
],
|
|
223
|
+
successMetrics: [
|
|
224
|
+
'Score delta and organic score delta',
|
|
225
|
+
'Number of recommendations accepted',
|
|
226
|
+
'Time to first useful Claude workflow',
|
|
227
|
+
'Rollback-free apply rate',
|
|
228
|
+
],
|
|
229
|
+
rollbackExpectations: [
|
|
230
|
+
'Every apply batch must emit a rollback artifact.',
|
|
231
|
+
'If a created artifact is rejected, delete the files listed in the rollback manifest.',
|
|
232
|
+
'Record the rollback event in the activity log for auditability.',
|
|
233
|
+
],
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
function clone(value) {
|
|
237
|
+
return JSON.parse(JSON.stringify(value));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function mergeUnique(existing = [], additions = []) {
|
|
241
|
+
return [...new Set([...(Array.isArray(existing) ? existing : []), ...additions])];
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function mergeHooks(existingHooks = {}, nextHooks = {}) {
|
|
245
|
+
const merged = clone(existingHooks || {});
|
|
246
|
+
|
|
247
|
+
for (const [stage, blocks] of Object.entries(nextHooks)) {
|
|
248
|
+
const targetBlocks = Array.isArray(merged[stage]) ? clone(merged[stage]) : [];
|
|
249
|
+
for (const incoming of blocks) {
|
|
250
|
+
const index = targetBlocks.findIndex(block => block.matcher === incoming.matcher);
|
|
251
|
+
if (index === -1) {
|
|
252
|
+
targetBlocks.push(clone(incoming));
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const current = targetBlocks[index];
|
|
257
|
+
const existingCommands = new Set((current.hooks || []).map(hook => `${hook.type}:${hook.command}:${hook.timeout || ''}`));
|
|
258
|
+
const mergedHooks = [...(current.hooks || [])];
|
|
259
|
+
for (const hook of incoming.hooks || []) {
|
|
260
|
+
const signature = `${hook.type}:${hook.command}:${hook.timeout || ''}`;
|
|
261
|
+
if (!existingCommands.has(signature)) {
|
|
262
|
+
mergedHooks.push(clone(hook));
|
|
263
|
+
existingCommands.add(signature);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
targetBlocks[index] = { ...current, hooks: mergedHooks };
|
|
267
|
+
}
|
|
268
|
+
merged[stage] = targetBlocks;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return merged;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function getPermissionProfile(key = 'safe-write') {
|
|
275
|
+
return PERMISSION_PROFILES.find(profile => profile.key === key) ||
|
|
276
|
+
PERMISSION_PROFILES.find(profile => profile.key === 'safe-write');
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function isWritableProfile(key = 'safe-write') {
|
|
280
|
+
return ['safe-write', 'power-user', 'internal-research'].includes(getPermissionProfile(key).key);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function ensureWritableProfile(key = 'safe-write', commandName = 'apply', dryRun = false) {
|
|
284
|
+
const profile = getPermissionProfile(key);
|
|
285
|
+
if (!dryRun && !isWritableProfile(profile.key)) {
|
|
286
|
+
throw new Error(`${commandName} requires a writable profile. Use --profile safe-write or --dry-run.`);
|
|
287
|
+
}
|
|
288
|
+
return profile;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function buildHookConfig(hookFiles, profileKey) {
|
|
292
|
+
const profile = getPermissionProfile(profileKey);
|
|
293
|
+
if (!isWritableProfile(profile.key)) {
|
|
294
|
+
return {};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const uniqueFiles = [...new Set(hookFiles)].sort();
|
|
298
|
+
if (uniqueFiles.length === 0) {
|
|
299
|
+
return {};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
302
|
// Detect hook runtime: .js files use node, .sh files use bash
|
|
303
303
|
const hookCommand = (file) => {
|
|
304
304
|
if (file.endsWith('.js')) return `node .claude/hooks/${file}`;
|
|
@@ -317,30 +317,30 @@ function buildHookConfig(hookFiles, profileKey) {
|
|
|
317
317
|
type: 'command',
|
|
318
318
|
command: hookCommand(file),
|
|
319
319
|
timeout: 10,
|
|
320
|
-
})),
|
|
321
|
-
}],
|
|
322
|
-
};
|
|
323
|
-
|
|
324
|
-
const secretsFile = uniqueFiles.find(isSecrets);
|
|
325
|
-
if (secretsFile) {
|
|
326
|
-
hookConfig.PreToolUse = [{
|
|
327
|
-
matcher: 'Read|Write|Edit|Bash',
|
|
328
|
-
hooks: [{
|
|
329
|
-
type: 'command',
|
|
330
|
-
command: hookCommand(secretsFile),
|
|
331
|
-
timeout: 5,
|
|
332
|
-
}],
|
|
333
|
-
}];
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
const sessionFile = uniqueFiles.find(isSession);
|
|
320
|
+
})),
|
|
321
|
+
}],
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
const secretsFile = uniqueFiles.find(isSecrets);
|
|
325
|
+
if (secretsFile) {
|
|
326
|
+
hookConfig.PreToolUse = [{
|
|
327
|
+
matcher: 'Read|Write|Edit|Bash',
|
|
328
|
+
hooks: [{
|
|
329
|
+
type: 'command',
|
|
330
|
+
command: hookCommand(secretsFile),
|
|
331
|
+
timeout: 5,
|
|
332
|
+
}],
|
|
333
|
+
}];
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const sessionFile = uniqueFiles.find(isSession);
|
|
337
337
|
if (sessionFile) {
|
|
338
338
|
hookConfig.SessionStart = [{
|
|
339
339
|
matcher: '*',
|
|
340
340
|
hooks: [{
|
|
341
341
|
type: 'command',
|
|
342
|
-
command: hookCommand(sessionFile),
|
|
343
|
-
timeout: 5,
|
|
342
|
+
command: hookCommand(sessionFile),
|
|
343
|
+
timeout: 5,
|
|
344
344
|
}],
|
|
345
345
|
}];
|
|
346
346
|
}
|
|
@@ -357,70 +357,70 @@ function buildHookConfig(hookFiles, profileKey) {
|
|
|
357
357
|
}],
|
|
358
358
|
});
|
|
359
359
|
}
|
|
360
|
-
|
|
361
|
-
if ((hookConfig.PostToolUse[0].hooks || []).length === 0) {
|
|
362
|
-
delete hookConfig.PostToolUse;
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
return hookConfig;
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
function buildSettingsForProfile({ profileKey = 'safe-write', hookFiles = [], existingSettings = null, mcpPackKeys = [] } = {}) {
|
|
369
|
-
const profile = getPermissionProfile(profileKey);
|
|
370
|
-
const base = existingSettings ? clone(existingSettings) : {};
|
|
371
|
-
const selectedMcpPacks = normalizeMcpPackKeys(mcpPackKeys);
|
|
372
|
-
base.permissions = base.permissions || {};
|
|
373
|
-
base.permissions.defaultMode = profile.defaultMode;
|
|
374
|
-
base.permissions.deny = mergeUnique(base.permissions.deny, profile.deny);
|
|
375
|
-
|
|
376
|
-
const hookConfig = buildHookConfig(hookFiles, profile.key);
|
|
377
|
-
if (Object.keys(hookConfig).length > 0) {
|
|
378
|
-
base.hooks = mergeHooks(base.hooks, hookConfig);
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
if (selectedMcpPacks.length > 0) {
|
|
382
|
-
base.mcpServers = mergeMcpServers(base.mcpServers, selectedMcpPacks);
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
base.nerviqSetup = {
|
|
386
|
-
...(base.nerviqSetup || {}),
|
|
387
|
-
profile: profile.key,
|
|
388
|
-
mcpPacks: selectedMcpPacks,
|
|
389
|
-
};
|
|
390
|
-
|
|
391
|
-
return base;
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
/**
|
|
395
|
-
* Return the full governance surface: permission profiles, hooks, policy packs, and pilot kit.
|
|
396
|
-
* @returns {Object} Summary containing permissionProfiles, hookRegistry, policyPacks, domainPacks, mcpPacks, and pilotRolloutKit.
|
|
397
|
-
*/
|
|
398
|
-
function getGovernanceSummary(platform = 'claude') {
|
|
399
|
-
if (platform === 'codex') {
|
|
400
|
-
return getCodexGovernanceSummary();
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
return {
|
|
404
|
-
platform: 'claude',
|
|
405
|
-
platformLabel: 'Claude',
|
|
406
|
-
permissionProfiles: PERMISSION_PROFILES,
|
|
407
|
-
hookRegistry: HOOK_REGISTRY,
|
|
408
|
-
policyPacks: POLICY_PACKS,
|
|
409
|
-
domainPacks: DOMAIN_PACKS,
|
|
410
|
-
mcpPacks: MCP_PACKS,
|
|
411
|
-
pilotRolloutKit: PILOT_ROLLOUT_KIT,
|
|
412
|
-
};
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
function printGovernanceSummary(summary, options = {}) {
|
|
416
|
-
if (options.json) {
|
|
417
|
-
console.log(JSON.stringify(summary, null, 2));
|
|
418
|
-
return;
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
console.log('');
|
|
422
|
-
console.log(` nerviq ${summary.platformLabel.toLowerCase()} governance`);
|
|
423
|
-
console.log(' ═══════════════════════════════════════');
|
|
360
|
+
|
|
361
|
+
if ((hookConfig.PostToolUse[0].hooks || []).length === 0) {
|
|
362
|
+
delete hookConfig.PostToolUse;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return hookConfig;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function buildSettingsForProfile({ profileKey = 'safe-write', hookFiles = [], existingSettings = null, mcpPackKeys = [] } = {}) {
|
|
369
|
+
const profile = getPermissionProfile(profileKey);
|
|
370
|
+
const base = existingSettings ? clone(existingSettings) : {};
|
|
371
|
+
const selectedMcpPacks = normalizeMcpPackKeys(mcpPackKeys);
|
|
372
|
+
base.permissions = base.permissions || {};
|
|
373
|
+
base.permissions.defaultMode = profile.defaultMode;
|
|
374
|
+
base.permissions.deny = mergeUnique(base.permissions.deny, profile.deny);
|
|
375
|
+
|
|
376
|
+
const hookConfig = buildHookConfig(hookFiles, profile.key);
|
|
377
|
+
if (Object.keys(hookConfig).length > 0) {
|
|
378
|
+
base.hooks = mergeHooks(base.hooks, hookConfig);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (selectedMcpPacks.length > 0) {
|
|
382
|
+
base.mcpServers = mergeMcpServers(base.mcpServers, selectedMcpPacks);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
base.nerviqSetup = {
|
|
386
|
+
...(base.nerviqSetup || {}),
|
|
387
|
+
profile: profile.key,
|
|
388
|
+
mcpPacks: selectedMcpPacks,
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
return base;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Return the full governance surface: permission profiles, hooks, policy packs, and pilot kit.
|
|
396
|
+
* @returns {Object} Summary containing permissionProfiles, hookRegistry, policyPacks, domainPacks, mcpPacks, and pilotRolloutKit.
|
|
397
|
+
*/
|
|
398
|
+
function getGovernanceSummary(platform = 'claude') {
|
|
399
|
+
if (platform === 'codex') {
|
|
400
|
+
return getCodexGovernanceSummary();
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return {
|
|
404
|
+
platform: 'claude',
|
|
405
|
+
platformLabel: 'Claude',
|
|
406
|
+
permissionProfiles: PERMISSION_PROFILES,
|
|
407
|
+
hookRegistry: HOOK_REGISTRY,
|
|
408
|
+
policyPacks: POLICY_PACKS,
|
|
409
|
+
domainPacks: DOMAIN_PACKS,
|
|
410
|
+
mcpPacks: MCP_PACKS,
|
|
411
|
+
pilotRolloutKit: PILOT_ROLLOUT_KIT,
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function printGovernanceSummary(summary, options = {}) {
|
|
416
|
+
if (options.json) {
|
|
417
|
+
console.log(JSON.stringify(summary, null, 2));
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
console.log('');
|
|
422
|
+
console.log(` nerviq ${summary.platformLabel.toLowerCase()} governance`);
|
|
423
|
+
console.log(' ═══════════════════════════════════════');
|
|
424
424
|
console.log(` Safe defaults, hook transparency, and pilot guidance for ${summary.platformLabel}.`);
|
|
425
425
|
console.log('');
|
|
426
426
|
|
|
@@ -430,166 +430,166 @@ function printGovernanceSummary(summary, options = {}) {
|
|
|
430
430
|
console.log('');
|
|
431
431
|
|
|
432
432
|
console.log(' Permission Profiles');
|
|
433
|
-
for (const profile of summary.permissionProfiles) {
|
|
434
|
-
console.log(` - ${profile.label} [${profile.risk}]`);
|
|
435
|
-
console.log(` ${profile.useWhen}`);
|
|
436
|
-
console.log(` defaultMode=${profile.defaultMode}`);
|
|
437
|
-
}
|
|
438
|
-
console.log('');
|
|
439
|
-
|
|
440
|
-
console.log(' Hook Registry');
|
|
441
|
-
for (const hook of summary.hookRegistry) {
|
|
442
|
-
const riskColor = hook.risk === 'low' ? '\x1b[32m' : hook.risk === 'medium' ? '\x1b[33m' : '\x1b[31m';
|
|
443
|
-
const rl = hook.riskLevel || classifyHookRiskLevel(hook);
|
|
444
|
-
const rlColor = rl === 'low' ? '\x1b[32m' : rl === 'medium' ? '\x1b[33m' : '\x1b[31m';
|
|
445
|
-
console.log(` - ${hook.file} ${riskColor}[${hook.risk} risk]\x1b[0m ${rlColor}[${rl} riskLevel]\x1b[0m`);
|
|
446
|
-
console.log(` ${hook.triggerPoint}${hook.matcher ? ` ${hook.matcher}` : ''} -> ${hook.purpose}`);
|
|
447
|
-
}
|
|
448
|
-
console.log('');
|
|
449
|
-
|
|
450
|
-
console.log(' Policy Packs');
|
|
451
|
-
for (const pack of summary.policyPacks) {
|
|
452
|
-
console.log(` - ${pack.label}: ${pack.modules.join(', ')}`);
|
|
453
|
-
}
|
|
454
|
-
console.log('');
|
|
455
|
-
|
|
456
|
-
const domainPacks = summary.domainPacks || [];
|
|
457
|
-
const mcpPacks = summary.mcpPacks || [];
|
|
458
|
-
const compact = !options.verbose;
|
|
459
|
-
const COMPACT_LIMIT = 5;
|
|
460
|
-
|
|
461
|
-
console.log(` Domain Packs (${domainPacks.length})`);
|
|
462
|
-
if (domainPacks.length === 0) {
|
|
463
|
-
console.log(' - none shipped yet for this platform');
|
|
464
|
-
}
|
|
465
|
-
const domainShow = compact ? domainPacks.slice(0, COMPACT_LIMIT) : domainPacks;
|
|
466
|
-
for (const pack of domainShow) {
|
|
467
|
-
console.log(` - ${pack.label}: ${pack.useWhen}`);
|
|
468
|
-
}
|
|
469
|
-
if (compact && domainPacks.length > COMPACT_LIMIT) {
|
|
470
|
-
console.log(` ... and ${domainPacks.length - COMPACT_LIMIT} more (use --verbose to see all)`);
|
|
471
|
-
}
|
|
472
|
-
console.log('');
|
|
473
|
-
|
|
474
|
-
console.log(` MCP Packs (${mcpPacks.length})`);
|
|
475
|
-
if (mcpPacks.length === 0) {
|
|
476
|
-
console.log(' - none shipped yet for this platform');
|
|
477
|
-
}
|
|
478
|
-
const mcpShow = compact ? mcpPacks.slice(0, COMPACT_LIMIT) : mcpPacks;
|
|
479
|
-
for (const pack of mcpShow) {
|
|
480
|
-
console.log(` - ${pack.label}: ${Object.keys(pack.servers).join(', ')}`);
|
|
481
|
-
}
|
|
482
|
-
if (compact && mcpPacks.length > COMPACT_LIMIT) {
|
|
483
|
-
console.log(` ... and ${mcpPacks.length - COMPACT_LIMIT} more (use --verbose to see all)`);
|
|
484
|
-
}
|
|
485
|
-
console.log('');
|
|
486
|
-
|
|
487
|
-
if (Array.isArray(summary.platformCaveats) && summary.platformCaveats.length > 0) {
|
|
488
|
-
console.log(' Platform Caveats');
|
|
489
|
-
for (const item of summary.platformCaveats) {
|
|
490
|
-
console.log(` - ${item}`);
|
|
491
|
-
}
|
|
492
|
-
console.log('');
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
console.log(' Pilot Rollout Kit');
|
|
496
|
-
for (const item of summary.pilotRolloutKit.recommendedScope) {
|
|
497
|
-
console.log(` - ${item}`);
|
|
498
|
-
}
|
|
499
|
-
console.log('');
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
/**
|
|
503
|
-
* Render a governance summary as a formatted markdown string.
|
|
504
|
-
* @param {Object} summary - The summary object returned by getGovernanceSummary().
|
|
505
|
-
* @returns {string} Markdown-formatted governance report.
|
|
506
|
-
*/
|
|
507
|
-
function renderGovernanceMarkdown(summary) {
|
|
508
|
-
const lines = [
|
|
509
|
-
'# NERVIQ CLI Governance Report',
|
|
510
|
-
'',
|
|
511
|
-
`Platform: ${summary.platformLabel}`,
|
|
512
|
-
'',
|
|
513
|
-
`This report summarizes the shipped governance surface for ${summary.platformLabel} rollout, review, and pilot approval.`,
|
|
514
|
-
'',
|
|
515
|
-
'## Permission Profiles',
|
|
516
|
-
];
|
|
517
|
-
|
|
518
|
-
for (const profile of summary.permissionProfiles) {
|
|
519
|
-
lines.push(`- **${profile.label}** \`${profile.key}\` | risk: \`${profile.risk}\` | defaultMode: \`${profile.defaultMode}\``);
|
|
520
|
-
lines.push(` - Use when: ${profile.useWhen}`);
|
|
521
|
-
lines.push(` - Behavior: ${profile.behavior}`);
|
|
522
|
-
if (Array.isArray(profile.deny) && profile.deny.length > 0) {
|
|
523
|
-
lines.push(` - Deny rules: ${profile.deny.join(', ')}`);
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
lines.push('', '## Hook Registry');
|
|
528
|
-
for (const hook of summary.hookRegistry) {
|
|
529
|
-
const rl = hook.riskLevel || classifyHookRiskLevel(hook);
|
|
530
|
-
lines.push(`- **${hook.key}** \`${hook.triggerPoint}${hook.matcher ? ` ${hook.matcher}` : ''}\` | risk: \`${hook.risk}\` | riskLevel: \`${rl}\``);
|
|
531
|
-
lines.push(` - File: ${hook.file}`);
|
|
532
|
-
lines.push(` - Purpose: ${hook.purpose}`);
|
|
533
|
-
lines.push(` - Dry run: ${hook.dryRunExample}`);
|
|
534
|
-
lines.push(` - Rollback: ${hook.rollbackPath}`);
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
lines.push('', '## Policy Packs');
|
|
538
|
-
for (const pack of summary.policyPacks) {
|
|
539
|
-
lines.push(`- **${pack.label}**`);
|
|
540
|
-
lines.push(` - Use when: ${pack.useWhen}`);
|
|
541
|
-
lines.push(` - Modules: ${pack.modules.join(', ')}`);
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
lines.push('', `## Domain Packs (${(summary.domainPacks || []).length})`);
|
|
545
|
-
for (const pack of summary.domainPacks || []) {
|
|
546
|
-
lines.push(`- **${pack.label}**: ${pack.useWhen}`);
|
|
547
|
-
}
|
|
548
|
-
if ((summary.domainPacks || []).length === 0) {
|
|
549
|
-
lines.push('- None shipped yet for this platform.');
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
lines.push('', `## MCP Packs (${(summary.mcpPacks || []).length})`);
|
|
553
|
-
for (const pack of summary.mcpPacks || []) {
|
|
554
|
-
lines.push(`- **${pack.label}**: ${Object.keys(pack.servers).join(', ')}`);
|
|
555
|
-
}
|
|
556
|
-
if ((summary.mcpPacks || []).length === 0) {
|
|
557
|
-
lines.push('- None shipped yet for this platform.');
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
if (Array.isArray(summary.platformCaveats) && summary.platformCaveats.length > 0) {
|
|
561
|
-
lines.push('', '## Platform Caveats');
|
|
562
|
-
for (const item of summary.platformCaveats) {
|
|
563
|
-
lines.push(`- ${item}`);
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
lines.push('', '## Pilot Rollout Kit', '### Recommended Scope');
|
|
568
|
-
for (const item of summary.pilotRolloutKit.recommendedScope) {
|
|
569
|
-
lines.push(`- ${item}`);
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
lines.push('', '### Approvals');
|
|
573
|
-
for (const item of summary.pilotRolloutKit.approvals) {
|
|
574
|
-
lines.push(`- ${item}`);
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
lines.push('', '### Success Metrics');
|
|
578
|
-
for (const item of summary.pilotRolloutKit.successMetrics) {
|
|
579
|
-
lines.push(`- ${item}`);
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
lines.push('', '### Rollback Expectations');
|
|
583
|
-
for (const item of summary.pilotRolloutKit.rollbackExpectations) {
|
|
584
|
-
lines.push(`- ${item}`);
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
lines.push('');
|
|
588
|
-
lines.push('---');
|
|
589
|
-
lines.push(`*Generated by nerviq v${require('../package.json').version} on ${new Date().toISOString().split('T')[0]}*`);
|
|
590
|
-
return lines.join('\n');
|
|
591
|
-
}
|
|
592
|
-
|
|
433
|
+
for (const profile of summary.permissionProfiles) {
|
|
434
|
+
console.log(` - ${profile.label} [${profile.risk}]`);
|
|
435
|
+
console.log(` ${profile.useWhen}`);
|
|
436
|
+
console.log(` defaultMode=${profile.defaultMode}`);
|
|
437
|
+
}
|
|
438
|
+
console.log('');
|
|
439
|
+
|
|
440
|
+
console.log(' Hook Registry');
|
|
441
|
+
for (const hook of summary.hookRegistry) {
|
|
442
|
+
const riskColor = hook.risk === 'low' ? '\x1b[32m' : hook.risk === 'medium' ? '\x1b[33m' : '\x1b[31m';
|
|
443
|
+
const rl = hook.riskLevel || classifyHookRiskLevel(hook);
|
|
444
|
+
const rlColor = rl === 'low' ? '\x1b[32m' : rl === 'medium' ? '\x1b[33m' : '\x1b[31m';
|
|
445
|
+
console.log(` - ${hook.file} ${riskColor}[${hook.risk} risk]\x1b[0m ${rlColor}[${rl} riskLevel]\x1b[0m`);
|
|
446
|
+
console.log(` ${hook.triggerPoint}${hook.matcher ? ` ${hook.matcher}` : ''} -> ${hook.purpose}`);
|
|
447
|
+
}
|
|
448
|
+
console.log('');
|
|
449
|
+
|
|
450
|
+
console.log(' Policy Packs');
|
|
451
|
+
for (const pack of summary.policyPacks) {
|
|
452
|
+
console.log(` - ${pack.label}: ${pack.modules.join(', ')}`);
|
|
453
|
+
}
|
|
454
|
+
console.log('');
|
|
455
|
+
|
|
456
|
+
const domainPacks = summary.domainPacks || [];
|
|
457
|
+
const mcpPacks = summary.mcpPacks || [];
|
|
458
|
+
const compact = !options.verbose;
|
|
459
|
+
const COMPACT_LIMIT = 5;
|
|
460
|
+
|
|
461
|
+
console.log(` Domain Packs (${domainPacks.length})`);
|
|
462
|
+
if (domainPacks.length === 0) {
|
|
463
|
+
console.log(' - none shipped yet for this platform');
|
|
464
|
+
}
|
|
465
|
+
const domainShow = compact ? domainPacks.slice(0, COMPACT_LIMIT) : domainPacks;
|
|
466
|
+
for (const pack of domainShow) {
|
|
467
|
+
console.log(` - ${pack.label}: ${pack.useWhen}`);
|
|
468
|
+
}
|
|
469
|
+
if (compact && domainPacks.length > COMPACT_LIMIT) {
|
|
470
|
+
console.log(` ... and ${domainPacks.length - COMPACT_LIMIT} more (use --verbose to see all)`);
|
|
471
|
+
}
|
|
472
|
+
console.log('');
|
|
473
|
+
|
|
474
|
+
console.log(` MCP Packs (${mcpPacks.length})`);
|
|
475
|
+
if (mcpPacks.length === 0) {
|
|
476
|
+
console.log(' - none shipped yet for this platform');
|
|
477
|
+
}
|
|
478
|
+
const mcpShow = compact ? mcpPacks.slice(0, COMPACT_LIMIT) : mcpPacks;
|
|
479
|
+
for (const pack of mcpShow) {
|
|
480
|
+
console.log(` - ${pack.label}: ${Object.keys(pack.servers).join(', ')}`);
|
|
481
|
+
}
|
|
482
|
+
if (compact && mcpPacks.length > COMPACT_LIMIT) {
|
|
483
|
+
console.log(` ... and ${mcpPacks.length - COMPACT_LIMIT} more (use --verbose to see all)`);
|
|
484
|
+
}
|
|
485
|
+
console.log('');
|
|
486
|
+
|
|
487
|
+
if (Array.isArray(summary.platformCaveats) && summary.platformCaveats.length > 0) {
|
|
488
|
+
console.log(' Platform Caveats');
|
|
489
|
+
for (const item of summary.platformCaveats) {
|
|
490
|
+
console.log(` - ${item}`);
|
|
491
|
+
}
|
|
492
|
+
console.log('');
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
console.log(' Pilot Rollout Kit');
|
|
496
|
+
for (const item of summary.pilotRolloutKit.recommendedScope) {
|
|
497
|
+
console.log(` - ${item}`);
|
|
498
|
+
}
|
|
499
|
+
console.log('');
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Render a governance summary as a formatted markdown string.
|
|
504
|
+
* @param {Object} summary - The summary object returned by getGovernanceSummary().
|
|
505
|
+
* @returns {string} Markdown-formatted governance report.
|
|
506
|
+
*/
|
|
507
|
+
function renderGovernanceMarkdown(summary) {
|
|
508
|
+
const lines = [
|
|
509
|
+
'# NERVIQ CLI Governance Report',
|
|
510
|
+
'',
|
|
511
|
+
`Platform: ${summary.platformLabel}`,
|
|
512
|
+
'',
|
|
513
|
+
`This report summarizes the shipped governance surface for ${summary.platformLabel} rollout, review, and pilot approval.`,
|
|
514
|
+
'',
|
|
515
|
+
'## Permission Profiles',
|
|
516
|
+
];
|
|
517
|
+
|
|
518
|
+
for (const profile of summary.permissionProfiles) {
|
|
519
|
+
lines.push(`- **${profile.label}** \`${profile.key}\` | risk: \`${profile.risk}\` | defaultMode: \`${profile.defaultMode}\``);
|
|
520
|
+
lines.push(` - Use when: ${profile.useWhen}`);
|
|
521
|
+
lines.push(` - Behavior: ${profile.behavior}`);
|
|
522
|
+
if (Array.isArray(profile.deny) && profile.deny.length > 0) {
|
|
523
|
+
lines.push(` - Deny rules: ${profile.deny.join(', ')}`);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
lines.push('', '## Hook Registry');
|
|
528
|
+
for (const hook of summary.hookRegistry) {
|
|
529
|
+
const rl = hook.riskLevel || classifyHookRiskLevel(hook);
|
|
530
|
+
lines.push(`- **${hook.key}** \`${hook.triggerPoint}${hook.matcher ? ` ${hook.matcher}` : ''}\` | risk: \`${hook.risk}\` | riskLevel: \`${rl}\``);
|
|
531
|
+
lines.push(` - File: ${hook.file}`);
|
|
532
|
+
lines.push(` - Purpose: ${hook.purpose}`);
|
|
533
|
+
lines.push(` - Dry run: ${hook.dryRunExample}`);
|
|
534
|
+
lines.push(` - Rollback: ${hook.rollbackPath}`);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
lines.push('', '## Policy Packs');
|
|
538
|
+
for (const pack of summary.policyPacks) {
|
|
539
|
+
lines.push(`- **${pack.label}**`);
|
|
540
|
+
lines.push(` - Use when: ${pack.useWhen}`);
|
|
541
|
+
lines.push(` - Modules: ${pack.modules.join(', ')}`);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
lines.push('', `## Domain Packs (${(summary.domainPacks || []).length})`);
|
|
545
|
+
for (const pack of summary.domainPacks || []) {
|
|
546
|
+
lines.push(`- **${pack.label}**: ${pack.useWhen}`);
|
|
547
|
+
}
|
|
548
|
+
if ((summary.domainPacks || []).length === 0) {
|
|
549
|
+
lines.push('- None shipped yet for this platform.');
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
lines.push('', `## MCP Packs (${(summary.mcpPacks || []).length})`);
|
|
553
|
+
for (const pack of summary.mcpPacks || []) {
|
|
554
|
+
lines.push(`- **${pack.label}**: ${Object.keys(pack.servers).join(', ')}`);
|
|
555
|
+
}
|
|
556
|
+
if ((summary.mcpPacks || []).length === 0) {
|
|
557
|
+
lines.push('- None shipped yet for this platform.');
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
if (Array.isArray(summary.platformCaveats) && summary.platformCaveats.length > 0) {
|
|
561
|
+
lines.push('', '## Platform Caveats');
|
|
562
|
+
for (const item of summary.platformCaveats) {
|
|
563
|
+
lines.push(`- ${item}`);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
lines.push('', '## Pilot Rollout Kit', '### Recommended Scope');
|
|
568
|
+
for (const item of summary.pilotRolloutKit.recommendedScope) {
|
|
569
|
+
lines.push(`- ${item}`);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
lines.push('', '### Approvals');
|
|
573
|
+
for (const item of summary.pilotRolloutKit.approvals) {
|
|
574
|
+
lines.push(`- ${item}`);
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
lines.push('', '### Success Metrics');
|
|
578
|
+
for (const item of summary.pilotRolloutKit.successMetrics) {
|
|
579
|
+
lines.push(`- ${item}`);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
lines.push('', '### Rollback Expectations');
|
|
583
|
+
for (const item of summary.pilotRolloutKit.rollbackExpectations) {
|
|
584
|
+
lines.push(`- ${item}`);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
lines.push('');
|
|
588
|
+
lines.push('---');
|
|
589
|
+
lines.push(`*Generated by nerviq v${require('../package.json').version} on ${new Date().toISOString().split('T')[0]}*`);
|
|
590
|
+
return lines.join('\n');
|
|
591
|
+
}
|
|
592
|
+
|
|
593
593
|
module.exports = {
|
|
594
594
|
PERMISSION_PROFILES,
|
|
595
595
|
HOOK_REGISTRY,
|
|
@@ -597,9 +597,9 @@ module.exports = {
|
|
|
597
597
|
getPermissionProfile,
|
|
598
598
|
isWritableProfile,
|
|
599
599
|
ensureWritableProfile,
|
|
600
|
-
buildSettingsForProfile,
|
|
601
|
-
getGovernanceSummary,
|
|
602
|
-
printGovernanceSummary,
|
|
603
|
-
renderGovernanceMarkdown,
|
|
604
|
-
classifyHookRiskLevel,
|
|
605
|
-
};
|
|
600
|
+
buildSettingsForProfile,
|
|
601
|
+
getGovernanceSummary,
|
|
602
|
+
printGovernanceSummary,
|
|
603
|
+
renderGovernanceMarkdown,
|
|
604
|
+
classifyHookRiskLevel,
|
|
605
|
+
};
|