claude-dev-env 1.57.2 → 1.59.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.
- package/CLAUDE.md +2 -2
- package/_shared/pr-loop/scripts/code_rules_gate.py +36 -3
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +6 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/reviews_disabled_constants.py +1 -0
- package/_shared/pr-loop/scripts/reviews_disabled.py +12 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +265 -0
- package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +29 -0
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +1 -1
- package/bin/install.mjs +317 -54
- package/bin/install.test.mjs +478 -3
- package/docs/CODE_RULES.md +3 -3
- package/hooks/blocking/code_rules_annotations_length.py +153 -0
- package/hooks/blocking/code_rules_dead_dataclass_field.py +319 -0
- package/hooks/blocking/code_rules_duplicate_body.py +287 -0
- package/hooks/blocking/code_rules_enforcer.py +175 -21
- package/hooks/blocking/code_rules_magic_values.py +98 -0
- package/hooks/blocking/code_rules_shared.py +41 -0
- package/hooks/blocking/destructive_command_blocker.py +1027 -12
- package/hooks/blocking/hook_prose_detector_consistency.py +150 -0
- package/hooks/blocking/intent_only_ending_blocker.py +155 -0
- package/hooks/blocking/session_handoff_blocker.py +190 -0
- package/hooks/blocking/subprocess_budget_completeness.py +380 -0
- package/hooks/blocking/test_code_rules_enforcer_annotations.py +225 -0
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +1 -0
- package/hooks/blocking/test_code_rules_enforcer_dead_dataclass_field.py +467 -0
- package/hooks/blocking/test_code_rules_enforcer_duplicate_body.py +330 -0
- package/hooks/blocking/test_code_rules_enforcer_duplicate_body_hook_routing.py +179 -0
- package/hooks/blocking/test_code_rules_enforcer_magic_slice_bounds.py +133 -0
- package/hooks/blocking/test_destructive_command_blocker.py +622 -3
- package/hooks/blocking/test_hook_prose_detector_consistency.py +265 -0
- package/hooks/blocking/test_intent_only_ending_blocker.py +175 -0
- package/hooks/blocking/test_session_handoff_blocker.py +312 -0
- package/hooks/blocking/test_subprocess_budget_completeness.py +588 -0
- package/hooks/blocking/test_workflow_substitution_slot_blocker.py +242 -0
- package/hooks/blocking/workflow_substitution_slot_blocker.py +159 -0
- package/hooks/hooks.json +25 -0
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +16 -0
- package/hooks/hooks_constants/dead_dataclass_field_constants.py +25 -0
- package/hooks/hooks_constants/destructive_command_segment_constants.py +178 -0
- package/hooks/hooks_constants/duplicate_function_body_constants.py +17 -0
- package/hooks/hooks_constants/hook_prose_detector_consistency_constants.py +30 -0
- package/hooks/hooks_constants/messages.py +4 -0
- package/hooks/hooks_constants/session_handoff_blocker_constants.py +10 -0
- package/hooks/hooks_constants/subprocess_budget_completeness_constants.py +5 -0
- package/hooks/hooks_constants/workflow_substitution_slot_blocker_constants.py +22 -0
- package/hooks/workflow/auto_formatter.py +26 -1
- package/hooks/workflow/test_auto_formatter.py +134 -0
- package/package.json +1 -1
- package/rules/conservative-action.md +1 -0
- package/rules/docstring-prose-matches-implementation.md +43 -0
- package/rules/hook-prose-matches-detector.md +26 -0
- package/rules/long-horizon-autonomy.md +43 -0
- package/rules/no-inline-destructive-literals.md +11 -0
- package/rules/workflow-substitution-slots.md +7 -0
- package/skills/autoconverge/SKILL.md +68 -6
- package/skills/autoconverge/reference/closing-report.md +44 -0
- package/skills/autoconverge/reference/convergence.md +7 -3
- package/skills/autoconverge/reference/stop-conditions.md +7 -2
- package/skills/autoconverge/workflow/autoconverge_report_constants/__init__.py +0 -0
- package/skills/autoconverge/workflow/autoconverge_report_constants/render_report_constants.py +105 -0
- package/skills/autoconverge/workflow/converge.contract.test.mjs +30 -1
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +265 -0
- package/skills/autoconverge/workflow/converge.mjs +106 -38
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a11d903476b803493.jsonl +2 -0
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a26213978adeef6fb.jsonl +2 -0
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a3def0d15ed9d9110.jsonl +2 -0
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a41f41b1b708ee3b7.jsonl +2 -0
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a758b880abecc3ff7.jsonl +2 -0
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a8897b89656b1bd16.jsonl +2 -0
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-abd463d744a1437bc.jsonl +2 -0
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ad19d027ae8ee1816.jsonl +2 -0
- package/skills/autoconverge/workflow/fixtures/wf_run/workflows/wf_881252e6-700.json +259 -0
- package/skills/autoconverge/workflow/render_report.py +903 -0
- package/skills/autoconverge/workflow/test_render_report.py +484 -0
- package/skills/pr-converge/scripts/check_convergence.py +195 -64
- package/skills/pr-converge/scripts/test_check_convergence.py +173 -2
- package/skills/update/SKILL.md +37 -5
package/bin/install.test.mjs
CHANGED
|
@@ -1,11 +1,24 @@
|
|
|
1
1
|
import { test } from 'node:test';
|
|
2
2
|
import { strict as assert } from 'node:assert';
|
|
3
3
|
import { execFileSync } from 'node:child_process';
|
|
4
|
-
import { mkdtempSync, rmSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { mkdtempSync, rmSync, mkdirSync, writeFileSync, symlinkSync } from 'node:fs';
|
|
5
5
|
import { tmpdir } from 'node:os';
|
|
6
6
|
import { join } from 'node:path';
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
import { pathToFileURL } from 'node:url';
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
collectPackageSourceConflicts,
|
|
11
|
+
CONTENT_DIRECTORIES,
|
|
12
|
+
pythonCandidatesForPlatform,
|
|
13
|
+
isWindowsStorePythonStub,
|
|
14
|
+
interpreterCommandFromPath,
|
|
15
|
+
invokedAsEntryPoint,
|
|
16
|
+
managedHookScriptRelativePaths,
|
|
17
|
+
managedHookScriptRelativePathsFromSourceRoots,
|
|
18
|
+
commandReferencesManagedHook,
|
|
19
|
+
mergeHooksIntoSettings,
|
|
20
|
+
pruneManagedHooksFromSettings,
|
|
21
|
+
} from './install.mjs';
|
|
9
22
|
|
|
10
23
|
|
|
11
24
|
function createTemporaryGitRepository() {
|
|
@@ -172,3 +185,465 @@ test('collectPackageSourceConflicts surfaces both-added and deleted-by-them entr
|
|
|
172
185
|
rmSync(repositoryRoot, { recursive: true, force: true });
|
|
173
186
|
}
|
|
174
187
|
});
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
test('pythonCandidatesForPlatform prefers py -3 ahead of python on win32 so the Microsoft Store stub is never probed first', () => {
|
|
191
|
+
const commands = pythonCandidatesForPlatform('win32').map(candidate => candidate.command);
|
|
192
|
+
assert.equal(commands[0], 'py -3');
|
|
193
|
+
assert.ok(commands.indexOf('py -3') < commands.indexOf('python'));
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
test('pythonCandidatesForPlatform keeps python3 first on non-Windows platforms', () => {
|
|
198
|
+
const commands = pythonCandidatesForPlatform('linux').map(candidate => candidate.command);
|
|
199
|
+
assert.equal(commands[0], 'python3');
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
test('pythonCandidatesForPlatform still offers python as a win32 fallback when py -3 and python3 are absent', () => {
|
|
204
|
+
const commands = pythonCandidatesForPlatform('win32').map(candidate => candidate.command);
|
|
205
|
+
assert.ok(commands.includes('python'));
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
test('isWindowsStorePythonStub flags the Microsoft Store WindowsApps alias paths', () => {
|
|
210
|
+
assert.equal(
|
|
211
|
+
isWindowsStorePythonStub('C:\\Program Files\\WindowsApps\\PythonSoftwareFoundation.Python.3.13_3.13.3824.0_x64__qbz5n2kfra8p0\\python3.13.exe'),
|
|
212
|
+
true,
|
|
213
|
+
);
|
|
214
|
+
assert.equal(
|
|
215
|
+
isWindowsStorePythonStub('C:/Users/jon/AppData/Local/Microsoft/WindowsApps/python.exe'),
|
|
216
|
+
true,
|
|
217
|
+
);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
test('isWindowsStorePythonStub does not flag a real interpreter install path', () => {
|
|
222
|
+
assert.equal(isWindowsStorePythonStub('C:\\Python313\\python.exe'), false);
|
|
223
|
+
assert.equal(isWindowsStorePythonStub('/usr/bin/python3'), false);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
test('interpreterCommandFromPath forward-slashes a Windows interpreter path and leaves a space-free path unquoted', () => {
|
|
228
|
+
assert.equal(interpreterCommandFromPath('C:\\Python313\\python.exe'), 'C:/Python313/python.exe');
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
test('interpreterCommandFromPath quotes an interpreter path that contains a space', () => {
|
|
233
|
+
assert.equal(
|
|
234
|
+
interpreterCommandFromPath('C:\\Program Files\\Python313\\python.exe'),
|
|
235
|
+
'"C:/Program Files/Python313/python.exe"',
|
|
236
|
+
);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
test('mergeHooksIntoSettings substitutes a quoted absolute interpreter path for the python3 prefix', () => {
|
|
241
|
+
const hooksConfig = {
|
|
242
|
+
hooks: {
|
|
243
|
+
PostToolUse: [
|
|
244
|
+
{
|
|
245
|
+
matcher: 'Edit',
|
|
246
|
+
hooks: [{ type: 'command', command: 'python3 ${CLAUDE_PLUGIN_ROOT}/hooks/workflow/auto_formatter.py' }],
|
|
247
|
+
},
|
|
248
|
+
],
|
|
249
|
+
},
|
|
250
|
+
};
|
|
251
|
+
const settings = {};
|
|
252
|
+
mergeHooksIntoSettings(settings, hooksConfig, 'C:/Users/x/.claude', '"C:/Program Files/Python313/python.exe"');
|
|
253
|
+
assert.equal(
|
|
254
|
+
settings.hooks.PostToolUse[0].hooks[0].command,
|
|
255
|
+
'"C:/Program Files/Python313/python.exe" C:/Users/x/.claude/hooks/workflow/auto_formatter.py',
|
|
256
|
+
);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
test('mergeHooksIntoSettings prunes a prior py -3 managed hook when reinstalling with an absolute interpreter path', () => {
|
|
261
|
+
const hooksConfig = {
|
|
262
|
+
hooks: {
|
|
263
|
+
PostToolUse: [
|
|
264
|
+
{
|
|
265
|
+
matcher: 'Edit',
|
|
266
|
+
hooks: [{ type: 'command', command: 'python3 ${CLAUDE_PLUGIN_ROOT}/hooks/workflow/auto_formatter.py' }],
|
|
267
|
+
},
|
|
268
|
+
],
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
const settings = {
|
|
272
|
+
hooks: {
|
|
273
|
+
PostToolUse: [
|
|
274
|
+
{
|
|
275
|
+
matcher: 'Edit',
|
|
276
|
+
hooks: [{ type: 'command', command: 'py -3 C:/Users/x/.claude/hooks/workflow/auto_formatter.py' }],
|
|
277
|
+
},
|
|
278
|
+
],
|
|
279
|
+
},
|
|
280
|
+
};
|
|
281
|
+
mergeHooksIntoSettings(settings, hooksConfig, 'C:/Users/x/.claude', 'C:/Python313/python.exe');
|
|
282
|
+
assert.deepEqual(
|
|
283
|
+
settings.hooks.PostToolUse[0].hooks.map(hook => hook.command),
|
|
284
|
+
['C:/Python313/python.exe C:/Users/x/.claude/hooks/workflow/auto_formatter.py'],
|
|
285
|
+
);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
test('invokedAsEntryPoint is true when the module url matches the invoked script path', () => {
|
|
290
|
+
const scriptPath = process.platform === 'win32' ? 'C:\\pkg\\bin\\install.mjs' : '/pkg/bin/install.mjs';
|
|
291
|
+
assert.equal(invokedAsEntryPoint(pathToFileURL(scriptPath).href, scriptPath), true);
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
test('invokedAsEntryPoint is false when the module is imported by another script', () => {
|
|
296
|
+
const modulePath = process.platform === 'win32' ? 'C:\\pkg\\bin\\install.mjs' : '/pkg/bin/install.mjs';
|
|
297
|
+
const entryScriptPath = process.platform === 'win32' ? 'C:\\pkg\\bin\\install.test.mjs' : '/pkg/bin/install.test.mjs';
|
|
298
|
+
assert.equal(invokedAsEntryPoint(pathToFileURL(modulePath).href, entryScriptPath), false);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
test('invokedAsEntryPoint is false when there is no invoked script path', () => {
|
|
303
|
+
assert.equal(invokedAsEntryPoint('file:///pkg/bin/install.mjs', undefined), false);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
test('invokedAsEntryPoint is true when the module is reached through a bin symlink', () => {
|
|
308
|
+
const linkRoot = mkdtempSync(join(tmpdir(), 'cdev-bin-symlink-'));
|
|
309
|
+
try {
|
|
310
|
+
const realModulePath = join(linkRoot, 'install.mjs');
|
|
311
|
+
const symlinkLauncherPath = join(linkRoot, 'claude-dev-env');
|
|
312
|
+
writeFileSync(realModulePath, 'export const sentinel = true;\n');
|
|
313
|
+
symlinkSync(realModulePath, symlinkLauncherPath);
|
|
314
|
+
const realModuleUrl = pathToFileURL(realModulePath).href;
|
|
315
|
+
assert.equal(invokedAsEntryPoint(realModuleUrl, symlinkLauncherPath), true);
|
|
316
|
+
} finally {
|
|
317
|
+
rmSync(linkRoot, { recursive: true, force: true });
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
test('invokedAsEntryPoint is false when a sibling script imports the real module', () => {
|
|
323
|
+
const importerRoot = mkdtempSync(join(tmpdir(), 'cdev-bin-importer-'));
|
|
324
|
+
try {
|
|
325
|
+
const realModulePath = join(importerRoot, 'install.mjs');
|
|
326
|
+
const importerScriptPath = join(importerRoot, 'install.test.mjs');
|
|
327
|
+
writeFileSync(realModulePath, 'export const sentinel = true;\n');
|
|
328
|
+
writeFileSync(importerScriptPath, 'import "./install.mjs";\n');
|
|
329
|
+
const realModuleUrl = pathToFileURL(realModulePath).href;
|
|
330
|
+
assert.equal(invokedAsEntryPoint(realModuleUrl, importerScriptPath), false);
|
|
331
|
+
} finally {
|
|
332
|
+
rmSync(importerRoot, { recursive: true, force: true });
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
const SAMPLE_HOOKS_CONFIG = {
|
|
338
|
+
hooks: {
|
|
339
|
+
Stop: [
|
|
340
|
+
{
|
|
341
|
+
matcher: '',
|
|
342
|
+
hooks: [
|
|
343
|
+
{ command: 'python3 ${CLAUDE_PLUGIN_ROOT}/hooks/notification/attention_needed_notify.py', timeout: 15 },
|
|
344
|
+
{ command: 'python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/hedging_language_blocker.py', timeout: 10 },
|
|
345
|
+
],
|
|
346
|
+
},
|
|
347
|
+
],
|
|
348
|
+
PreToolUse: [
|
|
349
|
+
{
|
|
350
|
+
matcher: 'Write',
|
|
351
|
+
hooks: [
|
|
352
|
+
{ command: 'python3 -c "import sys; sys.path.insert(0, r\'${CLAUDE_PLUGIN_ROOT}/hooks\'); print(1)"', timeout: 5 },
|
|
353
|
+
],
|
|
354
|
+
},
|
|
355
|
+
],
|
|
356
|
+
},
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
test('managedHookScriptRelativePaths collects every installed hook script path and ignores inline -c commands', () => {
|
|
361
|
+
const relativePaths = managedHookScriptRelativePaths(SAMPLE_HOOKS_CONFIG);
|
|
362
|
+
assert.ok(relativePaths.has('notification/attention_needed_notify.py'));
|
|
363
|
+
assert.ok(relativePaths.has('blocking/hedging_language_blocker.py'));
|
|
364
|
+
assert.equal([...relativePaths].length, 2);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
test('commandReferencesManagedHook matches managed scripts written with $HOME, ~, ${HOME}, and absolute path styles', () => {
|
|
369
|
+
const managedPaths = new Set(['notification/attention_needed_notify.py']);
|
|
370
|
+
assert.ok(commandReferencesManagedHook('python $HOME/.claude/hooks/notification/attention_needed_notify.py', managedPaths));
|
|
371
|
+
assert.ok(commandReferencesManagedHook('python ~/.claude/hooks/notification/attention_needed_notify.py', managedPaths));
|
|
372
|
+
assert.ok(commandReferencesManagedHook('python ${HOME}/.claude/hooks/notification/attention_needed_notify.py', managedPaths));
|
|
373
|
+
assert.ok(commandReferencesManagedHook('py -3 C:/Users/jonlo/.claude/hooks/notification/attention_needed_notify.py', managedPaths));
|
|
374
|
+
assert.ok(commandReferencesManagedHook('python /Users/jon/.claude/hooks/notification/attention_needed_notify.py', managedPaths));
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
test('commandReferencesManagedHook matches Windows backslash paths', () => {
|
|
379
|
+
const managedPaths = new Set(['blocking/hedging_language_blocker.py']);
|
|
380
|
+
assert.ok(commandReferencesManagedHook('py -3 C:\\Users\\jonlo\\.claude\\hooks\\blocking\\hedging_language_blocker.py', managedPaths));
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
test('commandReferencesManagedHook leaves user hooks outside the managed set untouched', () => {
|
|
385
|
+
const managedPaths = new Set(['notification/attention_needed_notify.py']);
|
|
386
|
+
assert.equal(commandReferencesManagedHook('python /home/me/custom-tools/my_own_hook.py', managedPaths), false);
|
|
387
|
+
assert.equal(commandReferencesManagedHook('py -3 ~/.claude/hooks/blocking/some_unmanaged_user_hook.py', managedPaths), false);
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
test('commandReferencesManagedHook leaves a user hook whose path is a managed tail plus a suffix untouched', () => {
|
|
392
|
+
const managedPaths = new Set(['blocking/code_rules_enforcer.py']);
|
|
393
|
+
assert.equal(commandReferencesManagedHook('python ~/.claude/hooks/blocking/code_rules_enforcer.py.bak', managedPaths), false);
|
|
394
|
+
assert.equal(commandReferencesManagedHook('python ~/.claude/hooks/blocking/code_rules_enforcer.py2', managedPaths), false);
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
test('commandReferencesManagedHook leaves a command whose managed tail is mid-path untouched', () => {
|
|
399
|
+
const managedPaths = new Set(['blocking/a.py']);
|
|
400
|
+
assert.equal(commandReferencesManagedHook('python /x/.claude/hooks/blocking/a.py/extra/thing.py', managedPaths), false);
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
test('commandReferencesManagedHook matches a managed script followed by a whitespace-separated argument', () => {
|
|
405
|
+
const managedPaths = new Set(['blocking/code_rules_enforcer.py']);
|
|
406
|
+
assert.ok(commandReferencesManagedHook('python ~/.claude/hooks/blocking/code_rules_enforcer.py PreToolUse', managedPaths));
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
test('commandReferencesManagedHook matches the rewritten inline validators-runner hook that carries no script tail', () => {
|
|
411
|
+
const managedPaths = new Set(['blocking/code_rules_enforcer.py']);
|
|
412
|
+
const rewrittenInlineCommand =
|
|
413
|
+
"py -3 -c \"import sys; sys.path.insert(0, r'C:/Users/jonlo/.claude/hooks'); from validators.run_all_validators import main; sys.exit(main())\"";
|
|
414
|
+
assert.ok(commandReferencesManagedHook(rewrittenInlineCommand, managedPaths));
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
test('commandReferencesManagedHook leaves an unmanaged inline -c command that imports a different module untouched', () => {
|
|
419
|
+
const managedPaths = new Set(['blocking/code_rules_enforcer.py']);
|
|
420
|
+
const userInlineCommand =
|
|
421
|
+
"python -c \"import sys; sys.path.insert(0, r'/home/me/tools'); from my_tools.runner import main; sys.exit(main())\"";
|
|
422
|
+
assert.equal(commandReferencesManagedHook(userInlineCommand, managedPaths), false);
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
function countManagedRunAllValidatorsHooks(settings) {
|
|
427
|
+
const writeEditGroups = (settings.hooks.PreToolUse || []).filter(
|
|
428
|
+
group => group.matcher === 'Write|Edit'
|
|
429
|
+
);
|
|
430
|
+
let runAllValidatorsCount = 0;
|
|
431
|
+
for (const group of writeEditGroups) {
|
|
432
|
+
for (const hook of group.hooks) {
|
|
433
|
+
if (hook.command.includes('run_all_validators')) {
|
|
434
|
+
runAllValidatorsCount++;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
return runAllValidatorsCount;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
test('mergeHooksIntoSettings is idempotent for the inline -c validators hook across two installs', () => {
|
|
443
|
+
const hooksConfig = {
|
|
444
|
+
hooks: {
|
|
445
|
+
'PreToolUse': [
|
|
446
|
+
{
|
|
447
|
+
matcher: 'Write|Edit',
|
|
448
|
+
hooks: [
|
|
449
|
+
{ command: 'python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/code_rules_enforcer.py', timeout: 30 },
|
|
450
|
+
{
|
|
451
|
+
command:
|
|
452
|
+
'python3 -c "import sys; sys.path.insert(0, r\'${CLAUDE_PLUGIN_ROOT}/hooks\'); from validators.run_all_validators import main; sys.exit(main())"',
|
|
453
|
+
timeout: 15,
|
|
454
|
+
},
|
|
455
|
+
],
|
|
456
|
+
},
|
|
457
|
+
],
|
|
458
|
+
},
|
|
459
|
+
};
|
|
460
|
+
const settings = {};
|
|
461
|
+
const pluginRootDir = 'C:/Users/jonlo/.claude';
|
|
462
|
+
|
|
463
|
+
mergeHooksIntoSettings(settings, hooksConfig, pluginRootDir, 'py -3');
|
|
464
|
+
mergeHooksIntoSettings(settings, hooksConfig, pluginRootDir, 'py -3');
|
|
465
|
+
|
|
466
|
+
assert.equal(countManagedRunAllValidatorsHooks(settings), 1);
|
|
467
|
+
const writeEditGroup = settings.hooks.PreToolUse.find(group => group.matcher === 'Write|Edit');
|
|
468
|
+
assert.equal(writeEditGroup.hooks.length, 2);
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
test('mergeHooksIntoSettings preserves user hooks in a managed matcher group across re-merges', () => {
|
|
473
|
+
const hooksConfig = {
|
|
474
|
+
hooks: {
|
|
475
|
+
'PreToolUse': [
|
|
476
|
+
{
|
|
477
|
+
matcher: 'Write|Edit',
|
|
478
|
+
hooks: [
|
|
479
|
+
{
|
|
480
|
+
command:
|
|
481
|
+
'python3 -c "import sys; sys.path.insert(0, r\'${CLAUDE_PLUGIN_ROOT}/hooks\'); from validators.run_all_validators import main; sys.exit(main())"',
|
|
482
|
+
timeout: 15,
|
|
483
|
+
},
|
|
484
|
+
],
|
|
485
|
+
},
|
|
486
|
+
],
|
|
487
|
+
},
|
|
488
|
+
};
|
|
489
|
+
const userHookCommand = 'python /home/me/custom-tools/my_own_hook.py';
|
|
490
|
+
const settings = {
|
|
491
|
+
hooks: {
|
|
492
|
+
PreToolUse: [
|
|
493
|
+
{ matcher: 'Write|Edit', hooks: [{ command: userHookCommand, timeout: 5 }] },
|
|
494
|
+
],
|
|
495
|
+
},
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
mergeHooksIntoSettings(settings, hooksConfig, 'C:/Users/jonlo/.claude', 'py -3');
|
|
499
|
+
mergeHooksIntoSettings(settings, hooksConfig, 'C:/Users/jonlo/.claude', 'py -3');
|
|
500
|
+
|
|
501
|
+
const writeEditGroup = settings.hooks.PreToolUse.find(group => group.matcher === 'Write|Edit');
|
|
502
|
+
const userHookSurvivors = writeEditGroup.hooks.filter(hook => hook.command === userHookCommand);
|
|
503
|
+
assert.equal(userHookSurvivors.length, 1);
|
|
504
|
+
assert.equal(countManagedRunAllValidatorsHooks(settings), 1);
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
test('pruneManagedHooksFromSettings removes a managed hook command written with the ~ home-path style', () => {
|
|
508
|
+
const managedPaths = new Set(['blocking/code_rules_enforcer.py']);
|
|
509
|
+
const settings = {
|
|
510
|
+
hooks: {
|
|
511
|
+
PreToolUse: [
|
|
512
|
+
{
|
|
513
|
+
matcher: 'Write|Edit',
|
|
514
|
+
hooks: [
|
|
515
|
+
{ command: 'python ~/.claude/hooks/blocking/code_rules_enforcer.py', timeout: 30 },
|
|
516
|
+
],
|
|
517
|
+
},
|
|
518
|
+
],
|
|
519
|
+
},
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
pruneManagedHooksFromSettings(settings, managedPaths);
|
|
523
|
+
|
|
524
|
+
assert.equal(settings.hooks, undefined);
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
test('pruneManagedHooksFromSettings removes managed hooks in every home-path and separator style while keeping user hooks', () => {
|
|
529
|
+
const managedPaths = new Set(['notification/attention_needed_notify.py']);
|
|
530
|
+
const userHookCommand = 'python /home/me/custom-tools/my_own_hook.py';
|
|
531
|
+
const settings = {
|
|
532
|
+
hooks: {
|
|
533
|
+
Stop: [
|
|
534
|
+
{
|
|
535
|
+
matcher: '',
|
|
536
|
+
hooks: [
|
|
537
|
+
{ command: 'python $HOME/.claude/hooks/notification/attention_needed_notify.py', timeout: 15 },
|
|
538
|
+
{ command: 'python ${HOME}/.claude/hooks/notification/attention_needed_notify.py', timeout: 15 },
|
|
539
|
+
{ command: 'py -3 C:\\Users\\jonlo\\.claude\\hooks\\notification\\attention_needed_notify.py', timeout: 15 },
|
|
540
|
+
{ command: userHookCommand, timeout: 5 },
|
|
541
|
+
],
|
|
542
|
+
},
|
|
543
|
+
],
|
|
544
|
+
},
|
|
545
|
+
};
|
|
546
|
+
|
|
547
|
+
pruneManagedHooksFromSettings(settings, managedPaths);
|
|
548
|
+
|
|
549
|
+
const stopGroup = settings.hooks.Stop.find(group => group.matcher === '');
|
|
550
|
+
assert.equal(stopGroup.hooks.length, 1);
|
|
551
|
+
assert.equal(stopGroup.hooks[0].command, userHookCommand);
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
function writeHooksJsonAtRoot(sourceRoot, hooksConfig) {
|
|
555
|
+
mkdirSync(join(sourceRoot, 'hooks'), { recursive: true });
|
|
556
|
+
writeFileSync(join(sourceRoot, 'hooks', 'hooks.json'), JSON.stringify(hooksConfig));
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
test('managedHookScriptRelativePathsFromSourceRoots reads each root hooks.json so purge matches every installed script', () => {
|
|
561
|
+
const sourceRoot = mkdtempSync(join(tmpdir(), 'cdev-purge-set-'));
|
|
562
|
+
try {
|
|
563
|
+
writeHooksJsonAtRoot(sourceRoot, SAMPLE_HOOKS_CONFIG);
|
|
564
|
+
|
|
565
|
+
const relativePaths = managedHookScriptRelativePathsFromSourceRoots([sourceRoot]);
|
|
566
|
+
|
|
567
|
+
assert.ok(relativePaths.has('notification/attention_needed_notify.py'));
|
|
568
|
+
assert.ok(relativePaths.has('blocking/hedging_language_blocker.py'));
|
|
569
|
+
assert.equal([...relativePaths].length, 2);
|
|
570
|
+
} finally {
|
|
571
|
+
rmSync(sourceRoot, { recursive: true, force: true });
|
|
572
|
+
}
|
|
573
|
+
});
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
test('managedHookScriptRelativePathsFromSourceRoots unions managed scripts across multiple package roots', () => {
|
|
577
|
+
const builtinRoot = mkdtempSync(join(tmpdir(), 'cdev-purge-builtin-'));
|
|
578
|
+
const dependencyRoot = mkdtempSync(join(tmpdir(), 'cdev-purge-dependency-'));
|
|
579
|
+
try {
|
|
580
|
+
writeHooksJsonAtRoot(builtinRoot, {
|
|
581
|
+
hooks: { Stop: [{ matcher: '', hooks: [{ command: 'python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/code_rules_enforcer.py' }] }] },
|
|
582
|
+
});
|
|
583
|
+
writeHooksJsonAtRoot(dependencyRoot, {
|
|
584
|
+
hooks: { PreToolUse: [{ matcher: 'Bash', hooks: [{ command: 'python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/pwsh_enforcer.py' }] }] },
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
const relativePaths = managedHookScriptRelativePathsFromSourceRoots([builtinRoot, dependencyRoot]);
|
|
588
|
+
|
|
589
|
+
assert.ok(relativePaths.has('blocking/code_rules_enforcer.py'));
|
|
590
|
+
assert.ok(relativePaths.has('blocking/pwsh_enforcer.py'));
|
|
591
|
+
assert.equal([...relativePaths].length, 2);
|
|
592
|
+
} finally {
|
|
593
|
+
rmSync(builtinRoot, { recursive: true, force: true });
|
|
594
|
+
rmSync(dependencyRoot, { recursive: true, force: true });
|
|
595
|
+
}
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
|
|
599
|
+
test('managedHookScriptRelativePathsFromSourceRoots skips roots whose hooks.json is absent', () => {
|
|
600
|
+
const rootWithoutHooks = mkdtempSync(join(tmpdir(), 'cdev-purge-empty-'));
|
|
601
|
+
try {
|
|
602
|
+
const relativePaths = managedHookScriptRelativePathsFromSourceRoots([rootWithoutHooks]);
|
|
603
|
+
assert.equal([...relativePaths].length, 0);
|
|
604
|
+
} finally {
|
|
605
|
+
rmSync(rootWithoutHooks, { recursive: true, force: true });
|
|
606
|
+
}
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
test('purge set sourced from package hooks.json prunes standalone managed script hooks and keeps user hooks', () => {
|
|
611
|
+
const sourceRoot = mkdtempSync(join(tmpdir(), 'cdev-purge-prune-'));
|
|
612
|
+
try {
|
|
613
|
+
writeHooksJsonAtRoot(sourceRoot, {
|
|
614
|
+
hooks: {
|
|
615
|
+
PreToolUse: [
|
|
616
|
+
{
|
|
617
|
+
matcher: 'Write|Edit',
|
|
618
|
+
hooks: [
|
|
619
|
+
{ command: 'python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/code_rules_enforcer.py', timeout: 30 },
|
|
620
|
+
],
|
|
621
|
+
},
|
|
622
|
+
],
|
|
623
|
+
},
|
|
624
|
+
});
|
|
625
|
+
const userHookCommand = 'python /home/me/custom-tools/my_own_hook.py';
|
|
626
|
+
const settings = {
|
|
627
|
+
hooks: {
|
|
628
|
+
PreToolUse: [
|
|
629
|
+
{
|
|
630
|
+
matcher: 'Write|Edit',
|
|
631
|
+
hooks: [
|
|
632
|
+
{ command: 'py -3 C:\\Users\\jonlo\\.claude\\hooks\\blocking\\code_rules_enforcer.py', timeout: 30 },
|
|
633
|
+
{ command: userHookCommand, timeout: 5 },
|
|
634
|
+
],
|
|
635
|
+
},
|
|
636
|
+
],
|
|
637
|
+
},
|
|
638
|
+
};
|
|
639
|
+
|
|
640
|
+
const managedHookRelativePaths = managedHookScriptRelativePathsFromSourceRoots([sourceRoot]);
|
|
641
|
+
pruneManagedHooksFromSettings(settings, managedHookRelativePaths);
|
|
642
|
+
|
|
643
|
+
const writeEditGroup = settings.hooks.PreToolUse.find(group => group.matcher === 'Write|Edit');
|
|
644
|
+
assert.equal(writeEditGroup.hooks.length, 1);
|
|
645
|
+
assert.equal(writeEditGroup.hooks[0].command, userHookCommand);
|
|
646
|
+
} finally {
|
|
647
|
+
rmSync(sourceRoot, { recursive: true, force: true });
|
|
648
|
+
}
|
|
649
|
+
});
|
package/docs/CODE_RULES.md
CHANGED
|
@@ -23,9 +23,9 @@ Compact reference for agents. ⚡ marks rules enforced by `code_rules_enforcer.p
|
|
|
23
23
|
|
|
24
24
|
`code_rules_enforcer.py` blocks each of these at Write/Edit and explains the specific violation when it fires; exact patterns and exemption lists live in the hook:
|
|
25
25
|
|
|
26
|
-
no new comments · imports at top · logging format args (`log_*("...", arg)`) · no magic values in production bodies (0, 1, -1 exempt) · UPPER_SNAKE constants only in `config/` (exempt: `config/*`, `/migrations/`, workflow registries `/workflow/` + `_tab.py` + `/states.py` + `/modules.py`, test files) · no hardcoded user home paths · guarded `sys.path.insert` · no unused module-level imports · banned identifiers (`ctx`, `cfg`, `msg`, `btn`, `idx`, `cnt`, `tmp`, `elem`, `val`) · banned function prefixes (`handle_`, `process_`, `manage_`, `do_`) · no type escape hatches (`Any` import, `cast()`, inline `Any`) outside boundary files · no bare/broad `except` · no `Any` in signatures or class attributes · no stub bodies (`pass`/`...`/`raise NotImplementedError`) outside abstract/Protocol · TypedDict `_encode_*`/`_decode_*` companions in the same module · no test-mode branching in production (use dependency injection) · no thin wrapper modules · Google-style docstrings on public functions with `Args:` matching the signature · boolean names prefixed `is_`/`has_`/`should_`/`can_`/`was_`/`did_` (assignments AND bool-typed parameters) · must-check returns (`find_and_click`, `write_outcome`) assigned and checked
|
|
26
|
+
no new comments · imports at top · logging format args (`log_*("...", arg)`) · no magic values in production bodies (0, 1, -1 exempt) · UPPER_SNAKE constants only in `config/` (exempt: `config/*`, `/migrations/`, workflow registries `/workflow/` + `_tab.py` + `/states.py` + `/modules.py`, test files) · no hardcoded user home paths · guarded `sys.path.insert` · no unused module-level imports · banned identifiers (`ctx`, `cfg`, `msg`, `btn`, `idx`, `cnt`, `tmp`, `elem`, `val`) · banned function prefixes (`handle_`, `process_`, `manage_`, `do_`) · no type escape hatches (`Any` import, `cast()`, inline `Any`) outside boundary files · no bare/broad `except` · no `Any` in signatures or class attributes · no stub bodies (`pass`/`...`/`raise NotImplementedError`) outside abstract/Protocol · TypedDict `_encode_*`/`_decode_*` companions in the same module · no test-mode branching in production (use dependency injection) · no thin wrapper modules · Google-style docstrings on public functions with `Args:` matching the signature · boolean names prefixed `is_`/`has_`/`should_`/`can_`/`was_`/`did_` (assignments AND bool-typed parameters) · must-check returns (`find_and_click`, `write_outcome`) assigned and checked · known pytest fixture parameters in test files annotated with their single documented type (`tmp_path: Path`, `monkeypatch: pytest.MonkeyPatch`, `capsys`, `caplog`, `request`, …)
|
|
27
27
|
|
|
28
|
-
Test files are exempt from most checks. See also the file-global constants use-count rule: [`rules/file-global-constants.md`](../rules/file-global-constants.md).
|
|
28
|
+
Test files are exempt from most checks. The one annotation the test-file exemption does NOT cover is a known pytest builtin fixture parameter: `tmp_path`, `monkeypatch`, `capsys`, `capfd`, `caplog`, `request`, and `tmp_path_factory` each have a single documented injected type, so the gate requires that annotation (`tmp_path: Path`) even inside a test file. Ordinary test parameters stay exempt. See also the file-global constants use-count rule: [`rules/file-global-constants.md`](../rules/file-global-constants.md).
|
|
29
29
|
|
|
30
30
|
---
|
|
31
31
|
|
|
@@ -94,4 +94,4 @@ If you already have the data, don't fetch it again.
|
|
|
94
94
|
|
|
95
95
|
## 11. ENFORCEMENT SURFACES
|
|
96
96
|
|
|
97
|
-
⚡ **Hooks** block pattern-matchable violations at Write/Edit time. 🤖 **Prompt context** carries judgment principles (SRP, Right-Sized Engineering, conservative-action, BDD discovery; the `/code` skill prepends strict mode for a session: no `Any`/`cast()`, immutable TypedDicts with `_encode_*`/`_decode_*` + `require_*` validation, per-module `_test_hooks.py` DI, 100% statement + branch coverage, zero mocks). 👥 **Audit rubrics** (`/check`, `packages/claude-dev-env/audit-rubrics/` categories A–P) cover cross-file architectural concerns. Rules with documented-but-pending hook coverage live in `~/.claude/rules/*.md` and `skills/code/SKILL.md`; each names its own promotion path.
|
|
97
|
+
⚡ **Hooks** block pattern-matchable violations at Write/Edit time. 🤖 **Prompt context** carries judgment principles (SRP, Right-Sized Engineering, conservative-action, BDD discovery, docstring-prose-matches-implementation; the `/code` skill prepends strict mode for a session: no `Any`/`cast()`, immutable TypedDicts with `_encode_*`/`_decode_*` + `require_*` validation, per-module `_test_hooks.py` DI, 100% statement + branch coverage, zero mocks). 👥 **Audit rubrics** (`/check`, `packages/claude-dev-env/audit-rubrics/` categories A–P) cover cross-file architectural concerns. Rules with documented-but-pending hook coverage live in `~/.claude/rules/*.md` and `skills/code/SKILL.md`; each names its own promotion path. The docstring-prose standard (free-form enumerations match the body) lives in `packages/claude-dev-env/rules/docstring-prose-matches-implementation.md`, enforced via Category O6 audit.
|