claude-dev-env 1.94.0 → 2.0.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 (280) hide show
  1. package/_shared/advisor/CLAUDE.md +2 -2
  2. package/_shared/advisor/advisor-protocol.md +41 -33
  3. package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +17 -13
  4. package/_shared/advisor/scripts/model_tier_run_validator.py +27 -18
  5. package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +95 -25
  6. package/_shared/advisor/scripts/tests/test_tier_model_ids.py +17 -17
  7. package/_shared/advisor/scripts/tier_model_ids.py +18 -18
  8. package/_shared/pr-loop/CLAUDE.md +1 -0
  9. package/_shared/pr-loop/scripts/CLAUDE.md +2 -1
  10. package/_shared/pr-loop/scripts/README.md +1 -0
  11. package/_shared/pr-loop/scripts/code_rules_gate.py +253 -1980
  12. package/_shared/pr-loop/scripts/code_rules_gate_parts/CLAUDE.md +32 -0
  13. package/_shared/pr-loop/scripts/code_rules_gate_parts/__init__.py +7 -0
  14. package/_shared/pr-loop/scripts/code_rules_gate_parts/added_line_maps.py +268 -0
  15. package/_shared/pr-loop/scripts/code_rules_gate_parts/enforcer_loading.py +172 -0
  16. package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_arguments.py +70 -0
  17. package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_running.py +326 -0
  18. package/_shared/pr-loop/scripts/code_rules_gate_parts/git_blob_readers.py +85 -0
  19. package/_shared/pr-loop/scripts/code_rules_gate_parts/git_file_sets.py +331 -0
  20. package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_running.py +369 -0
  21. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/conftest.py +14 -0
  22. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_added_line_maps.py +118 -0
  23. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_enforcer_loading.py +17 -0
  24. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_gate_arguments.py +29 -0
  25. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_gate_running.py +99 -0
  26. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_git_blob_readers.py +69 -0
  27. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_git_file_sets.py +137 -0
  28. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_staged_test_running.py +116 -0
  29. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_violation_scoping.py +75 -0
  30. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_wrapper_plumb_check.py +49 -0
  31. package/_shared/pr-loop/scripts/code_rules_gate_parts/violation_scoping.py +328 -0
  32. package/_shared/pr-loop/scripts/code_rules_gate_parts/wrapper_plumb_check.py +206 -0
  33. package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +24 -17
  34. package/_shared/pr-loop/scripts/pr_loop_shared_constants/reviews_disabled_constants.py +1 -0
  35. package/_shared/pr-loop/scripts/reviews_disabled.py +19 -2
  36. package/_shared/pr-loop/scripts/test_code_rules_gate.py +278 -0
  37. package/_shared/pr-loop/scripts/tests/test_code_rules_gate_constants.py +6 -39
  38. package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +43 -0
  39. package/_shared/pr-loop/worker-spawn.md +186 -0
  40. package/agents/code-verifier.md +1 -1
  41. package/bin/CLAUDE.md +10 -1
  42. package/bin/ever-shipped-skills.mjs +73 -0
  43. package/bin/expand_home_directory_tokens.mjs +1 -1
  44. package/bin/install.mjs +140 -7
  45. package/bin/install.prune.test.mjs +457 -0
  46. package/docs/CODE_RULES.md +1 -1
  47. package/hooks/advisory/refactor_guard.py +3 -4
  48. package/hooks/blocking/CLAUDE.md +7 -1
  49. package/hooks/blocking/block_main_commit.py +2 -2
  50. package/hooks/blocking/claude_md_orphan_file_blocker.py +75 -699
  51. package/hooks/blocking/claude_md_orphan_file_blocker_parts/CLAUDE.md +28 -0
  52. package/hooks/blocking/claude_md_orphan_file_blocker_parts/__init__.py +1 -0
  53. package/hooks/blocking/claude_md_orphan_file_blocker_parts/config/__init__.py +1 -0
  54. package/hooks/blocking/claude_md_orphan_file_blocker_parts/config/orphan_blocker_constants.py +18 -0
  55. package/hooks/blocking/claude_md_orphan_file_blocker_parts/decision.py +81 -0
  56. package/hooks/blocking/claude_md_orphan_file_blocker_parts/references.py +307 -0
  57. package/hooks/blocking/claude_md_orphan_file_blocker_parts/scan_plan.py +124 -0
  58. package/hooks/blocking/claude_md_orphan_file_blocker_parts/subtree_scan.py +179 -0
  59. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/conftest.py +10 -0
  60. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_decision.py +34 -0
  61. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_references.py +42 -0
  62. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_scan_plan.py +27 -0
  63. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_subtree_scan.py +30 -0
  64. package/hooks/blocking/code_rules_boolean_mustcheck.py +1 -1
  65. package/hooks/blocking/code_rules_enforcer.py +4 -0
  66. package/hooks/blocking/code_rules_mock_completeness.py +1 -1
  67. package/hooks/blocking/code_rules_optional_params.py +2 -2
  68. package/hooks/blocking/code_rules_shared.py +83 -1
  69. package/hooks/blocking/code_rules_test_assertions.py +1 -1
  70. package/hooks/blocking/code_rules_test_layout.py +9 -3
  71. package/hooks/blocking/code_rules_typeddict_stub.py +1 -1
  72. package/hooks/blocking/gh_pr_author_enforcer.py +1 -1
  73. package/hooks/blocking/inventory_intent_records/CLAUDE.md +26 -0
  74. package/hooks/blocking/inventory_intent_records/__init__.py +1 -0
  75. package/hooks/blocking/inventory_intent_records/config/__init__.py +1 -0
  76. package/hooks/blocking/inventory_intent_records/config/intent_records_constants.py +20 -0
  77. package/hooks/blocking/inventory_intent_records/records.py +271 -0
  78. package/hooks/blocking/inventory_intent_records/tests/conftest.py +10 -0
  79. package/hooks/blocking/inventory_intent_records/tests/test_records.py +80 -0
  80. package/hooks/blocking/package_inventory_stale_blocker.py +54 -384
  81. package/hooks/blocking/package_inventory_stale_blocker_parts/CLAUDE.md +26 -0
  82. package/hooks/blocking/package_inventory_stale_blocker_parts/__init__.py +1 -0
  83. package/hooks/blocking/package_inventory_stale_blocker_parts/config/__init__.py +1 -0
  84. package/hooks/blocking/package_inventory_stale_blocker_parts/config/inventory_blocker_constants.py +16 -0
  85. package/hooks/blocking/package_inventory_stale_blocker_parts/decision.py +84 -0
  86. package/hooks/blocking/package_inventory_stale_blocker_parts/inventory_detection.py +307 -0
  87. package/hooks/blocking/package_inventory_stale_blocker_parts/tests/conftest.py +10 -0
  88. package/hooks/blocking/package_inventory_stale_blocker_parts/tests/test_decision.py +38 -0
  89. package/hooks/blocking/package_inventory_stale_blocker_parts/tests/test_inventory_detection.py +61 -0
  90. package/hooks/blocking/pii_payload_scan.py +138 -42
  91. package/hooks/blocking/pii_prevention_blocker.py +185 -291
  92. package/hooks/blocking/pii_prevention_blocker_parts/CLAUDE.md +24 -0
  93. package/hooks/blocking/pii_prevention_blocker_parts/__init__.py +1 -0
  94. package/hooks/blocking/pii_prevention_blocker_parts/config/__init__.py +1 -0
  95. package/hooks/blocking/pii_prevention_blocker_parts/config/repository_resolution_constants.py +28 -0
  96. package/hooks/blocking/pii_prevention_blocker_parts/repository_exemption.py +214 -0
  97. package/hooks/blocking/pii_prevention_blocker_parts/repository_resolution.py +208 -0
  98. package/hooks/blocking/plain_language_blocker.py +138 -4
  99. package/hooks/blocking/pr_description_command_parser.py +8 -4
  100. package/hooks/blocking/precommit_code_rules_gate.py +3 -3
  101. package/hooks/blocking/sensitive_file_protector.py +114 -48
  102. package/hooks/blocking/tdd_enforcer.py +97 -601
  103. package/hooks/blocking/tdd_enforcer_parts/CLAUDE.md +30 -0
  104. package/hooks/blocking/tdd_enforcer_parts/__init__.py +1 -0
  105. package/hooks/blocking/tdd_enforcer_parts/candidate_paths.py +142 -0
  106. package/hooks/blocking/tdd_enforcer_parts/config/__init__.py +1 -0
  107. package/hooks/blocking/tdd_enforcer_parts/config/tdd_enforcer_constants.py +32 -0
  108. package/hooks/blocking/tdd_enforcer_parts/content_analysis.py +268 -0
  109. package/hooks/blocking/tdd_enforcer_parts/decisions.py +92 -0
  110. package/hooks/blocking/tdd_enforcer_parts/freshness.py +80 -0
  111. package/hooks/blocking/tdd_enforcer_parts/git_tracking.py +63 -0
  112. package/hooks/blocking/tdd_enforcer_parts/path_classification.py +119 -0
  113. package/hooks/blocking/tdd_enforcer_parts/tests/conftest.py +10 -0
  114. package/hooks/blocking/tdd_enforcer_parts/tests/test_candidate_paths.py +31 -0
  115. package/hooks/blocking/tdd_enforcer_parts/tests/test_content_analysis.py +30 -0
  116. package/hooks/blocking/tdd_enforcer_parts/tests/test_decisions.py +34 -0
  117. package/hooks/blocking/tdd_enforcer_parts/tests/test_freshness.py +28 -0
  118. package/hooks/blocking/tdd_enforcer_parts/tests/test_git_tracking.py +48 -0
  119. package/hooks/blocking/tdd_enforcer_parts/tests/test_path_classification.py +36 -0
  120. package/hooks/blocking/test_code_rules_enforcer_scratchpad.py +105 -0
  121. package/hooks/blocking/test_code_rules_shared.py +181 -0
  122. package/hooks/blocking/test_inventory_deadlock_resolution.py +154 -0
  123. package/hooks/blocking/test_pii_payload_scan.py +168 -0
  124. package/hooks/blocking/test_plain_language_blocker_allowlist.py +184 -0
  125. package/hooks/blocking/test_sensitive_file_protector.py +185 -0
  126. package/hooks/blocking/test_tdd_enforcer_restore.py +108 -0
  127. package/hooks/blocking/test_tdd_enforcer_scratchpad.py +105 -0
  128. package/hooks/blocking/tests/conftest.py +10 -0
  129. package/hooks/blocking/tests/test_pii_prevention_blocker.py +260 -0
  130. package/hooks/blocking/tests/test_repository_exemption.py +105 -0
  131. package/hooks/blocking/tests/test_repository_resolution.py +108 -0
  132. package/hooks/diagnostic/hook_log_extractor.py +12 -10
  133. package/hooks/git-hooks/post_commit.py +3 -4
  134. package/hooks/hooks_constants/CLAUDE.md +4 -2
  135. package/hooks/hooks_constants/banned_identifiers_constants.py +0 -1
  136. package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -1
  137. package/hooks/hooks_constants/harness_scratchpad_constants.py +17 -0
  138. package/hooks/hooks_constants/local_identity.py +59 -8
  139. package/hooks/hooks_constants/pii_prevention_constants.py +0 -6
  140. package/hooks/hooks_constants/plain_language_blocker_constants.py +5 -0
  141. package/hooks/hooks_constants/sensitive_file_protector_constants.py +42 -0
  142. package/hooks/hooks_constants/test_local_identity.py +105 -3
  143. package/hooks/pyproject.toml +52 -4
  144. package/hooks/session/plugin_data_dir_cleanup.py +0 -1
  145. package/hooks/validation/mypy_validator.py +2 -2
  146. package/hooks/validators/CLAUDE.md +1 -1
  147. package/hooks/validators/README.md +2 -0
  148. package/hooks/validators/health_check.py +1 -0
  149. package/hooks/validators/mypy_integration.py +2 -0
  150. package/hooks/validators/python_style_checks.py +114 -136
  151. package/hooks/validators/python_style_helpers.py +95 -0
  152. package/hooks/validators/ruff_integration.py +3 -0
  153. package/hooks/validators/test_python_style_checks.py +0 -164
  154. package/hooks/validators/test_python_style_checks_decorator_gap.py +119 -0
  155. package/hooks/validators/test_python_style_fixes.py +251 -0
  156. package/hooks/validators/test_python_style_helpers.py +125 -0
  157. package/hooks/workflow/auto_formatter.py +5 -4
  158. package/package.json +1 -1
  159. package/rules/CLAUDE.md +1 -0
  160. package/rules/anti-corollary-tests.md +69 -0
  161. package/rules/bdd.md +1 -3
  162. package/rules/code-reviews.md +1 -1
  163. package/rules/gh-paginate.md +1 -1
  164. package/rules/plain-language.md +2 -0
  165. package/scripts/CLAUDE.md +4 -0
  166. package/scripts/dev_env_scripts_constants/CLAUDE.md +6 -4
  167. package/scripts/dev_env_scripts_constants/code_review_constants.py +71 -0
  168. package/scripts/dev_env_scripts_constants/grok_worker_constants.py +435 -0
  169. package/scripts/dev_env_scripts_constants/timing.py +7 -1
  170. package/scripts/grok_headless_runner.py +294 -0
  171. package/scripts/grok_worker_preflight.py +410 -0
  172. package/scripts/invoke_code_review.py +463 -0
  173. package/scripts/resolve_worker_spawn.py +619 -0
  174. package/scripts/spawn_grok_batch.py +672 -0
  175. package/scripts/test_grok_headless_runner.py +626 -0
  176. package/scripts/test_grok_worker_preflight.py +1054 -0
  177. package/scripts/test_invoke_code_review.py +672 -0
  178. package/scripts/test_resolve_worker_spawn.py +1014 -0
  179. package/scripts/test_spawn_grok_batch.py +1017 -0
  180. package/skills/CLAUDE.md +6 -3
  181. package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +72 -13
  182. package/skills/_shared/pr-loop/scripts/build_fix_prompt.py +121 -14
  183. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +78 -0
  184. package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +121 -0
  185. package/skills/_shared/pr-loop/scripts/test_build_fix_prompt.py +196 -6
  186. package/skills/autoconverge/CLAUDE.md +3 -3
  187. package/skills/autoconverge/SKILL.md +9 -3
  188. package/skills/autoconverge/reference/CLAUDE.md +2 -2
  189. package/skills/autoconverge/reference/convergence.md +33 -11
  190. package/skills/autoconverge/reference/stop-conditions.md +16 -5
  191. package/skills/autoconverge/workflow/CLAUDE.md +2 -1
  192. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +7 -2
  193. package/skills/autoconverge/workflow/converge.codex-gate.test.mjs +300 -0
  194. package/skills/autoconverge/workflow/converge.contract.test.mjs +5 -5
  195. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +29 -29
  196. package/skills/autoconverge/workflow/converge.fix-progress.test.mjs +1 -1
  197. package/skills/autoconverge/workflow/converge.mjs +202 -18
  198. package/skills/bugteam/CLAUDE.md +2 -2
  199. package/skills/bugteam/CONSTRAINTS.md +3 -2
  200. package/skills/bugteam/PROMPTS.md +7 -6
  201. package/skills/bugteam/SKILL.md +18 -13
  202. package/skills/bugteam/reference/README.md +2 -3
  203. package/skills/bugteam/reference/audit-and-teammates.md +215 -35
  204. package/skills/bugteam/reference/design-rationale.md +1 -1
  205. package/skills/bugteam/reference/obstacles/CLAUDE.md +1 -1
  206. package/skills/bugteam/reference/team-setup.md +8 -2
  207. package/skills/closeout/SKILL.md +153 -0
  208. package/skills/closeout/reference/handoff-prompt-template.md +72 -0
  209. package/skills/closeout/reference/issue-body-templates.md +108 -0
  210. package/skills/closeout/reference/pii-redaction-checklist.md +36 -0
  211. package/skills/codex-review/CLAUDE.md +46 -0
  212. package/skills/codex-review/SKILL.md +181 -0
  213. package/skills/codex-review/reference/CLAUDE.md +15 -0
  214. package/skills/codex-review/reference/cli-contract.md +253 -0
  215. package/skills/codex-review/reference/loop-integration.md +118 -0
  216. package/skills/codex-review/scripts/codex_down_classifier.py +98 -0
  217. package/skills/codex-review/scripts/codex_review_scripts_constants/CLAUDE.md +18 -0
  218. package/skills/codex-review/scripts/codex_review_scripts_constants/__init__.py +1 -0
  219. package/skills/codex-review/scripts/codex_review_scripts_constants/classifier_constants.py +35 -0
  220. package/skills/codex-review/scripts/codex_review_scripts_constants/codex_usage_probe_constants.py +86 -0
  221. package/skills/codex-review/scripts/codex_review_scripts_constants/findings_constants.py +18 -0
  222. package/skills/codex-review/scripts/codex_review_scripts_constants/run_constants.py +45 -0
  223. package/skills/codex-review/scripts/codex_usage_probe.py +573 -0
  224. package/skills/codex-review/scripts/fixtures/auth_failure_synthetic.txt +1 -0
  225. package/skills/codex-review/scripts/fixtures/config_load_failure_v0.125.0.txt +1 -0
  226. package/skills/codex-review/scripts/fixtures/freeform_findings_v0.144.3.txt +6 -0
  227. package/skills/codex-review/scripts/fixtures/model_rejection_v0.125.0.jsonl +5 -0
  228. package/skills/codex-review/scripts/fixtures/structured_findings.txt +13 -0
  229. package/skills/codex-review/scripts/fixtures/success_stream_v0.144.3.jsonl +6 -0
  230. package/skills/codex-review/scripts/fixtures/unknown_failure_synthetic.txt +1 -0
  231. package/skills/codex-review/scripts/fixtures/usage_limit_synthetic.txt +1 -0
  232. package/skills/codex-review/scripts/parse_codex_findings.py +207 -0
  233. package/skills/codex-review/scripts/run_codex_review.py +415 -0
  234. package/skills/codex-review/scripts/test_codex_down_classifier.py +143 -0
  235. package/skills/codex-review/scripts/test_codex_usage_probe.py +678 -0
  236. package/skills/codex-review/scripts/test_parse_codex_findings.py +130 -0
  237. package/skills/codex-review/scripts/test_run_codex_review.py +812 -0
  238. package/skills/codex-review/test_skill_scaffold.py +192 -0
  239. package/skills/grok-spawn/CLAUDE.md +28 -0
  240. package/skills/grok-spawn/SKILL.md +226 -0
  241. package/skills/grok-spawn/reference/flag-profiles.md +132 -0
  242. package/skills/grok-spawn/reference/worker-briefs.md +152 -0
  243. package/skills/grokify/SKILL.md +9 -1
  244. package/skills/grokify/capability-claims.test.mjs +28 -0
  245. package/skills/grokify/evals/README.md +72 -0
  246. package/skills/grokify/evals/parse-payload.test.mjs +171 -0
  247. package/skills/grokify/evals/run-capability-evals.mjs +545 -0
  248. package/skills/orchestrator/SKILL.md +32 -22
  249. package/skills/orchestrator-refresh/SKILL.md +12 -8
  250. package/skills/pr-converge/CLAUDE.md +1 -1
  251. package/skills/pr-converge/SKILL.md +34 -13
  252. package/skills/pr-converge/reference/convergence-gates.md +42 -15
  253. package/skills/pr-converge/reference/fix-protocol.md +1 -1
  254. package/skills/pr-converge/reference/ground-rules.md +1 -1
  255. package/skills/pr-converge/reference/per-tick.md +130 -42
  256. package/skills/pr-converge/reference/state-schema.md +10 -0
  257. package/skills/pr-converge/scripts/CLAUDE.md +2 -0
  258. package/skills/pr-converge/scripts/_pr_converge_path_setup.py +5 -1
  259. package/skills/pr-converge/scripts/check_convergence.py +605 -29
  260. package/skills/pr-converge/scripts/check_convergence_availability.py +232 -0
  261. package/skills/pr-converge/scripts/check_convergence_gates.py +279 -235
  262. package/skills/pr-converge/scripts/check_convergence_thread_gates.py +1 -1
  263. package/skills/pr-converge/scripts/pr_converge_scripts_constants/convergence_gate_constants.py +36 -2
  264. package/skills/pr-converge/scripts/test__pr_converge_path_setup.py +4 -0
  265. package/skills/pr-converge/scripts/test_check_convergence.py +71 -3
  266. package/skills/pr-converge/scripts/test_check_convergence_availability.py +326 -0
  267. package/skills/pr-converge/scripts/test_check_convergence_codex.py +507 -0
  268. package/skills/pr-converge/scripts/test_check_convergence_contract.py +89 -17
  269. package/skills/pr-converge/scripts/test_check_convergence_fixture.py +179 -0
  270. package/skills/pr-converge/scripts/test_check_convergence_gates.py +84 -68
  271. package/skills/pr-converge/scripts/test_check_convergence_thread_gates.py +24 -0
  272. package/skills/pr-converge/test_step5_host_branch.py +106 -0
  273. package/skills/pr-fix-protocol/SKILL.md +65 -0
  274. package/skills/pr-loop-cloud-transport/SKILL.md +2 -0
  275. package/skills/reviewer-gates/SKILL.md +7 -5
  276. package/skills/skill-builder/references/skill-modularity.md +1 -1
  277. package/skills/team-advisor/SKILL.md +16 -12
  278. package/system-prompts/software-engineer.xml +7 -6
  279. package/hooks/validators/test_verify_paths.py +0 -32
  280. package/hooks/validators/verify_paths.py +0 -57
@@ -0,0 +1,457 @@
1
+ import { test, after } from 'node:test';
2
+ import { strict as assert } from 'node:assert';
3
+ import { execFileSync } from 'node:child_process';
4
+ import {
5
+ mkdtempSync,
6
+ mkdirSync,
7
+ writeFileSync,
8
+ existsSync,
9
+ readFileSync,
10
+ readdirSync,
11
+ rmSync,
12
+ cpSync,
13
+ } from 'node:fs';
14
+ import { tmpdir } from 'node:os';
15
+ import { join, dirname, basename } from 'node:path';
16
+ import { fileURLToPath } from 'node:url';
17
+
18
+ const THIS_DIRECTORY = dirname(fileURLToPath(import.meta.url));
19
+ const INSTALLER_PATH = join(THIS_DIRECTORY, 'install.mjs');
20
+ const PACKAGE_DIRECTORY = dirname(THIS_DIRECTORY);
21
+ const EXCLUDED_PACKAGE_COPY_DIRECTORY = 'node_modules';
22
+
23
+ const RETIRED_SKILL_DIRECTORIES = [
24
+ 'findbugs',
25
+ 'fixbugs',
26
+ 'pr-scope-resolve',
27
+ 'post-audit-findings',
28
+ 'pr-consistency-audit',
29
+ 'bdd-protocol',
30
+ ];
31
+ const PERSONAL_SKILL_DIRECTORIES = ['balatro', 'midjourney-sref', 'credit-card-picker'];
32
+ const SHIPPED_SKILL_DIRECTORY = 'autoconverge';
33
+ const PRUNED_BACKUP_DIRECTORY_NAME = '.claude-dev-env-pruned';
34
+ const SKIP_PRUNE_NOTICE_MARKER = 'Skipping retired-skill prune';
35
+ const DEPENDENCY_STUB_PACKAGE_SEGMENTS = ['@jl-cmd', 'prompt-generator'];
36
+
37
+ /**
38
+ * Create a stub node_modules tree the installer can resolve the declared
39
+ * dependency package from, and return its root for the child's NODE_PATH.
40
+ *
41
+ * The installer resolves `@jl-cmd/prompt-generator/package.json`; a lone
42
+ * package.json at that path is enough for `createRequire.resolve` to succeed.
43
+ * The stub makes the dependency resolvable inside the sandbox so a full install
44
+ * exercises the real prune path. It does not prove the real private package
45
+ * resolves in a real install — CI cannot host that package — only that the
46
+ * resolved-dependency branch prunes as designed.
47
+ *
48
+ * @param {string} homeDirectory The sandbox home the stub is nested under.
49
+ * @returns {string} The stub modules root to place on the child's NODE_PATH.
50
+ */
51
+ let isolatedInstallerPath = null;
52
+ let isolatedPackageCopyRoot = null;
53
+
54
+ /**
55
+ * Copy the package into a temp directory without node_modules and return the
56
+ * copy's installer path.
57
+ *
58
+ * The installer resolves its declared dependencies with Node's regular
59
+ * node_modules walk starting at its own file, so the real installer under the
60
+ * repo always finds `@jl-cmd/prompt-generator` once `npm install` has run —
61
+ * removing NODE_PATH from the child cannot make the dependency unresolvable
62
+ * there. Running the copy makes resolution genuinely fail through the
63
+ * installer's own catch path: no node_modules sits in the copy's ancestry and
64
+ * the child gets no NODE_PATH. The copy is created once and shared by every
65
+ * unresolved-dependency install; installs write only into their sandbox HOME.
66
+ *
67
+ * @returns {string} The path to `bin/install.mjs` inside the package copy.
68
+ */
69
+ function ensureIsolatedInstallerPath() {
70
+ if (isolatedInstallerPath !== null) return isolatedInstallerPath;
71
+ isolatedPackageCopyRoot = mkdtempSync(join(tmpdir(), 'cdev-prune-package-'));
72
+ cpSync(PACKAGE_DIRECTORY, isolatedPackageCopyRoot, {
73
+ recursive: true,
74
+ filter: sourcePath => basename(sourcePath) !== EXCLUDED_PACKAGE_COPY_DIRECTORY,
75
+ });
76
+ isolatedInstallerPath = join(isolatedPackageCopyRoot, 'bin', 'install.mjs');
77
+ return isolatedInstallerPath;
78
+ }
79
+
80
+ after(() => {
81
+ if (isolatedPackageCopyRoot !== null) {
82
+ rmSync(isolatedPackageCopyRoot, { recursive: true, force: true });
83
+ }
84
+ });
85
+
86
+ function ensureDependencyStub(homeDirectory) {
87
+ const stubModulesRoot = join(homeDirectory, 'dependency-stub-modules');
88
+ const stubPackageDirectory = join(stubModulesRoot, ...DEPENDENCY_STUB_PACKAGE_SEGMENTS);
89
+ mkdirSync(stubPackageDirectory, { recursive: true });
90
+ writeFileSync(
91
+ join(stubPackageDirectory, 'package.json'),
92
+ JSON.stringify({
93
+ name: DEPENDENCY_STUB_PACKAGE_SEGMENTS.join('/'),
94
+ version: '1.0.0',
95
+ description: 'sandbox dependency stub',
96
+ }) + '\n',
97
+ );
98
+ return stubModulesRoot;
99
+ }
100
+
101
+ /**
102
+ * Report whether a retired skill directory landed under the prune backup root.
103
+ *
104
+ * @param {string} claudeDirectory The sandbox ~/.claude directory.
105
+ * @param {string} skillName The retired skill directory name to look for.
106
+ * @returns {boolean} True when a timestamped backup holds the skill directory.
107
+ */
108
+ function prunedBackupContains(claudeDirectory, skillName) {
109
+ const backupRoot = join(claudeDirectory, PRUNED_BACKUP_DIRECTORY_NAME);
110
+ if (!existsSync(backupRoot)) return false;
111
+ return readdirSync(backupRoot, { withFileTypes: true })
112
+ .filter(entry => entry.isDirectory())
113
+ .some(timestampDir => existsSync(join(backupRoot, timestampDir.name, skillName)));
114
+ }
115
+
116
+ /**
117
+ * Create an isolated ~/.claude sandbox and return the paths a prune test reads.
118
+ *
119
+ * The returned home directory becomes the installer's HOME, so every file the
120
+ * install writes and every skill directory the prune inspects stays inside the
121
+ * temp tree and never touches the machine's real ~/.claude.
122
+ *
123
+ * @returns {{homeDirectory: string, claudeDirectory: string, skillsDirectory: string, manifestPath: string}}
124
+ */
125
+ function createSandbox() {
126
+ const homeDirectory = mkdtempSync(join(tmpdir(), 'cdev-prune-home-'));
127
+ const claudeDirectory = join(homeDirectory, '.claude');
128
+ const skillsDirectory = join(claudeDirectory, 'skills');
129
+ mkdirSync(skillsDirectory, { recursive: true });
130
+ const manifestPath = join(claudeDirectory, '.claude-dev-env-manifest.json');
131
+ return { homeDirectory, claudeDirectory, skillsDirectory, manifestPath };
132
+ }
133
+
134
+ /**
135
+ * Plant a skill directory under the sandbox with a single marker file.
136
+ *
137
+ * A personal directory is planted with no ``SKILL.md`` so it never looks like a
138
+ * shipped skill; a retired directory is planted with a ``SKILL.md`` so it mirrors
139
+ * a real skill the package once installed and later dropped.
140
+ *
141
+ * @param {string} skillsDirectory The sandbox skills directory.
142
+ * @param {string} skillName The directory name to plant.
143
+ * @param {boolean} withSkillManifest Whether to write a ``SKILL.md`` marker.
144
+ */
145
+ function plantSkillDirectory(skillsDirectory, skillName, withSkillManifest) {
146
+ const skillDirectory = join(skillsDirectory, skillName);
147
+ mkdirSync(skillDirectory, { recursive: true });
148
+ const markerName = withSkillManifest ? 'SKILL.md' : 'notes.md';
149
+ writeFileSync(join(skillDirectory, markerName), `seeded ${skillName}\n`);
150
+ }
151
+
152
+ /**
153
+ * Run the real installer against the sandbox home and return its stdout.
154
+ *
155
+ * The declared dependency package is resolvable by default: the child's
156
+ * NODE_PATH points at a sandbox stub so `createRequire.resolve` finds it and the
157
+ * full-install prune runs. Passing ``{ dependencyResolvable: false }`` runs the
158
+ * installer from an isolated package copy with no node_modules in its ancestry
159
+ * and no NODE_PATH, so the dependency fails to resolve and the installer skips
160
+ * the prune.
161
+ *
162
+ * @param {string} homeDirectory The sandbox home the installer writes into.
163
+ * @param {string[]} extraArguments Installer arguments (for example ``['--only', 'core']``).
164
+ * @param {{dependencyResolvable?: boolean}} options Whether the dependency resolves.
165
+ * @returns {string} The installer's stdout.
166
+ */
167
+ function runInstaller(homeDirectory, extraArguments, options = {}) {
168
+ const dependencyResolvable = options.dependencyResolvable !== false;
169
+ const childEnvironment = {
170
+ ...process.env,
171
+ HOME: homeDirectory,
172
+ USERPROFILE: homeDirectory,
173
+ GIT_CONFIG_GLOBAL: join(homeDirectory, '.gitconfig'),
174
+ };
175
+ let installerPath;
176
+ if (dependencyResolvable) {
177
+ childEnvironment.NODE_PATH = ensureDependencyStub(homeDirectory);
178
+ installerPath = INSTALLER_PATH;
179
+ } else {
180
+ delete childEnvironment.NODE_PATH;
181
+ installerPath = ensureIsolatedInstallerPath();
182
+ }
183
+ return execFileSync('node', [installerPath, ...extraArguments], {
184
+ cwd: dirname(installerPath),
185
+ encoding: 'utf8',
186
+ env: childEnvironment,
187
+ });
188
+ }
189
+
190
+ function readManifest(manifestPath) {
191
+ return JSON.parse(readFileSync(manifestPath, 'utf8'));
192
+ }
193
+
194
+ test('a full reinstall over a pre-manifest dirty tree prunes retired skills and keeps personal ones', () => {
195
+ const sandbox = createSandbox();
196
+ try {
197
+ for (const retiredSkill of RETIRED_SKILL_DIRECTORIES) {
198
+ plantSkillDirectory(sandbox.skillsDirectory, retiredSkill, true);
199
+ }
200
+ for (const personalSkill of PERSONAL_SKILL_DIRECTORIES) {
201
+ plantSkillDirectory(sandbox.skillsDirectory, personalSkill, false);
202
+ }
203
+ assert.equal(existsSync(sandbox.manifestPath), false, 'sandbox starts with no manifest');
204
+
205
+ const installerOutput = runInstaller(sandbox.homeDirectory, []);
206
+
207
+ assert.equal(
208
+ installerOutput.includes(SKIP_PRUNE_NOTICE_MARKER),
209
+ false,
210
+ 'the resolvable-dependency install runs the prune rather than skipping it',
211
+ );
212
+ for (const retiredSkill of RETIRED_SKILL_DIRECTORIES) {
213
+ assert.equal(
214
+ existsSync(join(sandbox.skillsDirectory, retiredSkill)),
215
+ false,
216
+ `retired skill ${retiredSkill} should be pruned`,
217
+ );
218
+ assert.equal(
219
+ prunedBackupContains(sandbox.claudeDirectory, retiredSkill),
220
+ true,
221
+ `retired skill ${retiredSkill} should be moved to the prune backup`,
222
+ );
223
+ }
224
+ for (const personalSkill of PERSONAL_SKILL_DIRECTORIES) {
225
+ assert.equal(
226
+ existsSync(join(sandbox.skillsDirectory, personalSkill)),
227
+ true,
228
+ `personal skill ${personalSkill} should survive`,
229
+ );
230
+ }
231
+ assert.equal(
232
+ existsSync(join(sandbox.skillsDirectory, SHIPPED_SKILL_DIRECTORY)),
233
+ true,
234
+ 'shipped skills should install',
235
+ );
236
+ const manifest = readManifest(sandbox.manifestPath);
237
+ assert.ok(Array.isArray(manifest.skills), 'manifest gains a skills array');
238
+ assert.ok(manifest.skills.includes(SHIPPED_SKILL_DIRECTORY), 'manifest skills lists shipped skills');
239
+ assert.equal(manifest.skills.includes('_shared'), false, 'manifest skills omits _shared');
240
+ assert.equal(manifest.skills.includes('__pycache__'), false, 'manifest skills omits __pycache__');
241
+ } finally {
242
+ rmSync(sandbox.homeDirectory, { recursive: true, force: true });
243
+ }
244
+ });
245
+
246
+ test('a full reinstall over an old-format manifest without a skills key still prunes via the ever-shipped fallback', () => {
247
+ const sandbox = createSandbox();
248
+ try {
249
+ for (const retiredSkill of RETIRED_SKILL_DIRECTORIES) {
250
+ plantSkillDirectory(sandbox.skillsDirectory, retiredSkill, true);
251
+ }
252
+ writeFileSync(
253
+ sandbox.manifestPath,
254
+ JSON.stringify({
255
+ package: 'claude-dev-env',
256
+ version: '0.0.0',
257
+ installedAt: new Date().toISOString(),
258
+ files: [join(sandbox.skillsDirectory, 'findbugs', 'SKILL.md')],
259
+ }, null, 2) + '\n',
260
+ );
261
+
262
+ const installerOutput = runInstaller(sandbox.homeDirectory, []);
263
+
264
+ assert.equal(
265
+ installerOutput.includes(SKIP_PRUNE_NOTICE_MARKER),
266
+ false,
267
+ 'the resolvable-dependency install runs the prune rather than skipping it',
268
+ );
269
+ for (const retiredSkill of RETIRED_SKILL_DIRECTORIES) {
270
+ assert.equal(
271
+ existsSync(join(sandbox.skillsDirectory, retiredSkill)),
272
+ false,
273
+ `retired skill ${retiredSkill} should be pruned via the ever-shipped fallback`,
274
+ );
275
+ assert.equal(
276
+ prunedBackupContains(sandbox.claudeDirectory, retiredSkill),
277
+ true,
278
+ `retired skill ${retiredSkill} should be moved to the prune backup`,
279
+ );
280
+ }
281
+ const manifest = readManifest(sandbox.manifestPath);
282
+ assert.ok(Array.isArray(manifest.skills), 'the reinstall writes the skills key onto the old-format manifest');
283
+ } finally {
284
+ rmSync(sandbox.homeDirectory, { recursive: true, force: true });
285
+ }
286
+ });
287
+
288
+ test('a full reinstall prunes a manifest-recorded skill absent from the package and keeps a personal directory', () => {
289
+ const sandbox = createSandbox();
290
+ try {
291
+ const retiredManifestSkill = 'ghost-skill';
292
+ const personalSkill = 'balatro';
293
+ plantSkillDirectory(sandbox.skillsDirectory, retiredManifestSkill, true);
294
+ plantSkillDirectory(sandbox.skillsDirectory, personalSkill, false);
295
+ writeFileSync(
296
+ sandbox.manifestPath,
297
+ JSON.stringify({
298
+ package: 'claude-dev-env',
299
+ version: '0.0.0',
300
+ installedAt: new Date().toISOString(),
301
+ files: [],
302
+ skills: [SHIPPED_SKILL_DIRECTORY, retiredManifestSkill],
303
+ }, null, 2) + '\n',
304
+ );
305
+
306
+ const installerOutput = runInstaller(sandbox.homeDirectory, []);
307
+
308
+ assert.equal(
309
+ installerOutput.includes(SKIP_PRUNE_NOTICE_MARKER),
310
+ false,
311
+ 'the resolvable-dependency install runs the prune rather than skipping it',
312
+ );
313
+ assert.equal(
314
+ existsSync(join(sandbox.skillsDirectory, retiredManifestSkill)),
315
+ false,
316
+ 'a skill the prior manifest recorded but the package no longer ships is pruned',
317
+ );
318
+ assert.equal(
319
+ prunedBackupContains(sandbox.claudeDirectory, retiredManifestSkill),
320
+ true,
321
+ 'the manifest-recorded retired skill is moved to the prune backup',
322
+ );
323
+ assert.equal(
324
+ existsSync(join(sandbox.skillsDirectory, personalSkill)),
325
+ true,
326
+ 'a personal directory in neither the manifest nor the ever-shipped set survives',
327
+ );
328
+ } finally {
329
+ rmSync(sandbox.homeDirectory, { recursive: true, force: true });
330
+ }
331
+ });
332
+
333
+ test('a scoped --only install leaves retired skills in place because prune runs on full installs only', () => {
334
+ const sandbox = createSandbox();
335
+ try {
336
+ for (const retiredSkill of RETIRED_SKILL_DIRECTORIES) {
337
+ plantSkillDirectory(sandbox.skillsDirectory, retiredSkill, true);
338
+ }
339
+
340
+ runInstaller(sandbox.homeDirectory, ['--only', 'core']);
341
+
342
+ for (const retiredSkill of RETIRED_SKILL_DIRECTORIES) {
343
+ assert.equal(
344
+ existsSync(join(sandbox.skillsDirectory, retiredSkill)),
345
+ true,
346
+ `retired skill ${retiredSkill} should survive a scoped install`,
347
+ );
348
+ assert.equal(
349
+ prunedBackupContains(sandbox.claudeDirectory, retiredSkill),
350
+ false,
351
+ `retired skill ${retiredSkill} should not be moved to backup by a scoped install`,
352
+ );
353
+ }
354
+ } finally {
355
+ rmSync(sandbox.homeDirectory, { recursive: true, force: true });
356
+ }
357
+ });
358
+
359
+ test('a full reinstall keeps pr-fix-protocol because the package ships it again', () => {
360
+ const sandbox = createSandbox();
361
+ try {
362
+ plantSkillDirectory(sandbox.skillsDirectory, 'pr-fix-protocol', true);
363
+ writeFileSync(join(sandbox.skillsDirectory, 'pr-fix-protocol', 'SKILL.md'), 'stale seeded copy\n');
364
+
365
+ const installerOutput = runInstaller(sandbox.homeDirectory, []);
366
+
367
+ assert.equal(
368
+ installerOutput.includes(SKIP_PRUNE_NOTICE_MARKER),
369
+ false,
370
+ 'the resolvable-dependency install runs the prune rather than skipping it',
371
+ );
372
+ const restoredSkillPath = join(sandbox.skillsDirectory, 'pr-fix-protocol', 'SKILL.md');
373
+ assert.equal(existsSync(restoredSkillPath), true, 'pr-fix-protocol survives and is reinstalled');
374
+ assert.notEqual(
375
+ readFileSync(restoredSkillPath, 'utf8'),
376
+ 'stale seeded copy\n',
377
+ 'the shipped pr-fix-protocol overwrites the stale seeded copy',
378
+ );
379
+ } finally {
380
+ rmSync(sandbox.homeDirectory, { recursive: true, force: true });
381
+ }
382
+ });
383
+
384
+ test('a full reinstall with an unresolved dependency skips the prune and leaves retired skills untouched', () => {
385
+ const sandbox = createSandbox();
386
+ try {
387
+ for (const retiredSkill of RETIRED_SKILL_DIRECTORIES) {
388
+ plantSkillDirectory(sandbox.skillsDirectory, retiredSkill, true);
389
+ }
390
+
391
+ const installerOutput = runInstaller(sandbox.homeDirectory, [], { dependencyResolvable: false });
392
+
393
+ assert.equal(
394
+ installerOutput.includes(SKIP_PRUNE_NOTICE_MARKER),
395
+ true,
396
+ 'the installer logs a notice that it is skipping the prune',
397
+ );
398
+ for (const retiredSkill of RETIRED_SKILL_DIRECTORIES) {
399
+ assert.equal(
400
+ existsSync(join(sandbox.skillsDirectory, retiredSkill)),
401
+ true,
402
+ `retired skill ${retiredSkill} should survive when a dependency is unresolved`,
403
+ );
404
+ assert.equal(
405
+ prunedBackupContains(sandbox.claudeDirectory, retiredSkill),
406
+ false,
407
+ `retired skill ${retiredSkill} should not be moved to backup when the prune is skipped`,
408
+ );
409
+ }
410
+ } finally {
411
+ rmSync(sandbox.homeDirectory, { recursive: true, force: true });
412
+ }
413
+ });
414
+
415
+ test('the prune skip is scoped: once the dependency resolves a later full install prunes normally', () => {
416
+ const sandbox = createSandbox();
417
+ try {
418
+ for (const retiredSkill of RETIRED_SKILL_DIRECTORIES) {
419
+ plantSkillDirectory(sandbox.skillsDirectory, retiredSkill, true);
420
+ }
421
+
422
+ const skippedOutput = runInstaller(sandbox.homeDirectory, [], { dependencyResolvable: false });
423
+ assert.equal(
424
+ skippedOutput.includes(SKIP_PRUNE_NOTICE_MARKER),
425
+ true,
426
+ 'the unresolved-dependency install skips the prune',
427
+ );
428
+ for (const retiredSkill of RETIRED_SKILL_DIRECTORIES) {
429
+ assert.equal(
430
+ existsSync(join(sandbox.skillsDirectory, retiredSkill)),
431
+ true,
432
+ `retired skill ${retiredSkill} should still be present after the skipped prune`,
433
+ );
434
+ }
435
+
436
+ const resolvedOutput = runInstaller(sandbox.homeDirectory, [], { dependencyResolvable: true });
437
+ assert.equal(
438
+ resolvedOutput.includes(SKIP_PRUNE_NOTICE_MARKER),
439
+ false,
440
+ 'the resolved-dependency install runs the prune, proving the skip is not a permanent disable',
441
+ );
442
+ for (const retiredSkill of RETIRED_SKILL_DIRECTORIES) {
443
+ assert.equal(
444
+ existsSync(join(sandbox.skillsDirectory, retiredSkill)),
445
+ false,
446
+ `retired skill ${retiredSkill} should be pruned once the dependency resolves`,
447
+ );
448
+ assert.equal(
449
+ prunedBackupContains(sandbox.claudeDirectory, retiredSkill),
450
+ true,
451
+ `retired skill ${retiredSkill} should be moved to the prune backup on the resolved install`,
452
+ );
453
+ }
454
+ } finally {
455
+ rmSync(sandbox.homeDirectory, { recursive: true, force: true });
456
+ }
457
+ });
@@ -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, 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–Q) 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. The diagram-first docstring standard (a summary line, then a `::` example or doctest, then a couple of short prose lines) lives in `packages/claude-dev-env/rules/plain-illustrative-docstrings.md`, enforced by the `check_docstring_runon_sentence` and `check_docstring_prose_wall_without_illustration` backstop hooks and Category O9 audit.
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). 👥 **Audit rubrics** (`/check`, `packages/claude-dev-env/audit-rubrics/` categories A–Q) cover cross-file architectural concerns. Rules with documented-but-pending hook coverage live in `~/.claude/rules/*.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. The diagram-first docstring standard (a summary line, then a `::` example or doctest, then a couple of short prose lines) lives in `packages/claude-dev-env/rules/plain-illustrative-docstrings.md`, enforced by the `check_docstring_runon_sentence` and `check_docstring_prose_wall_without_illustration` backstop hooks and Category O9 audit.
@@ -8,7 +8,6 @@ functions, restructuring) rather than writing new code or replacing wholesale.
8
8
  Only fires for Edit operations (not Write, which creates/replaces entire files).
9
9
  """
10
10
  import json
11
- import os
12
11
  import re
13
12
  import subprocess
14
13
  import sys
@@ -24,7 +23,7 @@ def get_git_diff_added_lines(file_path: str) -> set[str]:
24
23
  try:
25
24
  result = subprocess.run(
26
25
  ["git", "diff", "HEAD", "--", file_path],
27
- capture_output=True,
26
+ check=False, capture_output=True,
28
27
  text=True,
29
28
  timeout=5,
30
29
  )
@@ -34,7 +33,7 @@ def get_git_diff_added_lines(file_path: str) -> set[str]:
34
33
 
35
34
  staged_result = subprocess.run(
36
35
  ["git", "diff", "--staged", "--", file_path],
37
- capture_output=True,
36
+ check=False, capture_output=True,
38
37
  text=True,
39
38
  timeout=5,
40
39
  )
@@ -51,7 +50,7 @@ def is_new_file(file_path: str) -> bool:
51
50
  try:
52
51
  result = subprocess.run(
53
52
  ["git", "ls-files", "--others", "--exclude-standard", "--", file_path],
54
- capture_output=True,
53
+ check=False, capture_output=True,
55
54
  text=True,
56
55
  timeout=5,
57
56
  )
@@ -7,6 +7,12 @@ PreToolUse hooks that deny (block) tool calls when a rule is violated. The main
7
7
  | Directory | Role |
8
8
  |---|---|
9
9
  | `config/` | Shared constants for the verified-commit gate family (`verified_commit_constants.py`) |
10
+ | `tdd_enforcer_parts/` | Concern modules the `tdd_enforcer.py` entry hook wires together: path classification, content analysis, candidate-path resolution, freshness, git-tracking restore detection, decisions, and constants |
11
+ | `claude_md_orphan_file_blocker_parts/` | Concern modules the `claude_md_orphan_file_blocker.py` entry hook wires together: reference extraction, subtree scan, scan plan, decision, and constants |
12
+ | `package_inventory_stale_blocker_parts/` | Concern modules the `package_inventory_stale_blocker.py` entry hook wires together: inventory detection, decision, and constants |
13
+ | `inventory_intent_records/` | The shared per-session pending-intent store both inventory blockers read to break the file/row add-order deadlock |
14
+ | `pii_prevention_blocker_parts/` | Concern modules the `pii_prevention_blocker.py` entry hook wires together: per-repository scan exemption, the per-repository allowlist of exact values a commit may carry, and resolving the repository a commit command targets (with `config/` for the resolution deny-message constants) |
15
+ | `tests/` | pytest suite for `pii_prevention_blocker.py` repository resolution and the `pii_prevention_blocker_parts` modules |
10
16
 
11
17
  ## Core enforcer
12
18
 
@@ -83,7 +89,7 @@ The check modules it calls are the `code_rules_<concern>.py` files below.
83
89
  | `package_inventory_stale_blocker.py` | PreToolUse (Write) | A new production code file created in a directory whose `README.md`/`CLAUDE.md` inventory (or a parent skill's `SKILL.md` Layout table mapping the `scripts/` subdirectory) names two or more sibling files but no entry for the new file |
84
90
  | `pii_commit_command.py` | library | Token-aware git-commit detection reused by `pii_prevention_blocker.py` |
85
91
  | `pii_payload_scan.py` | library | Write/Edit and durable post-body PII evaluation reused by `pii_prevention_blocker.py` |
86
- | `pii_prevention_blocker.py` | PreToolUse (Write/Edit/MultiEdit/Bash/PowerShell/MCP GitHub) | Content that carries high-confidence personal data or secrets (real emails, home-dir paths, private IPs, credential material) on write, durable GitHub posts, or staged commit paths |
92
+ | `pii_prevention_blocker.py` | PreToolUse (Write/Edit/MultiEdit/Bash/PowerShell/MCP GitHub) | Entry hook — content that carries high-confidence personal data or secrets (real emails, home-dir paths, private IPs, credential material) on write, durable GitHub posts, or staged commit paths; resolves the staged-commit repository from the command it gates (via `pii_prevention_blocker_parts`), not the session working directory |
87
93
  | `pii_scanner.py` | library | Pure text scanners shared by `pii_prevention_blocker.py` |
88
94
  | `plain_language_blocker.py` | PreToolUse (Write/Edit/AskUserQuestion) | Heavy or jargon words in user-facing prose |
89
95
  | `pr_converge_bugteam_enforcer.py` | PreToolUse | Enforces that bugteam runs in parallel with bugbot in pr-converge loops |
@@ -80,7 +80,7 @@ def get_branch_at_directory(working_dir: str | None = None) -> str | None:
80
80
  try:
81
81
  completed_process = subprocess.run(
82
82
  ["git", "rev-parse", "--abbrev-ref", "HEAD"],
83
- capture_output=True,
83
+ check=False, capture_output=True,
84
84
  text=True,
85
85
  timeout=GIT_COMMAND_TIMEOUT_SECONDS,
86
86
  cwd=working_dir,
@@ -99,7 +99,7 @@ def is_protected_repo(working_dir: str | None = None) -> bool:
99
99
  try:
100
100
  completed_process = subprocess.run(
101
101
  ["git", "remote", "get-url", "origin"],
102
- capture_output=True,
102
+ check=False, capture_output=True,
103
103
  text=True,
104
104
  timeout=GIT_COMMAND_TIMEOUT_SECONDS,
105
105
  cwd=working_dir,