claude-dev-env 2.18.0 → 2.20.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 (133) hide show
  1. package/.agents/skills/AGENTS.md +4 -4
  2. package/.agents/skills/_shared/AGENTS.md +1 -0
  3. package/.agents/skills/_shared/pr-loop/AGENTS.md +1 -0
  4. package/.agents/skills/_shared/pr-loop/preflight-proposal.contract.test.mjs +119 -0
  5. package/.agents/skills/_shared/pr-loop/preflight-proposal.md +5 -0
  6. package/.agents/skills/e-code-review/SKILL.md +12 -1
  7. package/.agents/skills/e-code-review/reference/fix.md +5 -1
  8. package/.agents/skills/e-code-review/reference/loop.md +4 -0
  9. package/.agents/skills/e-code-review/reference/mode-contract.test.mjs +66 -0
  10. package/.agents/skills/e-code-review/reference/preflight-proposal.md +40 -0
  11. package/.agents/skills/e-code-review/reference/runner-selection.md +1 -0
  12. package/.agents/skills/pr-cleanup/SKILL.md +105 -75
  13. package/.agents/skills/pr-cleanup/reference/process-inventory.md +15 -0
  14. package/.agents/skills/pr-cleanup/reference/task-seeds.md +20 -0
  15. package/.agents/skills/{name-by-capability-audit → pr-name-by-capability}/SKILL.md +18 -12
  16. package/.agents/skills/{name-by-capability-audit → pr-name-by-capability}/reference/offense-examples.md +1 -1
  17. package/.agents/skills/{name-by-capability-audit → pr-name-by-capability}/reference/task-seeds.md +3 -2
  18. package/.agents/skills/pr-refinement/SKILL.md +31 -0
  19. package/.agents/skills/{shared-extraction-audit → pr-shared-extraction}/SKILL.md +24 -7
  20. package/.agents/skills/{small-cl → pr-small-cl}/SKILL.md +1 -1
  21. package/.agents/skills/prototype/scripts/test_probe_sandbox_safety.py +33 -5
  22. package/.agents/skills/skill-builder/SKILL.md +1 -0
  23. package/.agents/skills-archived/bugteam/SKILL.md +1 -1
  24. package/.agents/skills-archived/bugteam/test_skill_additions.py +1 -1
  25. package/.agents/skills-archived/comments/SKILL.md +1 -1
  26. package/.agents/skills-archived/descriptions/SKILL.md +1 -1
  27. package/.agents/skills-archived/reviews/SKILL.md +1 -1
  28. package/AGENTS.md +1 -1
  29. package/_shared/pr-loop/AGENTS.md +2 -1
  30. package/_shared/pr-loop/preflight-proposal.md +56 -0
  31. package/_shared/pr-loop/scripts/code_rules_gate.py +29 -6
  32. package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_arguments.py +15 -3
  33. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_gate_running.py +47 -4
  34. package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +4 -0
  35. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +47 -0
  36. package/bin/ever-shipped-skills.mjs +14 -0
  37. package/bin/install-constants.mjs +5 -0
  38. package/bin/install.agents-home.test.mjs +23 -0
  39. package/bin/install.codex-rules.test.mjs +61 -6
  40. package/bin/install.mjs +57 -9
  41. package/bin/install.prune.test.mjs +65 -3
  42. package/bin/install.test.mjs +202 -2
  43. package/docs/CODE_RULES.md +6 -0
  44. package/hooks/advisory/conftest.py +10 -0
  45. package/hooks/advisory/migration_safety_advisor.py +14 -14
  46. package/hooks/advisory/refactor_guard.py +250 -144
  47. package/hooks/advisory/refactor_guard_test_support.py +46 -0
  48. package/hooks/advisory/test_migration_safety_advisor.py +120 -0
  49. package/hooks/advisory/test_refactor_guard_advisory.py +171 -0
  50. package/hooks/advisory/test_refactor_guard_eligibility.py +166 -0
  51. package/hooks/atomic_file_writer.py +81 -0
  52. package/hooks/blocking/AGENTS.md +2 -1
  53. package/hooks/blocking/block_main_commit.py +66 -33
  54. package/hooks/blocking/code_rules_banned_identifiers.py +55 -4
  55. package/hooks/blocking/code_rules_blast_radius.py +194 -0
  56. package/hooks/blocking/code_rules_enforcer.py +12 -0
  57. package/hooks/blocking/code_rules_enforcer_test_support.py +92 -0
  58. package/hooks/blocking/destructive_command_blocker.py +8 -7
  59. package/hooks/blocking/pre_tool_use_dispatcher.py +11 -7
  60. package/hooks/blocking/precommit_code_rules_gate.py +14 -139
  61. package/hooks/blocking/sensitive_file_protector.py +8 -6
  62. package/hooks/blocking/session_edit_stage_gate.py +31 -1
  63. package/hooks/blocking/test_block_main_commit.py +145 -0
  64. package/hooks/blocking/test_claude_md_orphan_file_blocker.py +171 -1
  65. package/hooks/blocking/test_code_rules_blast_radius.py +161 -0
  66. package/hooks/blocking/test_code_rules_enforcer_agent_home_tooling.py +37 -12
  67. package/hooks/blocking/test_code_rules_enforcer_banned_noun_word.py +54 -0
  68. package/hooks/blocking/test_code_rules_enforcer_duplicate_body_hook_routing.py +15 -53
  69. package/hooks/blocking/test_code_rules_enforcer_ephemeral.py +13 -62
  70. package/hooks/blocking/test_code_rules_enforcer_narrow_edit.py +309 -0
  71. package/hooks/blocking/test_code_rules_enforcer_precheck_forecast.py +14 -71
  72. package/hooks/blocking/test_code_rules_enforcer_scratchpad.py +14 -16
  73. package/hooks/blocking/test_code_rules_enforcer_split_entry_2.py +31 -46
  74. package/hooks/blocking/test_code_rules_enforcer_stage.py +192 -0
  75. package/hooks/blocking/test_code_rules_enforcer_zero_payload_alias_hook_routing.py +13 -59
  76. package/hooks/blocking/test_destructive_command_blocker.py +154 -138
  77. package/hooks/blocking/test_destructive_command_blocker_deny_mode.py +85 -25
  78. package/hooks/blocking/test_destructive_command_blocker_no_verify.py +58 -38
  79. package/hooks/blocking/test_destructive_command_blocker_patterns.py +133 -0
  80. package/hooks/blocking/test_docstring_rule_gate_count_blocker.py +113 -1
  81. package/hooks/blocking/test_env_var_table_code_drift_blocker.py +70 -3
  82. package/hooks/blocking/test_hook_subprocess_support.py +296 -0
  83. package/hooks/blocking/test_package_inventory_stale_blocker.py +1 -1
  84. package/hooks/blocking/test_pii_scanner.py +81 -0
  85. package/hooks/blocking/test_pre_tool_use_dispatcher.py +32 -3
  86. package/hooks/blocking/test_precommit_code_rules_gate.py +11 -16
  87. package/hooks/blocking/test_precommit_code_rules_gate_native_owner.py +234 -0
  88. package/hooks/blocking/test_pytest_testpaths_orphan_blocker.py +25 -18
  89. package/hooks/blocking/test_sensitive_file_protector.py +145 -5
  90. package/hooks/blocking/test_session_edit_stage_gate_staging.py +64 -0
  91. package/hooks/blocking/test_session_edit_stage_gate_support.py +204 -0
  92. package/hooks/blocking/test_session_edit_stage_gate_tracker.py +116 -0
  93. package/hooks/blocking/test_shared_stdin_adoption.py +31 -20
  94. package/hooks/blocking/test_test_preflight_check.py +80 -0
  95. package/hooks/git-hooks/AGENTS.md +1 -1
  96. package/hooks/git-hooks/git_hooks_constants/__init__.py +1 -0
  97. package/hooks/git-hooks/post_commit.py +160 -51
  98. package/hooks/git-hooks/pre_commit.py +9 -5
  99. package/hooks/git-hooks/test_post_commit.py +203 -0
  100. package/hooks/git-hooks/test_pre_commit.py +2 -2
  101. package/hooks/git-hooks/test_pre_push.py +57 -0
  102. package/hooks/hooks_constants/AGENTS.md +3 -1
  103. package/hooks/hooks_constants/atomic_file_writer_constants.py +4 -0
  104. package/hooks/hooks_constants/banned_identifiers_constants.py +1 -0
  105. package/hooks/hooks_constants/blast_radius_constants.py +14 -0
  106. package/hooks/hooks_constants/destructive_command_environment_constants.py +18 -0
  107. package/hooks/hooks_constants/destructive_command_segment_constants.py +1 -11
  108. package/hooks/hooks_constants/hardcoded_user_path_constants.py +9 -3
  109. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +1 -1
  110. package/hooks/hooks_constants/refactor_guard_constants.py +75 -0
  111. package/hooks/hooks_constants/sensitive_file_protector_constants.py +2 -4
  112. package/hooks/hooks_constants/test_refactor_guard_constants.py +21 -0
  113. package/hooks/json_file_reader.py +22 -0
  114. package/hooks/observability/test_instructions_loaded_logger.py +54 -0
  115. package/hooks/pending_sidecars.py +33 -0
  116. package/hooks/session/test_plugin_data_dir_cleanup.py +70 -0
  117. package/hooks/session/test_session_edit_tracker_cleanup.py +16 -3
  118. package/hooks/test_atomic_file_writer.py +73 -0
  119. package/hooks/test_json_file_reader.py +24 -0
  120. package/hooks/test_pending_sidecars.py +25 -0
  121. package/hooks/validation/mypy_validator.py +213 -80
  122. package/hooks/validation/test_mypy_validator.py +288 -13
  123. package/hooks/workflow/auto_formatter.py +225 -93
  124. package/hooks/workflow/investigation_tracker_reset.py +2 -0
  125. package/hooks/workflow/test_auto_formatter.py +261 -12
  126. package/hooks/workflow/test_investigation_tracker_reset.py +90 -0
  127. package/package.json +1 -1
  128. package/rules/failure-blast-radius.md +126 -0
  129. /package/.agents/skills/{name-by-capability-audit → pr-name-by-capability}/reference/fetch-commands.md +0 -0
  130. /package/.agents/skills/{name-by-capability-audit → pr-name-by-capability}/reference/report-template.md +0 -0
  131. /package/.agents/skills/{name-by-capability-audit → pr-name-by-capability}/reference/rule-checklist.md +0 -0
  132. /package/.agents/skills/{shared-extraction-audit → pr-shared-extraction}/reference/examples.md +0 -0
  133. /package/.agents/skills/{shared-extraction-audit → pr-shared-extraction}/reference/offense-taxonomy.md +0 -0
@@ -1,6 +1,6 @@
1
1
  import { test } from 'node:test';
2
2
  import { strict as assert } from 'node:assert';
3
- import { execFileSync } from 'node:child_process';
3
+ import { execFileSync, spawnSync } from 'node:child_process';
4
4
  import {
5
5
  mkdtempSync,
6
6
  rmSync,
@@ -27,6 +27,7 @@ import {
27
27
  pythonCandidatesForPlatform,
28
28
  isWindowsStorePythonStub,
29
29
  interpreterCommandFromPath,
30
+ commandArgumentFromPath,
30
31
  invokedAsEntryPoint,
31
32
  managedHookScriptRelativePaths,
32
33
  managedHookScriptRelativePathsFromSourceRoots,
@@ -504,7 +505,9 @@ test('mergeHooksIntoSettings inserts dollar characters in plugin root and interp
504
505
  const rewrittenCommand = settings.hooks.SessionStart[0].hooks[0].command;
505
506
  assert.equal(
506
507
  rewrittenCommand,
507
- 'C:/Users/$&evil$1/Python/python.exe C:/Users/$&evil$1/.claude/hooks/session/session_env_cleanup.py',
508
+ process.platform === 'win32'
509
+ ? 'C:/Users/$&evil$1/Python/python.exe "C:/Users/$&evil$1/.claude/hooks/session/session_env_cleanup.py"'
510
+ : "C:/Users/$&evil$1/Python/python.exe 'C:/Users/$&evil$1/.claude/hooks/session/session_env_cleanup.py'",
508
511
  );
509
512
  assert.equal(rewrittenCommand.includes('${CLAUDE_PLUGIN_ROOT}'), false);
510
513
  });
@@ -530,6 +533,191 @@ test('mergeHooksIntoSettings substitutes a quoted absolute interpreter path for
530
533
  });
531
534
 
532
535
 
536
+ test('mergeHooksIntoSettings quotes standalone plugin-root hook paths and preserves existing quotes', () => {
537
+ const hooksConfig = {
538
+ hooks: {
539
+ PreToolUse: [{
540
+ matcher: 'Write|Edit|MultiEdit',
541
+ hooks: [
542
+ { type: 'command', command: 'python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/pre_tool_use_dispatcher.py' },
543
+ { type: 'command', command: 'python3 "${CLAUDE_PLUGIN_ROOT}/hooks/blocking/pre_tool_use_dispatcher.py"' },
544
+ { type: 'command', command: 'python3 -c "import sys; sys.path.insert(0, r\'${CLAUDE_PLUGIN_ROOT}/hooks\')"' },
545
+ { type: 'command', command: 'python3 -c "import sys; sys.path.insert(0, r\"${CLAUDE_PLUGIN_ROOT}/hooks\")"' },
546
+ { type: 'command', command: "python3 -c \"print('${CLAUDE_PLUGIN_ROOT}/hooks/foo.py')\"" },
547
+ ],
548
+ }],
549
+ },
550
+ };
551
+ const settings = {};
552
+
553
+ mergeHooksIntoSettings(settings, hooksConfig, 'C:/Program Files/Claude/.claude', 'python3');
554
+
555
+ assert.deepEqual(
556
+ settings.hooks.PreToolUse[0].hooks.map(eachHook => eachHook.command),
557
+ [
558
+ process.platform === 'win32'
559
+ ? 'python3 "C:/Program Files/Claude/.claude/hooks/blocking/pre_tool_use_dispatcher.py"'
560
+ : "python3 'C:/Program Files/Claude/.claude/hooks/blocking/pre_tool_use_dispatcher.py'",
561
+ process.platform === 'win32'
562
+ ? 'python3 "C:/Program Files/Claude/.claude/hooks/blocking/pre_tool_use_dispatcher.py"'
563
+ : "python3 'C:/Program Files/Claude/.claude/hooks/blocking/pre_tool_use_dispatcher.py'",
564
+ "python3 -c \"import sys; sys.path.insert(0, (__import__('base64').b64decode('QzovUHJvZ3JhbSBGaWxlcy9DbGF1ZGUvLmNsYXVkZS9ob29rcw==').decode(), '__claude_dev_env_managed_hooks__')[0])\"",
565
+ "python3 -c \"import sys; sys.path.insert(0, (__import__('base64').b64decode('QzovUHJvZ3JhbSBGaWxlcy9DbGF1ZGUvLmNsYXVkZS9ob29rcw==').decode(), '__claude_dev_env_managed_hooks__')[0])\"",
566
+ "python3 -c \"print('C:/Program Files/Claude/.claude/hooks/foo.py')\"",
567
+ ],
568
+ );
569
+ });
570
+
571
+
572
+ test('mergeHooksIntoSettings embeds a decodable inline plugin-root path', () => {
573
+ const hooksConfig = {
574
+ hooks: {
575
+ SessionStart: [{
576
+ matcher: '',
577
+ hooks: [{
578
+ type: 'command',
579
+ command: 'python3 -c "import sys; sys.path.insert(0, r\'${CLAUDE_PLUGIN_ROOT}/hooks\'); print(sys.path[0])"',
580
+ }],
581
+ }],
582
+ },
583
+ };
584
+ const settings = {};
585
+ const pluginRootDirectory = 'C:/Program Files/Claude/.claude';
586
+
587
+ mergeHooksIntoSettings(settings, hooksConfig, pluginRootDirectory, 'python3');
588
+
589
+ const rewrittenCommand = settings.hooks.SessionStart[0].hooks[0].command;
590
+ const pythonCommand = process.platform === 'win32' ? 'python' : 'python3';
591
+ const inlineRun = spawnSync(rewrittenCommand.replace(/^python3\b/, pythonCommand), {
592
+ encoding: 'utf8',
593
+ shell: process.platform === 'win32' ? true : '/bin/sh',
594
+ timeout: 30000,
595
+ });
596
+ assert.equal(inlineRun.status, 0, inlineRun.stderr);
597
+ assert.equal(inlineRun.stdout.trim(), `${pluginRootDirectory}/hooks`);
598
+ assert.equal(rewrittenCommand.includes("r'${CLAUDE_PLUGIN_ROOT}/hooks'"), false);
599
+ });
600
+
601
+
602
+ test('commandArgumentFromPath returns raw shell-safe paths', () => {
603
+ assert.equal(
604
+ commandArgumentFromPath('/opt/claude/hooks/pre_tool_use_dispatcher.py', 'linux'),
605
+ '/opt/claude/hooks/pre_tool_use_dispatcher.py',
606
+ );
607
+ assert.equal(
608
+ commandArgumentFromPath('C:\\Python313\\python.exe', 'win32'),
609
+ 'C:/Python313/python.exe',
610
+ );
611
+ });
612
+
613
+
614
+ test('commandArgumentFromPath quotes POSIX shell metacharacters', () => {
615
+ assert.equal(
616
+ commandArgumentFromPath("/opt/Claude's $hooks `pre_tool_use_dispatcher`.py", 'linux'),
617
+ "'/opt/Claude'\\''s $hooks `pre_tool_use_dispatcher`.py'",
618
+ );
619
+ });
620
+
621
+
622
+ test('commandArgumentFromPath quotes Windows percent and shell metacharacters', () => {
623
+ assert.equal(
624
+ commandArgumentFromPath('C:\\Program Files\\%PATH% & (x) ! ^.py', 'win32'),
625
+ '"C:/Program Files/"^%"PATH"^%" & (x) ! ^.py"',
626
+ );
627
+ assert.equal(
628
+ commandArgumentFromPath('C:\\safe\\%PATH%.py', 'win32'),
629
+ '"C:/safe/"^%"PATH"^%".py"',
630
+ );
631
+ });
632
+
633
+
634
+ test('commandArgumentFromPath launches a script from a metacharacter path', () => {
635
+ const temporaryDirectory = mkdtempSync(join(tmpdir(), 'cdev command & (path)!-'));
636
+ const isWindows = process.platform === 'win32';
637
+ const scriptPath = join(
638
+ temporaryDirectory,
639
+ isWindows ? 'launcher script %PATH% & (x) !.py' : "launcher script & (x) '$'.py",
640
+ );
641
+ try {
642
+ writeFileSync(scriptPath, 'print("launched")\n');
643
+ const pythonCommand = isWindows ? 'python' : 'python3';
644
+ const shellCommand = `${pythonCommand} ${commandArgumentFromPath(scriptPath, process.platform)}`;
645
+ const launcherRun = spawnSync(shellCommand, {
646
+ encoding: 'utf8',
647
+ shell: isWindows ? true : '/bin/sh',
648
+ timeout: 30000,
649
+ });
650
+ assert.equal(launcherRun.error, undefined);
651
+ assert.equal(launcherRun.signal, null);
652
+ assert.equal(launcherRun.status, 0, launcherRun.stderr);
653
+ assert.equal(launcherRun.stdout.trim(), 'launched');
654
+ } finally {
655
+ rmSync(temporaryDirectory, { recursive: true, force: true });
656
+ }
657
+ });
658
+
659
+
660
+ test('commandArgumentFromPath launches a no-space percent path', () => {
661
+ const temporaryDirectory = mkdtempSync(join(tmpdir(), 'cdev-command-launch-'));
662
+ const isWindows = process.platform === 'win32';
663
+ const scriptName = isWindows ? 'launcher%PATH%.py' : 'launcher.py';
664
+ const scriptPath = join(temporaryDirectory, scriptName);
665
+ try {
666
+ writeFileSync(scriptPath, 'print("launched")\n');
667
+ const pythonCommand = isWindows ? 'python' : 'python3';
668
+ const shellCommand = `${pythonCommand} ${commandArgumentFromPath(scriptPath, process.platform)}`;
669
+ const launcherRun = spawnSync(shellCommand, {
670
+ encoding: 'utf8',
671
+ shell: isWindows ? true : '/bin/sh',
672
+ timeout: 30000,
673
+ });
674
+ assert.equal(launcherRun.error, undefined);
675
+ assert.equal(launcherRun.signal, null);
676
+ assert.equal(launcherRun.status, 0, launcherRun.stderr);
677
+ assert.equal(launcherRun.stdout.trim(), 'launched');
678
+ } finally {
679
+ rmSync(temporaryDirectory, { recursive: true, force: true });
680
+ }
681
+ });
682
+
683
+
684
+ test('mergeHooksIntoSettings matches full standalone arguments at boundaries', () => {
685
+ const hooksConfig = {
686
+ hooks: {
687
+ PreToolUse: [{
688
+ matcher: 'Write',
689
+ hooks: [
690
+ { type: 'command', command: 'python3 prefix${CLAUDE_PLUGIN_ROOT}/hooks/a.py' },
691
+ { type: 'command', command: 'python3 ${CLAUDE_PLUGIN_ROOT}/hooks/a.py.bak' },
692
+ { type: 'command', command: 'python3 ${CLAUDE_PLUGIN_ROOT}/hooks/a.py' },
693
+ { type: 'command', command: 'python3 ${CLAUDE_PLUGIN_ROOT}/hooks/a.py; next' },
694
+ { type: 'command', command: 'python3 ${CLAUDE_PLUGIN_ROOT}/hooks/a.py& next' },
695
+ { type: 'command', command: 'python3 ${CLAUDE_PLUGIN_ROOT}/hooks/a.py| next' },
696
+ ],
697
+ }],
698
+ },
699
+ };
700
+ const settings = {};
701
+
702
+ mergeHooksIntoSettings(settings, hooksConfig, 'C:/Program Files/Claude/.claude', 'python3');
703
+
704
+ const expectedQuotedHookPath = process.platform === 'win32'
705
+ ? '"C:/Program Files/Claude/.claude/hooks/a.py"'
706
+ : "'C:/Program Files/Claude/.claude/hooks/a.py'";
707
+ assert.deepEqual(
708
+ settings.hooks.PreToolUse[0].hooks.map(eachHook => eachHook.command),
709
+ [
710
+ 'python3 prefixC:/Program Files/Claude/.claude/hooks/a.py',
711
+ 'python3 C:/Program Files/Claude/.claude/hooks/a.py.bak',
712
+ `python3 ${expectedQuotedHookPath}`,
713
+ `python3 ${expectedQuotedHookPath}; next`,
714
+ `python3 ${expectedQuotedHookPath}& next`,
715
+ `python3 ${expectedQuotedHookPath}| next`,
716
+ ],
717
+ );
718
+ });
719
+
720
+
533
721
  test('mergeHooksIntoSettings prunes a prior py -3 managed hook when reinstalling with an absolute interpreter path', () => {
534
722
  const hooksConfig = {
535
723
  hooks: {
@@ -687,6 +875,12 @@ test('commandReferencesManagedHook matches the rewritten inline validators-runne
687
875
  const rewrittenInlineCommand =
688
876
  "py -3 -c \"import sys; sys.path.insert(0, r'C:/Users/example/.claude/hooks'); from validators.run_all_validators import main; sys.exit(main())\"";
689
877
  assert.ok(commandReferencesManagedHook(rewrittenInlineCommand, managedPaths));
878
+ const base64InlineCommand =
879
+ "py -3 -c \"import sys; sys.path.insert(0, (__import__('base64').b64decode('QzovVXNlcnMvZXhhbXBsZS8uY2xhdWRlL2hvb2tz').decode(), '__claude_dev_env_managed_hooks__')[0]); from validators.run_all_validators import main; sys.exit(main())\"";
880
+ assert.ok(commandReferencesManagedHook(base64InlineCommand, managedPaths));
881
+ const unmarkedBase64InlineCommand =
882
+ "py -3 -c \"import sys; sys.path.insert(0, __import__('base64').b64decode('QzovVXNlcnMvZXhhbXBsZS8uY2xhdWRlL2hvb2tz').decode()); from validators.run_all_validators import main; sys.exit(main())\"";
883
+ assert.equal(commandReferencesManagedHook(unmarkedBase64InlineCommand, managedPaths), false);
690
884
  });
691
885
 
692
886
 
@@ -695,6 +889,12 @@ test('commandReferencesManagedHook leaves an unmanaged inline -c command that im
695
889
  const userInlineCommand =
696
890
  "python -c \"import sys; sys.path.insert(0, r'/home/me/tools'); from my_tools.runner import main; sys.exit(main())\"";
697
891
  assert.equal(commandReferencesManagedHook(userInlineCommand, managedPaths), false);
892
+ const similarlyNamedUserInlineCommand =
893
+ "python -c \"import sys; sys.path.insert(0, r'/home/me/tools'); from mypkg.run_all_validators import main; sys.exit(main())\"";
894
+ assert.equal(commandReferencesManagedHook(similarlyNamedUserInlineCommand, managedPaths), false);
895
+ const userPathWithManagedImport =
896
+ "python -c \"import sys; sys.path.insert(0, r'/home/me/tools'); from validators.run_all_validators import main; sys.exit(main())\"";
897
+ assert.equal(commandReferencesManagedHook(userPathWithManagedImport, managedPaths), false);
698
898
  });
699
899
 
700
900
 
@@ -44,6 +44,10 @@ Before writing ANY constant: search `config/` for the exact value → semantic m
44
44
 
45
45
  Full words only (`context`, not `ctx`). Exceptions: `i`/`j`/`k` in loops, `e` for exception. Naming patterns: loop vars `each_*`; booleans `is_/has_/should_/can_/was_/did_`; collections `all_*`; maps `X_by_Y`; preposition params (`from_path=`, `to=`, `into=`). Banned names: `result`, `data`, `output`, `response`, `value`, `item`, `temp`. Banned prefixes: `handle`, `process`, `manage`, `do`. Name a component for what it IS — `Overlay`, `Validator`, `InvoicePreview`.
46
46
 
47
+ ### Public compatibility definitions
48
+
49
+ A public function may use the exact inline marker `# pragma: no-banned-noun` on its `def` line. The banned-noun check permits the function name and parameters on that definition. Body local bindings, other definitions, and similar names remain checked.
50
+
47
51
  ---
48
52
 
49
53
  ## 6. COMPLETE TYPE HINTS
@@ -90,6 +94,8 @@ Removed code is removed: no renamed re-export aliases, no `_old_*` aliases, no k
90
94
 
91
95
  Never swallow a failure into a default unless the caller explicitly opted in at the boundary. Name the specific exception (`except KeyError:`) and propagate the rest — collapsing every error class to `None` masks programming errors and makes debugging impossible.
92
96
 
97
+ **A per-member boundary records each member outcome.** In a batch loop, a `try`/`except` inside the loop body catches a declared `*ItemBlocked` type, records the failure with its reason, and continues to the next member — the failure reaches the run report by name. The boundary preserves the blast radius: escalations re-raise first so a `*RunFatal` passes through directly, while `except Exception` triggers the rule. Types, boundary shape, and the parked-member report: [`rules/failure-blast-radius.md`](../rules/failure-blast-radius.md).
98
+
93
99
  ## 9.8 REMOVE CODE YOU ORPHAN (Dead Code Elimination)
94
100
 
95
101
  An edit that deletes or rewrites code also removes everything it makes dead: unread variables, uncalled functions, unpassed parameters, dead branches, unused imports, helper files whose only consumer that edit deleted. Prove unreachability first: Serena `find_referencing_symbols` plus a text search for dynamic lookups (`getattr`, entry-point names). A symbol is live only when a reference chain reaches a live entry point (CLI command, route, public API, test); a self-referential dead cluster is removed together in the same commit. **When liveness is uncertain (public API, plugin hook, reflective dispatch), do NOT delete — surface the ambiguity via AskUserQuestion.** Source links: [`references/dead-code-elimination.md`](references/dead-code-elimination.md).
@@ -0,0 +1,10 @@
1
+ """Pytest registration for shared refactor guard test support."""
2
+
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ advisory_directory = str(Path(__file__).resolve().parent)
7
+ if advisory_directory not in sys.path:
8
+ sys.path.insert(0, advisory_directory)
9
+
10
+ from refactor_guard_test_support import git_repository # noqa: E402, F401
@@ -26,21 +26,21 @@ def main() -> None:
26
26
 
27
27
  if found_unsafe:
28
28
  operations = ", ".join(found_unsafe)
29
- print(
30
- json.dumps(
31
- {
32
- "hookSpecificOutput": {
33
- "hookEventName": "PreToolUse",
34
- "permissionDecision": "ask",
35
- "permissionDecisionReason": (
36
- f"MIGRATION SAFETY: Contains {operations}. "
37
- "Post-launch, model changes MUST be backwards-compatible. "
38
- "Verify this won't break running instances during deployment."
39
- ),
40
- }
41
- }
42
- )
29
+ advisory_message = (
30
+ f"MIGRATION SAFETY: Contains {operations}. "
31
+ "Post-launch, model changes MUST be backwards-compatible. "
32
+ "Verify this won't break running instances during deployment."
43
33
  )
34
+ advisory_payload = {
35
+ "systemMessage": advisory_message,
36
+ "hookSpecificOutput": {
37
+ "hookEventName": "PreToolUse",
38
+ "permissionDecision": "allow",
39
+ "permissionDecisionReason": advisory_message,
40
+ "additionalContext": advisory_message,
41
+ },
42
+ }
43
+ print(json.dumps(advisory_payload))
44
44
 
45
45
  sys.exit(0)
46
46