@nerviq/cli 1.11.0 → 1.13.0

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.
Files changed (62) hide show
  1. package/README.md +216 -124
  2. package/bin/cli.js +620 -183
  3. package/package.json +3 -2
  4. package/src/activity.js +49 -9
  5. package/src/adoption-advisor.js +299 -0
  6. package/src/aider/freshness.js +65 -20
  7. package/src/aider/techniques.js +16 -11
  8. package/src/analyze.js +128 -0
  9. package/src/anti-patterns.js +13 -0
  10. package/src/audit/instruction-files.js +180 -0
  11. package/src/audit/recommendations.js +531 -0
  12. package/src/audit.js +53 -681
  13. package/src/behavioral-drift.js +801 -0
  14. package/src/codex/freshness.js +84 -25
  15. package/src/continuous-ops.js +681 -0
  16. package/src/copilot/freshness.js +57 -20
  17. package/src/cost-tracking.js +61 -0
  18. package/src/cursor/freshness.js +65 -20
  19. package/src/cursor/techniques.js +17 -12
  20. package/src/deep-review.js +83 -0
  21. package/src/diff-only.js +280 -0
  22. package/src/doctor.js +118 -55
  23. package/src/freshness.js +74 -21
  24. package/src/gemini/freshness.js +66 -21
  25. package/src/governance.js +59 -43
  26. package/src/hook-validation.js +342 -0
  27. package/src/index.js +5 -0
  28. package/src/integrations.js +42 -5
  29. package/src/mcp-server.js +95 -59
  30. package/src/mcp-validation.js +337 -0
  31. package/src/opencode/freshness.js +66 -21
  32. package/src/opencode/techniques.js +12 -7
  33. package/src/operating-profile.js +574 -0
  34. package/src/org.js +97 -13
  35. package/src/plans.js +192 -8
  36. package/src/platform-change-manifest.js +86 -0
  37. package/src/policy-layers.js +210 -0
  38. package/src/profiles.js +4 -1
  39. package/src/prompt-injection.js +74 -0
  40. package/src/repo-archetype.js +386 -0
  41. package/src/setup/analysis.js +619 -0
  42. package/src/setup/runtime.js +172 -0
  43. package/src/setup.js +62 -748
  44. package/src/source-urls.js +132 -132
  45. package/src/supplemental-checks.js +13 -12
  46. package/src/techniques/api.js +407 -0
  47. package/src/techniques/automation.js +316 -0
  48. package/src/techniques/compliance.js +257 -0
  49. package/src/techniques/hygiene.js +294 -0
  50. package/src/techniques/instructions.js +243 -0
  51. package/src/techniques/observability.js +226 -0
  52. package/src/techniques/optimization.js +142 -0
  53. package/src/techniques/quality.js +317 -0
  54. package/src/techniques/security.js +237 -0
  55. package/src/techniques/shared.js +443 -0
  56. package/src/techniques/stacks.js +2294 -0
  57. package/src/techniques/tools.js +106 -0
  58. package/src/techniques/workflow.js +413 -0
  59. package/src/techniques.js +78 -5607
  60. package/src/watch.js +18 -0
  61. package/src/windsurf/freshness.js +36 -21
  62. package/src/windsurf/techniques.js +17 -12
@@ -0,0 +1,294 @@
1
+ /**
2
+ * Hygiene technique fragments.
3
+ * Generated mechanically from the legacy techniques.js monolith during HR-09.
4
+ */
5
+
6
+ const {
7
+ hasFrontendSignals,
8
+ hasProjectFile,
9
+ readProjectFiles,
10
+ isGoProject,
11
+ containsEmbeddedSecret,
12
+ } = require('./shared');
13
+
14
+ module.exports = {
15
+ gitIgnoreClaudeTracked: {
16
+ id: 976,
17
+ name: '.claude/ tracked in git',
18
+ check: (ctx) => {
19
+ if (!ctx.fileContent('.gitignore')) return true; // no gitignore = ok
20
+ const lines = ctx.fileContent('.gitignore')
21
+ .split(/\r?\n/)
22
+ .map(line => line.trim())
23
+ .filter(line => line && !line.startsWith('#'));
24
+ const ignoresClaudeDir = lines.some(line => /^(\/|\*\*\/)?\.claude\/?$/.test(line));
25
+ const unignoresClaudeDir = lines.some(line => /^!(\/)?\.claude(\/|\*\*)?$/.test(line));
26
+ return !ignoresClaudeDir || unignoresClaudeDir;
27
+ },
28
+ impact: 'high',
29
+ rating: 4,
30
+ category: 'git',
31
+ fix: 'Remove .claude/ from .gitignore (keep .claude/settings.local.json ignored).',
32
+ template: null
33
+ },
34
+
35
+ gitIgnoreEnv: {
36
+ id: 917,
37
+ name: '.gitignore blocks .env files',
38
+ check: (ctx) => {
39
+ const gitignore = ctx.fileContent('.gitignore') || '';
40
+ return gitignore.includes('.env');
41
+ },
42
+ impact: 'critical',
43
+ rating: 5,
44
+ category: 'git',
45
+ fix: 'Add .env to .gitignore to prevent leaking secrets.',
46
+ template: null
47
+ },
48
+
49
+ gitIgnoreNodeModules: {
50
+ id: 91701,
51
+ name: '.gitignore blocks node_modules',
52
+ check: (ctx) => {
53
+ const hasNodeSignals = ctx.files.includes('package.json') ||
54
+ ctx.files.includes('tsconfig.json') ||
55
+ ctx.files.some(f => /package-lock\.json|pnpm-lock\.yaml|yarn\.lock|next\.config|vite\.config/i.test(f));
56
+ if (!hasNodeSignals) return null;
57
+ const gitignore = ctx.fileContent('.gitignore') || '';
58
+ return gitignore.includes('node_modules');
59
+ },
60
+ impact: 'high',
61
+ rating: 4,
62
+ category: 'git',
63
+ fix: 'Add node_modules/ to .gitignore.',
64
+ template: null
65
+ },
66
+
67
+ noSecretsInClaude: {
68
+ id: 1039,
69
+ name: 'CLAUDE.md has no embedded secrets',
70
+ check: (ctx) => {
71
+ const md = ctx.claudeMdContent() || '';
72
+ return !containsEmbeddedSecret(md);
73
+ },
74
+ impact: 'critical',
75
+ rating: 5,
76
+ category: 'git',
77
+ fix: 'Remove hardcoded secrets, tokens, private keys, and connection strings from CLAUDE.md. Use environment variables or external secret stores instead.',
78
+ template: null
79
+ },
80
+
81
+ readme: {
82
+ id: 416,
83
+ name: 'Has README.md',
84
+ check: (ctx) => ctx.files.some(f => /^readme\.md$/i.test(f)),
85
+ impact: 'high',
86
+ rating: 4,
87
+ category: 'hygiene',
88
+ fix: 'Add a README.md with project overview, setup instructions, and usage.',
89
+ template: null
90
+ },
91
+
92
+ changelog: {
93
+ id: 417,
94
+ name: 'Has CHANGELOG.md',
95
+ check: (ctx) => ctx.files.some(f => /^changelog\.md$/i.test(f)),
96
+ impact: 'low',
97
+ rating: 3,
98
+ category: 'hygiene',
99
+ fix: 'Add a CHANGELOG.md to track notable changes across versions.',
100
+ template: null
101
+ },
102
+
103
+ contributing: {
104
+ id: 418,
105
+ name: 'Has CONTRIBUTING.md',
106
+ check: (ctx) => ctx.files.some(f => /^contributing\.md$/i.test(f)),
107
+ impact: 'low',
108
+ rating: 3,
109
+ category: 'hygiene',
110
+ fix: 'Add a CONTRIBUTING.md with contribution guidelines and code standards.',
111
+ template: null
112
+ },
113
+
114
+ license: {
115
+ id: 434,
116
+ name: 'Has LICENSE file',
117
+ check: (ctx) => ctx.files.some(f => /^license/i.test(f)),
118
+ impact: 'low',
119
+ rating: 3,
120
+ category: 'hygiene',
121
+ fix: 'Add a LICENSE file to clarify usage rights.',
122
+ template: null
123
+ },
124
+
125
+ editorconfig: {
126
+ id: 5001,
127
+ name: 'Has .editorconfig',
128
+ check: (ctx) => ctx.files.includes('.editorconfig'),
129
+ impact: 'low',
130
+ rating: 3,
131
+ category: 'hygiene',
132
+ fix: 'Add .editorconfig for consistent formatting across editors and Claude.',
133
+ template: null
134
+ },
135
+
136
+ nvmrc: {
137
+ id: 5002,
138
+ name: 'Node version pinned',
139
+ check: (ctx) => {
140
+ const hasNodeSignals = ctx.files.includes('package.json') ||
141
+ ctx.files.includes('tsconfig.json') ||
142
+ ctx.files.some(f => /package-lock\.json|pnpm-lock\.yaml|yarn\.lock|next\.config|vite\.config/i.test(f));
143
+ if (!hasNodeSignals) return null;
144
+ if (ctx.files.includes('.nvmrc') || ctx.files.includes('.node-version')) return true;
145
+ const pkg = ctx.jsonFile('package.json');
146
+ return !!(pkg && pkg.engines && pkg.engines.node);
147
+ },
148
+ impact: 'low',
149
+ rating: 3,
150
+ category: 'hygiene',
151
+ fix: 'Add .nvmrc, .node-version, or engines.node in package.json to pin Node version.',
152
+ template: null
153
+ },
154
+
155
+ gitAttributionDecision: {
156
+ id: 2015,
157
+ name: 'Git attribution configured',
158
+ check: (ctx) => {
159
+ const shared = ctx.jsonFile('.claude/settings.json') || {};
160
+ const local = ctx.jsonFile('.claude/settings.local.json') || {};
161
+ return shared.attribution !== undefined || local.attribution !== undefined ||
162
+ shared.includeCoAuthoredBy !== undefined || local.includeCoAuthoredBy !== undefined;
163
+ },
164
+ impact: 'low', rating: 3, category: 'git',
165
+ fix: 'Decide on git attribution: set attribution.commit or includeCoAuthoredBy in settings.',
166
+ template: null
167
+ },
168
+
169
+ gitIgnoreClaudeLocal: {
170
+ id: 2028,
171
+ name: '.gitignore excludes settings.local.json',
172
+ check: (ctx) => {
173
+ const gitignore = ctx.fileContent('.gitignore') || '';
174
+ return /settings\.local\.json|settings\.local/i.test(gitignore);
175
+ },
176
+ impact: 'medium', rating: 4, category: 'git',
177
+ fix: 'Add .claude/settings.local.json to .gitignore. Personal overrides should not be committed.',
178
+ template: null
179
+ },
180
+
181
+ envExampleExists: {
182
+ id: 2029,
183
+ name: '.env.example or .env.template exists',
184
+ check: (ctx) => {
185
+ return !!(ctx.fileContent('.env.example') || ctx.fileContent('.env.template') || ctx.fileContent('.env.sample'));
186
+ },
187
+ impact: 'low', rating: 3, category: 'hygiene',
188
+ fix: 'Add .env.example so new developers know which environment variables are needed.',
189
+ template: null
190
+ },
191
+
192
+ packageJsonHasScripts: {
193
+ id: 2030,
194
+ name: 'package.json has dev/test/build scripts',
195
+ check: (ctx) => {
196
+ const pkg = ctx.jsonFile('package.json');
197
+ if (!pkg) return null;
198
+ const scripts = pkg.scripts || {};
199
+ const has = (k) => !!scripts[k];
200
+ return has('test') || has('dev') || has('build') || has('start');
201
+ },
202
+ impact: 'medium', rating: 3, category: 'hygiene',
203
+ fix: 'Add scripts to package.json (test, dev, build). Claude uses these for verification.',
204
+ template: null
205
+ },
206
+
207
+ gitignoreClaudeLocal: {
208
+ id: 2036,
209
+ name: 'CLAUDE.local.md in .gitignore',
210
+ check: (ctx) => {
211
+ const gitignore = ctx.fileContent('.gitignore') || '';
212
+ return /CLAUDE\.local\.md/i.test(gitignore);
213
+ },
214
+ impact: 'medium',
215
+ rating: 3,
216
+ category: 'git',
217
+ fix: 'Add CLAUDE.local.md to .gitignore — it contains personal overrides that should not be committed.',
218
+ template: null
219
+ },
220
+
221
+ readmeQuality: {
222
+ id: 130151,
223
+ name: 'README has installation, usage, and contributing sections',
224
+ check: (ctx) => {
225
+ const readme = ctx.fileContent('README.md') || '';
226
+ if (!readme) return false;
227
+ return /install/i.test(readme) && /usage/i.test(readme) && /contribut/i.test(readme);
228
+ },
229
+ impact: 'medium',
230
+ category: 'docs-quality',
231
+ fix: 'Ensure README.md includes installation, usage, and contributing sections for developer onboarding.',
232
+ confidence: 0.7,
233
+ },
234
+
235
+ contributingGuide: {
236
+ id: 130152,
237
+ name: 'CONTRIBUTING.md exists',
238
+ check: (ctx) => ctx.files.some(f => /^contributing\.md$/i.test(f)),
239
+ impact: 'low',
240
+ category: 'docs-quality',
241
+ fix: 'Add CONTRIBUTING.md with contribution guidelines, code standards, and PR process.',
242
+ confidence: 0.7,
243
+ },
244
+
245
+ apiDocsGenerated: {
246
+ id: 130153,
247
+ name: 'API documentation generator configured',
248
+ check: (ctx) => {
249
+ const pkg = ctx.fileContent('package.json') || '';
250
+ if (/typedoc|jsdoc|apidoc|compodoc/i.test(pkg)) return true;
251
+ const py = readProjectFiles(ctx, /(^|\/)pyproject\.toml$/i) + readProjectFiles(ctx, /(^|\/)requirements[^/]*\.txt$/i);
252
+ if (/sphinx|pdoc|mkdocstrings/i.test(py)) return true;
253
+ if (isGoProject(ctx) && hasProjectFile(ctx, /(^|\/)doc\.go$/i)) return true;
254
+ return false;
255
+ },
256
+ impact: 'low',
257
+ category: 'docs-quality',
258
+ fix: 'Add an API documentation generator (typedoc, jsdoc, sphinx, godoc) for auto-generated docs.',
259
+ confidence: 0.7,
260
+ },
261
+
262
+ storybookConfigured: {
263
+ id: 130154,
264
+ name: 'Storybook configured for component docs',
265
+ check: (ctx) => {
266
+ if (!hasFrontendSignals(ctx)) return null;
267
+ return ctx.hasDir('.storybook') || hasProjectFile(ctx, /(^|\/)\.storybook\//i);
268
+ },
269
+ impact: 'low',
270
+ category: 'docs-quality',
271
+ fix: 'Add Storybook (.storybook/) for interactive component documentation and visual testing.',
272
+ confidence: 0.7,
273
+ },
274
+
275
+ codeOfConduct: {
276
+ id: 130155,
277
+ name: 'CODE_OF_CONDUCT.md exists',
278
+ check: (ctx) => ctx.files.some(f => /^code.of.conduct\.md$/i.test(f)),
279
+ impact: 'low',
280
+ category: 'docs-quality',
281
+ fix: 'Add CODE_OF_CONDUCT.md to set community standards and expectations.',
282
+ confidence: 0.7,
283
+ },
284
+
285
+ licenseDeclared: {
286
+ id: 130156,
287
+ name: 'LICENSE file exists',
288
+ check: (ctx) => ctx.files.some(f => /^license/i.test(f)),
289
+ impact: 'low',
290
+ category: 'docs-quality',
291
+ fix: 'Add a LICENSE file to clarify usage rights and legal terms.',
292
+ confidence: 0.7,
293
+ },
294
+ };
@@ -0,0 +1,243 @@
1
+ /**
2
+ * Instructions technique fragments.
3
+ * Generated mechanically from the legacy techniques.js monolith during HR-09.
4
+ */
5
+
6
+ const {
7
+ path,
8
+ getClaudeHookContents,
9
+ } = require('./shared');
10
+
11
+ module.exports = {
12
+ claudeMd: {
13
+ id: 1,
14
+ name: 'CLAUDE.md project instructions',
15
+ check: (ctx) => ctx.files.includes('CLAUDE.md') || ctx.files.includes('.claude/CLAUDE.md'),
16
+ impact: 'critical',
17
+ rating: 5,
18
+ category: 'memory',
19
+ fix: 'Create CLAUDE.md with project-specific instructions, build commands, and coding conventions.',
20
+ template: 'claude-md'
21
+ },
22
+
23
+ mermaidArchitecture: {
24
+ id: 51,
25
+ name: 'Mermaid architecture diagram',
26
+ check: (ctx) => {
27
+ const md = ctx.claudeMdContent() || '';
28
+ return md.includes('mermaid') || md.includes('graph ') || md.includes('flowchart ');
29
+ },
30
+ impact: 'high',
31
+ rating: 5,
32
+ category: 'memory',
33
+ fix: 'Add a Mermaid diagram to CLAUDE.md showing project architecture. Saves 73% tokens vs prose.',
34
+ template: 'mermaid'
35
+ },
36
+
37
+ pathRules: {
38
+ id: 3,
39
+ name: 'Path-specific rules',
40
+ check: (ctx) => ctx.hasDir('.claude/rules') && ctx.dirFiles('.claude/rules').length > 0,
41
+ impact: 'medium',
42
+ rating: 4,
43
+ category: 'memory',
44
+ fix: 'Add rules for different file types (frontend vs backend conventions).',
45
+ template: 'rules'
46
+ },
47
+
48
+ importSyntax: {
49
+ id: 763,
50
+ name: 'CLAUDE.md uses @path imports for modularity',
51
+ check: (ctx) => {
52
+ const md = ctx.claudeMdContent() || '';
53
+ // Current syntax is @path/to/file (no "import" keyword)
54
+ return /@\S+\.(md|txt|json|yml|yaml|toml)/i.test(md) || /@\w+\//.test(md);
55
+ },
56
+ impact: 'medium',
57
+ rating: 4,
58
+ category: 'memory',
59
+ fix: 'Use @path syntax in CLAUDE.md to split instructions into focused modules (e.g. @docs/coding-style.md). You can also use .claude/rules/ for path-specific rules.',
60
+ template: null
61
+ },
62
+
63
+ underlines200: {
64
+ id: 681,
65
+ name: 'CLAUDE.md under 200 lines (concise)',
66
+ check: (ctx) => {
67
+ const md = ctx.claudeMdContent() || '';
68
+ return md.split('\n').length <= 200;
69
+ },
70
+ impact: 'medium',
71
+ rating: 4,
72
+ category: 'memory',
73
+ fix: 'Keep CLAUDE.md under 200 lines. Use @import or .claude/rules/ to split large instructions.',
74
+ template: null
75
+ },
76
+
77
+ xmlTags: {
78
+ id: 96,
79
+ name: 'XML tags for structured prompts',
80
+ check: (ctx) => {
81
+ const md = ctx.claudeMdContent() || '';
82
+ // Give credit for XML tags OR well-structured markdown with clear sections
83
+ const hasXml = md.includes('<constraints') || md.includes('<rules') ||
84
+ md.includes('<validation') || md.includes('<instructions');
85
+ const hasStructuredMd = (md.includes('## Rules') || md.includes('## Constraints') ||
86
+ md.includes('## Do not') || md.includes('## Never') || md.includes('## Important')) &&
87
+ md.split('\n').length > 20;
88
+ return hasXml || hasStructuredMd;
89
+ },
90
+ impact: 'medium',
91
+ rating: 4,
92
+ category: 'prompting',
93
+ fix: 'Add clear rules sections to CLAUDE.md. XML tags (<constraints>) are optional but improve clarity.',
94
+ template: null
95
+ },
96
+
97
+ fewShotExamples: {
98
+ id: 9,
99
+ name: 'CLAUDE.md contains code examples',
100
+ check: (ctx) => {
101
+ const md = ctx.claudeMdContent() || '';
102
+ return (md.match(/```/g) || []).length >= 2;
103
+ },
104
+ impact: 'high',
105
+ rating: 5,
106
+ category: 'prompting',
107
+ fix: 'Add code examples (few-shot) in CLAUDE.md to show preferred patterns and conventions.',
108
+ template: null
109
+ },
110
+
111
+ roleDefinition: {
112
+ id: 10,
113
+ name: 'CLAUDE.md defines a role or persona',
114
+ check: (ctx) => {
115
+ const md = ctx.claudeMdContent() || '';
116
+ return /^you are a |^your role is|^act as a |persona:|behave as a /im.test(md);
117
+ },
118
+ impact: 'medium',
119
+ rating: 4,
120
+ category: 'prompting',
121
+ fix: 'Define a role or persona in CLAUDE.md (e.g. "You are a senior backend engineer...").',
122
+ template: null
123
+ },
124
+
125
+ constraintBlocks: {
126
+ id: 9601,
127
+ name: 'XML constraint blocks in CLAUDE.md',
128
+ check: (ctx) => {
129
+ const md = ctx.claudeMdContent() || '';
130
+ return /<constraints|<rules|<requirements|<boundaries/i.test(md);
131
+ },
132
+ impact: 'high',
133
+ rating: 5,
134
+ category: 'prompting',
135
+ fix: 'Wrap critical rules in <constraints> XML blocks for 40% better adherence.',
136
+ template: null
137
+ },
138
+
139
+ claudeLocalMd: {
140
+ id: 2002,
141
+ name: 'CLAUDE.local.md for personal overrides',
142
+ check: (ctx) => {
143
+ // CLAUDE.local.md is for personal, non-committed overrides
144
+ return ctx.files.includes('CLAUDE.local.md') || ctx.files.includes('.claude/CLAUDE.local.md');
145
+ },
146
+ impact: 'low',
147
+ rating: 2,
148
+ category: 'memory',
149
+ fix: 'Create CLAUDE.local.md for personal preferences that should not be committed (add to .gitignore).',
150
+ template: null
151
+ },
152
+
153
+ autoMemoryAwareness: {
154
+ id: 2012,
155
+ name: 'Auto-memory or memory management mentioned',
156
+ check: (ctx) => {
157
+ const md = ctx.claudeMdContent() || '';
158
+ return /auto.?memory|memory.*manage|remember|persistent.*context/i.test(md);
159
+ },
160
+ impact: 'low', rating: 3, category: 'memory',
161
+ fix: 'Claude Code supports auto-memory for cross-session learning. Mention your memory strategy if relevant.',
162
+ template: null
163
+ },
164
+
165
+ negativeInstructions: {
166
+ id: 2019,
167
+ name: 'CLAUDE.md includes "do not" instructions',
168
+ check: (ctx) => {
169
+ const md = ctx.claudeMdContent() || '';
170
+ return /do not|don't|never|avoid|must not/i.test(md);
171
+ },
172
+ impact: 'medium', rating: 4, category: 'prompting',
173
+ fix: 'Add explicit "do not" rules to CLAUDE.md. Negative constraints reduce common mistakes.',
174
+ template: null
175
+ },
176
+
177
+ outputStyleGuidance: {
178
+ id: 2020,
179
+ name: 'CLAUDE.md includes output or style guidance',
180
+ check: (ctx) => {
181
+ const md = ctx.claudeMdContent() || '';
182
+ return /coding style|naming convention|code style|style guide|formatting rules|\bprefer\b.*\b(single|double|tabs|spaces|camel|snake|kebab|named|default|const|let|arrow|function)\b/i.test(md);
183
+ },
184
+ impact: 'medium', rating: 3, category: 'prompting',
185
+ fix: 'Add coding style and naming conventions to CLAUDE.md so Claude matches your project patterns.',
186
+ template: null
187
+ },
188
+
189
+ projectDescriptionInClaudeMd: {
190
+ id: 2022,
191
+ name: 'CLAUDE.md describes what the project does',
192
+ check: (ctx) => {
193
+ const md = ctx.claudeMdContent() || '';
194
+ return /what.*does|overview|purpose|about|description|project.*is/i.test(md) && md.length > 100;
195
+ },
196
+ impact: 'high', rating: 4, category: 'memory',
197
+ fix: 'Start CLAUDE.md with a clear project description. Claude needs to know what your project does.',
198
+ template: null
199
+ },
200
+
201
+ directoryStructureInClaudeMd: {
202
+ id: 2023,
203
+ name: 'CLAUDE.md documents directory structure',
204
+ check: (ctx) => {
205
+ const md = ctx.claudeMdContent() || '';
206
+ return /src\/|app\/|lib\/|structure|director|folder/i.test(md);
207
+ },
208
+ impact: 'medium', rating: 4, category: 'memory',
209
+ fix: 'Document your directory structure in CLAUDE.md so Claude navigates your codebase efficiently.',
210
+ template: null
211
+ },
212
+
213
+ hookExitCodesDefined: {
214
+ id: 110003,
215
+ name: 'Hook scripts handle exit codes correctly',
216
+ check: (ctx) => {
217
+ const hookContents = getClaudeHookContents(ctx);
218
+ if (hookContents.length === 0) return null;
219
+ return hookContents.some(content => /process\.exit|exit\s+[012]|sys\.exit|return\s+[012]/i.test(content));
220
+ },
221
+ impact: 'low', rating: 3, category: 'governance',
222
+ fix: 'Hooks should use explicit exit codes: 0=success, 1=warning, 2=block. See Claude Code docs.',
223
+ template: null,
224
+ confidence: 0.7,
225
+ },
226
+
227
+ loopSafetyBoundaries: {
228
+ id: 110004,
229
+ name: 'Loop safety boundaries configured',
230
+ check: (ctx) => {
231
+ const md = ctx.claudeMdContent() || '';
232
+ const settings = ctx.fileContent('.claude/settings.json') || '';
233
+ const hookContents = getClaudeHookContents(ctx).join('\n');
234
+ const loopSafetyConfig = [md, settings, hookContents].filter(Boolean).join('\n');
235
+
236
+ return /max[-_ ]?turns|maxTurns|max[-_ ]?tokens|maxTokens|loop(?:[-_ ]?(?:limit|limits|safety|guard|budget|boundary|boundaries))|iteration(?:[-_ ]?(?:limit|limits|guard|budget|cap|caps|count|max(?:imum)?))/i.test(loopSafetyConfig);
237
+ },
238
+ impact: 'medium', rating: 4, category: 'governance',
239
+ fix: 'Document loop safety limits such as maxTurns, maxTokens, or iteration caps in CLAUDE.md, settings, or hook guards.',
240
+ template: null,
241
+ confidence: 0.8,
242
+ },
243
+ };