claude-dev-env 2.3.0 → 2.5.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 (290) hide show
  1. package/CLAUDE.md +53 -48
  2. package/_shared/pr-loop/scripts/_claude_permissions_common.py +84 -0
  3. package/_shared/pr-loop/scripts/code_rules_gate.py +4 -2
  4. package/_shared/pr-loop/scripts/grant_project_claude_permissions.py +306 -306
  5. package/_shared/pr-loop/scripts/pr_loop_shared_constants/claude_permissions_constants.py +44 -0
  6. package/_shared/pr-loop/scripts/pr_loop_shared_constants/copilot_quota_constants.py +24 -24
  7. package/_shared/pr-loop/scripts/pr_loop_shared_constants/stale_worktree_rule_sweep_constants.py +107 -107
  8. package/_shared/pr-loop/scripts/revoke_project_claude_permissions.py +290 -48
  9. package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +42 -2
  10. package/_shared/pr-loop/scripts/tests/test_claude_permissions_constants.py +36 -0
  11. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +100 -1
  12. package/_shared/pr-loop/scripts/tests/test_fix_hookspath.py +497 -497
  13. package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +311 -2
  14. package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep.py +301 -301
  15. package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep_constants.py +85 -85
  16. package/_shared/pr-loop/worker-spawn.md +1 -1
  17. package/agents/CLAUDE.md +3 -1
  18. package/agents/caveman.md +0 -1
  19. package/agents/clasp-deployment-orchestrator.md +0 -1
  20. package/agents/clean-coder.md +0 -1
  21. package/agents/code-advisor.md +0 -1
  22. package/agents/code-quality-agent.md +1 -2
  23. package/agents/code-verifier.md +3 -4
  24. package/agents/deep-research.md +0 -1
  25. package/agents/docs-agent.md +0 -1
  26. package/agents/git-commit-crafter.md +0 -1
  27. package/agents/issue-tracker.md +42 -0
  28. package/agents/plan-packet-validator.md +0 -1
  29. package/agents/pr-description-writer.md +0 -1
  30. package/agents/skill-writer-agent.md +84 -0
  31. package/agents/test_agent_frontmatter.py +67 -18
  32. package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +105 -3
  33. package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +29 -13
  34. package/bin/CLAUDE.md +68 -5
  35. package/bin/ever-shipped-skills.mjs +1 -0
  36. package/bin/install-constants.mjs +88 -0
  37. package/bin/install.mjs +1138 -114
  38. package/bin/install.prune.test.mjs +869 -19
  39. package/bin/install.test.mjs +906 -2
  40. package/commands/implement.md +1 -1
  41. package/commands/right-size.md +1 -1
  42. package/docs/CLAUDE.md +2 -0
  43. package/docs/host-pool-health-monitor.md +102 -0
  44. package/docs/references/CLAUDE.md +5 -2
  45. package/docs/references/advisor-tool.md +13 -0
  46. package/docs/references/code-review-enforcement.md +107 -0
  47. package/docs/references/team-advisor-skill.md +14 -0
  48. package/docs/wsl-docker-cowork-starter-matrix.md +89 -0
  49. package/hooks/blocking/CLAUDE.md +9 -1
  50. package/hooks/blocking/code_review_enforcement_config_bootstrap.py +53 -0
  51. package/hooks/blocking/code_review_gate_deny.py +74 -0
  52. package/hooks/blocking/code_review_pr_create_gate.py +198 -0
  53. package/hooks/blocking/code_review_push_gate.py +145 -0
  54. package/hooks/blocking/code_review_stamp_directory_write_blocker.py +348 -0
  55. package/hooks/blocking/code_review_stamp_store.py +233 -0
  56. package/hooks/blocking/code_review_stamp_write_blocker_parts/__init__.py +7 -0
  57. package/hooks/blocking/code_review_stamp_write_blocker_parts/conftest.py +15 -0
  58. package/hooks/blocking/code_review_stamp_write_blocker_parts/obfuscated_stamp_path_reference.py +212 -0
  59. package/hooks/blocking/code_review_stamp_write_blocker_parts/split_directory_change_into_stamp.py +138 -0
  60. package/hooks/blocking/code_review_stamp_write_blocker_parts/test_obfuscated_stamp_path_reference.py +49 -0
  61. package/hooks/blocking/code_review_stamp_write_blocker_parts/test_split_directory_change_into_stamp.py +38 -0
  62. package/hooks/blocking/code_verifier_spawn_preflight_gate.py +39 -27
  63. package/hooks/blocking/config/__init__.py +5 -5
  64. package/hooks/blocking/config/code_review_enforcement_constants.py +113 -0
  65. package/hooks/blocking/config/test_code_review_enforcement_constants.py +113 -0
  66. package/hooks/blocking/config/verified_commit_constants.py +160 -155
  67. package/hooks/blocking/conftest.py +2 -0
  68. package/hooks/blocking/convergence_gate_blocker.py +112 -23
  69. package/hooks/blocking/destructive_command_blocker.py +19 -6
  70. package/hooks/blocking/orchestrator_refresh_reschedule_gate.py +256 -0
  71. package/hooks/blocking/pr_description_proof_of_work.py +52 -34
  72. package/hooks/blocking/pre_tool_use_dispatcher.py +24 -24
  73. package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +4 -1
  74. package/hooks/blocking/test_code_review_enforcement_config_bootstrap.py +62 -0
  75. package/hooks/blocking/test_code_review_gate_deny.py +54 -0
  76. package/hooks/blocking/test_code_review_pr_create_gate.py +199 -0
  77. package/hooks/blocking/test_code_review_push_gate.py +205 -0
  78. package/hooks/blocking/test_code_review_stamp_directory_write_blocker.py +199 -0
  79. package/hooks/blocking/test_code_review_stamp_store.py +205 -0
  80. package/hooks/blocking/test_code_verifier_spawn_preflight_gate.py +124 -2
  81. package/hooks/blocking/test_convergence_gate_blocker.py +153 -5
  82. package/hooks/blocking/test_destructive_command_blocker.py +1 -1
  83. package/hooks/blocking/test_destructive_command_blocker_deny_mode.py +45 -0
  84. package/hooks/blocking/test_orchestrator_refresh_reschedule_gate.py +231 -0
  85. package/hooks/blocking/test_pr_description_proof_of_work.py +151 -0
  86. package/hooks/blocking/test_pre_tool_use_dispatcher.py +17 -8
  87. package/hooks/blocking/test_verdict_directory_write_blocker.py +808 -808
  88. package/hooks/blocking/test_verification_verdict_store.py +974 -903
  89. package/hooks/blocking/test_verified_commit_gate.py +581 -581
  90. package/hooks/blocking/test_verified_commit_message_accuracy_blocker.py +131 -131
  91. package/hooks/blocking/test_volatile_path_in_post_blocker.py +114 -2
  92. package/hooks/blocking/verdict_directory_write_blocker.py +687 -687
  93. package/hooks/blocking/verification_verdict_store.py +1039 -1014
  94. package/hooks/blocking/verified_commit_gate_parts/gated_invocations.py +29 -17
  95. package/hooks/blocking/verified_commit_gate_parts/tests/test_gated_invocations.py +35 -0
  96. package/hooks/blocking/verified_commit_message_accuracy_blocker.py +167 -167
  97. package/hooks/blocking/verifier_verdict_minter.py +280 -280
  98. package/hooks/blocking/volatile_path_in_post_blocker.py +69 -8
  99. package/hooks/git-hooks/git_hooks_constants/__init__.py +6 -0
  100. package/hooks/git-hooks/pre_push.py +89 -2
  101. package/hooks/git-hooks/test_pre_push.py +128 -0
  102. package/hooks/hooks.json +26 -1
  103. package/hooks/hooks_constants/CLAUDE.md +3 -1
  104. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +8 -0
  105. package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -0
  106. package/hooks/hooks_constants/code_verifier_spawn_preflight_gate_constants.py +26 -11
  107. package/hooks/hooks_constants/convergence_gate_blocker_constants.py +20 -3
  108. package/hooks/hooks_constants/destructive_command_segment_constants.py +3 -1
  109. package/hooks/hooks_constants/enter_worktree_prefetch_constants.py +18 -18
  110. package/hooks/hooks_constants/orchestrator_refresh_reschedule_gate_constants.py +48 -0
  111. package/hooks/hooks_constants/pr_description_proof_of_work_constants.py +0 -4
  112. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +4 -0
  113. package/hooks/hooks_constants/pyproject_config_discovery_constants.py +16 -0
  114. package/hooks/hooks_constants/ruff_integration_constants.py +16 -0
  115. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +24 -0
  116. package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +6 -0
  117. package/hooks/hooks_constants/volatile_path_in_post_blocker_constants.py +8 -1
  118. package/hooks/lifecycle/enter_worktree_origin_prefetch.py +163 -146
  119. package/hooks/lifecycle/test_enter_worktree_origin_prefetch.py +185 -178
  120. package/hooks/pyproject.toml +1 -0
  121. package/hooks/validators/CLAUDE.md +2 -0
  122. package/hooks/validators/config/__init__.py +0 -0
  123. package/hooks/validators/config/directory_exemption_constants.py +183 -0
  124. package/hooks/validators/config/test_directory_exemption_constants.py +21 -0
  125. package/hooks/validators/conftest.py +4 -0
  126. package/hooks/validators/mypy_integration.py +63 -50
  127. package/hooks/validators/pyproject_config_discovery.py +101 -0
  128. package/hooks/validators/ruff_integration.py +257 -25
  129. package/hooks/validators/run_all_validators.py +223 -19
  130. package/hooks/validators/test_directory_exemption_constants.py +185 -0
  131. package/hooks/validators/test_mypy_integration.py +32 -0
  132. package/hooks/validators/test_pyproject_config_discovery.py +94 -0
  133. package/hooks/validators/test_python_antipattern_checks.py +110 -5
  134. package/hooks/validators/test_ruff_integration.py +160 -2
  135. package/hooks/validators/test_run_all_validators.py +140 -68
  136. package/hooks/validators/test_run_all_validators_config_discovery.py +123 -0
  137. package/hooks/validators/test_run_all_validators_pretooluse.py +159 -1
  138. package/package.json +10 -2
  139. package/rules/CLAUDE.md +1 -0
  140. package/rules/docstring-prose-matches-implementation.md +45 -67
  141. package/rules/durable-post-artifacts.md +7 -0
  142. package/rules/state-what-is.md +25 -0
  143. package/rules/verified-commit-gate-skip.md +1 -1
  144. package/scripts/CLAUDE.md +1 -0
  145. package/scripts/Capture-PoolHealth.ps1 +410 -0
  146. package/scripts/_code_review_test_support.py +404 -0
  147. package/scripts/claude_chain_runner.py +141 -1
  148. package/scripts/codec_forwarding_test_support.py +83 -0
  149. package/scripts/conftest.py +23 -0
  150. package/scripts/dev_env_scripts_constants/CLAUDE.md +2 -2
  151. package/scripts/dev_env_scripts_constants/claude_chain_constants.py +53 -1
  152. package/scripts/dev_env_scripts_constants/code_review_constants.py +129 -12
  153. package/scripts/dev_env_scripts_constants/test_code_review_constants.py +55 -0
  154. package/scripts/invoke_code_review.py +550 -38
  155. package/scripts/resolve_worker_spawn.py +626 -619
  156. package/scripts/spawn_grok_batch.py +672 -672
  157. package/scripts/test_claude_chain_runner.py +131 -0
  158. package/scripts/test_invoke_code_review_chain.py +70 -0
  159. package/scripts/test_invoke_code_review_cli.py +192 -0
  160. package/scripts/test_invoke_code_review_codec.py +77 -0
  161. package/scripts/test_invoke_code_review_contract.py +256 -0
  162. package/scripts/test_invoke_code_review_git.py +123 -0
  163. package/scripts/test_invoke_code_review_mode.py +99 -0
  164. package/scripts/test_resolve_worker_spawn.py +1014 -1014
  165. package/scripts/test_resolve_worker_spawn_codec.py +101 -0
  166. package/skills/CLAUDE.md +2 -0
  167. package/skills/auditing-claude-config/SKILL.md +114 -114
  168. package/skills/autoconverge/SKILL.md +427 -421
  169. package/skills/autoconverge/reference/convergence.md +26 -4
  170. package/skills/autoconverge/reference/multi-pr.md +6 -1
  171. package/skills/autoconverge/reference/stop-conditions.md +16 -10
  172. package/skills/autoconverge/workflow/CLAUDE.md +1 -0
  173. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +4 -4
  174. package/skills/autoconverge/workflow/converge.codex-gate.test.mjs +175 -3
  175. package/skills/autoconverge/workflow/converge.contract.test.mjs +1263 -1244
  176. package/skills/autoconverge/workflow/converge.mjs +191 -8
  177. package/skills/autoconverge/workflow/converge.p2-advance.test.mjs +202 -0
  178. package/skills/autoconverge/workflow/converge_multi.mjs +7 -3
  179. package/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +5 -0
  180. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a11d903476b803493.jsonl +2 -2
  181. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a26213978adeef6fb.jsonl +2 -2
  182. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a3def0d15ed9d9110.jsonl +2 -2
  183. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a41f41b1b708ee3b7.jsonl +2 -2
  184. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a758b880abecc3ff7.jsonl +2 -2
  185. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a8897b89656b1bd16.jsonl +2 -2
  186. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-abd463d744a1437bc.jsonl +2 -2
  187. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ad19d027ae8ee1816.jsonl +2 -2
  188. package/skills/autoconverge/workflow/fixtures/wf_run/workflows/wf_881252e6-700.json +265 -265
  189. package/skills/closeout/SKILL.md +33 -50
  190. package/skills/codex-review/scripts/codex_review_scripts_constants/run_constants.py +8 -0
  191. package/skills/codex-review/scripts/run_codex_review.py +233 -1
  192. package/skills/codex-review/scripts/test_run_codex_review.py +189 -0
  193. package/skills/condensing-instructions/SKILL.md +81 -0
  194. package/skills/copilot-review/SKILL.md +119 -119
  195. package/skills/e-code-review/SKILL.md +52 -0
  196. package/skills/e-code-review/reference/fix.md +54 -0
  197. package/skills/e-code-review/reference/loop.md +43 -0
  198. package/skills/e-code-review/reference/low.md +57 -0
  199. package/skills/e-code-review/reference/medium.md +153 -0
  200. package/skills/e-code-review/reference/xhigh.md +182 -0
  201. package/skills/e-simplify/SKILL.md +97 -0
  202. package/skills/fresh-branch/CLAUDE.md +2 -0
  203. package/skills/fresh-branch/SKILL.md +2 -0
  204. package/skills/fresh-branch/scripts/create_fresh_branch.py +78 -180
  205. package/skills/fresh-branch/scripts/fresh_branch_git_commands.py +285 -0
  206. package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +1 -0
  207. package/skills/fresh-branch/scripts/pytest.ini +4 -0
  208. package/skills/fresh-branch/scripts/test_create_fresh_branch.py +98 -0
  209. package/skills/fresh-branch/scripts/test_fresh_branch_git_commands.py +310 -0
  210. package/skills/issue-tracker/SKILL.md +92 -0
  211. package/skills/issue-tracker/reference/epic-and-sub-issue-model.md +55 -0
  212. package/skills/issue-tracker/reference/handoff-schema.md +64 -0
  213. package/skills/issue-tracker/reference/operation-matrix.md +41 -0
  214. package/skills/orchestrator/SKILL.md +162 -21
  215. package/skills/orchestrator/scripts/status_gate.py +625 -0
  216. package/skills/orchestrator/scripts/status_gate_constants/__init__.py +1 -0
  217. package/skills/orchestrator/scripts/status_gate_constants/config/__init__.py +1 -0
  218. package/skills/orchestrator/scripts/status_gate_constants/config/constants.py +47 -0
  219. package/skills/orchestrator/scripts/test_status_gate.py +439 -0
  220. package/skills/orchestrator-refresh/SKILL.md +110 -35
  221. package/skills/plan-to-pr/SKILL.md +155 -0
  222. package/skills/plan-to-pr/reference/final-validation-tasks.md +15 -0
  223. package/skills/plan-to-pr/reference/model-routing.md +36 -0
  224. package/skills/plan-to-pr/reference/packet-contract.md +43 -0
  225. package/skills/plan-to-pr/reference/packet-schema.json +57 -0
  226. package/skills/plan-to-pr/reference/process-inventory.md +22 -0
  227. package/skills/plan-to-pr/reference/review-loop.md +33 -0
  228. package/skills/plan-to-pr/reference/run-record.schema.json +27 -0
  229. package/skills/plan-to-pr/reference/self-audit-tasks.md +15 -0
  230. package/skills/plan-to-pr/reference/task-seeds.md +14 -0
  231. package/skills/plan-to-pr/reference/task-ticket.md +38 -0
  232. package/skills/plan-to-pr/scripts/config/__init__.py +1 -0
  233. package/skills/plan-to-pr/scripts/config/constants.py +193 -0
  234. package/skills/plan-to-pr/scripts/create_packet.py +173 -0
  235. package/skills/plan-to-pr/scripts/test_create_packet.py +102 -0
  236. package/skills/plan-to-pr/scripts/test_validate_packet.py +256 -0
  237. package/skills/plan-to-pr/scripts/test_validate_protocol.py +135 -0
  238. package/skills/plan-to-pr/scripts/test_validate_run.py +158 -0
  239. package/skills/plan-to-pr/scripts/validate_packet.py +655 -0
  240. package/skills/plan-to-pr/scripts/validate_protocol.py +622 -0
  241. package/skills/plan-to-pr/scripts/validate_run.py +173 -0
  242. package/skills/plan-to-pr/test_skill_contract.py +207 -0
  243. package/skills/plan-to-pr/test_task_ticket_contract.py +151 -0
  244. package/skills/pr-converge/SKILL.md +472 -469
  245. package/skills/pr-converge/reference/examples.md +3 -3
  246. package/skills/pr-converge/reference/fix-protocol.md +1 -1
  247. package/skills/pr-converge/reference/ground-rules.md +7 -4
  248. package/skills/pr-converge/reference/multi-pr-orchestration.md +4 -1
  249. package/skills/pr-converge/reference/per-tick.md +5 -5
  250. package/skills/pr-converge/reference/progress-checklist.md +1 -1
  251. package/skills/pr-converge/scripts/check_convergence_gates.py +279 -279
  252. package/skills/pr-converge/scripts/test_check_convergence_codex.py +507 -507
  253. package/skills/pr-converge/scripts/test_check_convergence_gates.py +84 -84
  254. package/skills/pr-converge/test_step5_host_branch.py +1 -1
  255. package/skills/pr-fix-protocol/SKILL.md +1 -1
  256. package/skills/privacy-hygiene/SKILL.md +68 -68
  257. package/skills/prototype/SKILL.md +86 -0
  258. package/skills/prototype/reference/honest-limitations.md +23 -0
  259. package/skills/prototype/reference/promotion-tasks.md +23 -0
  260. package/skills/prototype/scripts/build_sandbox_settings.py +249 -0
  261. package/skills/prototype/scripts/conftest.py +15 -0
  262. package/skills/prototype/scripts/launch_sandbox.py +205 -0
  263. package/skills/prototype/scripts/probe_sandbox_safety.py +311 -0
  264. package/skills/prototype/scripts/prototype_scripts_constants/__init__.py +1 -0
  265. package/skills/prototype/scripts/prototype_scripts_constants/config/__init__.py +0 -0
  266. package/skills/prototype/scripts/prototype_scripts_constants/config/build_sandbox_settings_constants.py +41 -0
  267. package/skills/prototype/scripts/prototype_scripts_constants/config/launch_sandbox_constants.py +23 -0
  268. package/skills/prototype/scripts/prototype_scripts_constants/config/probe_sandbox_safety_constants.py +45 -0
  269. package/skills/prototype/scripts/prototype_scripts_constants/config/prototype_common_constants.py +10 -0
  270. package/skills/prototype/scripts/test_build_sandbox_settings.py +275 -0
  271. package/skills/prototype/scripts/test_launch_sandbox.py +303 -0
  272. package/skills/prototype/scripts/test_probe_sandbox_safety.py +284 -0
  273. package/skills/prototype/workflows/promotion.md +27 -0
  274. package/skills/prototype/workflows/sandbox.md +35 -0
  275. package/skills/release-notes-html/SKILL.md +164 -0
  276. package/skills/skill-builder/CLAUDE.md +3 -3
  277. package/skills/skill-builder/SKILL.md +5 -5
  278. package/skills/skill-builder/references/CLAUDE.md +1 -1
  279. package/skills/skill-builder/references/delegation-map.md +3 -3
  280. package/skills/skill-builder/references/description-field.md +1 -1
  281. package/skills/skill-builder/references/skill-modularity.md +2 -3
  282. package/skills/skill-builder/workflows/CLAUDE.md +1 -1
  283. package/skills/skill-builder/workflows/improve-skill.md +1 -1
  284. package/skills/skill-builder/workflows/new-skill.md +2 -2
  285. package/skills/task-build/CLAUDE.md +8 -7
  286. package/skills/task-build/SKILL.md +16 -8
  287. package/skills/task-build/reference/tool-routing.md +19 -0
  288. package/skills/team-advisor/SKILL.md +2 -2
  289. package/scripts/test_invoke_code_review.py +0 -672
  290. package/skills/closeout/reference/issue-body-templates.md +0 -108
@@ -9,6 +9,19 @@
9
9
  - A docstring lists three responsibilities; only one is implemented, the other two live elsewhere.
10
10
  - A docstring describes step ordering `A then B`; the body does `B then A`.
11
11
  - A docstring references a sentinel marker (`# pragma: no-tdd-gate`) or filename shape (`test_code-rules-enforcer.py`) that the module body and the repo's naming convention do not use.
12
+ - A workflow gate-outcome flag's schema or doc prose calls it a bypass, but the branch handling that outcome sets a blocker and `break`s to hold the PR in draft.
13
+
14
+ ## Division of labor
15
+
16
+ This file is the **single thick source** for Category O judgment (sub-buckets O1–O9, the write-time gate inventory, free-form checklists, and worked examples).
17
+
18
+ | Surface | Role |
19
+ |---|---|
20
+ | `packages/claude-dev-env/rules/docstring-prose-matches-implementation.md` | Always-on write-time policy: the policy sentence, a compact checklist a writer applies at Write/Edit, and a pointer here. |
21
+ | **This rubric** | On-demand thick home. The code-quality agent loads it per category. Holds every judgment standard, gate inventory, and worked example. |
22
+ | `packages/claude-dev-env/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md` | Variant C audit template: source-material slots, forced-exhaustion protocol, adversarial probes, cross-bucket questions, output shape, and a PR worked example. Points here for the judgment standard. |
23
+
24
+ Plainness for a general developer (diagram-first shape) also lives under O9; the write-time companion rule for that slice is `packages/claude-dev-env/rules/plain-illustrative-docstrings.md`.
12
25
 
13
26
  **Companion reference:** see `../source-material-section-types.md`.
14
27
 
@@ -20,18 +33,105 @@ Decomposition is by the **kind of docstring claim** that needs to be cross-check
20
33
 
21
34
  | ID | Axis name | Concrete checks |
22
35
  |---|---|---|
23
- | O1 | Module-level responsibility verbs | A module docstring uses verbs (`detects`, `validates`, `enforces`, `recovers`, `parses`, `routes`) — every claimed responsibility is implemented by an exported symbol in the same module. Symbols absent from the module body should not appear as this module's responsibilities. |
36
+ | O1 | Module-level responsibility verbs | A module docstring uses verbs (`detects`, `validates`, `enforces`, `recovers`, `parses`, `routes`) — every claimed responsibility is implemented by an exported symbol in the same module. Symbols absent from the module body should not appear as this module's responsibilities. A module whose one-line docstring scopes its contents to user-facing text (`User-facing strings: CLI flag names, help text, and log messages`) also names every category of constant the body holds. When the body also defines serialization field keys (`JSONL_FIELD_*`), run-metadata schema keys (`RUN_METADATA_CLI_ARG_KEY_*`), or runtime config (`STDOUT_ENCODING`, `MAIN_LOGGING_FORMAT_STRING`), the strings-only summary under-describes the module. Broaden the summary to name the data-schema keys and runtime config. The `check_module_docstring_scope_omits_data_schema_constants` gate blocks this drift at Write/Edit time when the summary claims a user-facing-text scope and names no data-schema or runtime-config category. |
24
37
  | O2 | Fixture docstring vs sibling-test behavior | An autouse / module-scope fixture docstring asserts an invariant (`readability is disabled`, `network is mocked`, `tmp_path is empty`). No sibling test in the same module explicitly opts out of the invariant. |
25
38
  | O3 | Predicate-name and -docstring vs body breadth | A boolean helper's name and docstring promise a narrow predicate. Walk the body's branches: every branch's `return True` path is consistent with the promised name. Bodies that accept inputs broader than the name (`_dir_value_resolves_to_shared_temp` also accepting HOME/TMP env-derived paths) are O3 findings. |
26
39
  | O4 | Step-ordering narrative | A docstring describes processing as `A then B then C`. Walk the body and confirm the call order matches. Mismatched order is an O4 finding regardless of whether the final output is the same. A docstring step enumeration that names the body's linear steps but omits a corrective workflow step the body guards inside an `if`/`elif` branch (`if not await cancel_and_reinitiate_update(...): return`) is also an O4 finding: the reader trusts the step list to be complete and misses the conditional path. The branch-guarded-dispatch shape of this drift — a docstring that names two or more linear-step callees while the body guards a two-or-more-token dispatch callee inside a branch whose name the prose never spells out — is gated deterministically at Write/Edit time by `check_docstring_step_enumeration_dispatch_coverage` (`packages/claude-dev-env/hooks/blocking/code_rules_docstrings.py`), so the audit lane focuses on the step-ordering shapes the gate cannot match (re-ordered steps, plain unguarded steps the prose omits). |
27
40
  | O5 | Named-sentinel / filename references | A docstring names a sentinel marker, environment variable, filename, or magic string. Confirm the named token actually exists in the module body or in the repo's naming convention. |
28
- | O6 | Free-form `Args:`-adjacent claims | A docstring's `Returns:` / `Raises:` / `Note:` / `Example:` sections make claims (`returns shared-temp only`, `raises ValueError on missing key`). Verify each claim against the body. When a docstring enumerates the inputs a body counts (a "field counts as read when ..." list, a list of conditions treated as a match, a list of cases the body skips), list every union member and every suppressor the body applies (`read_names = a | b | c`, each early-return guard) and confirm each appears in the prose enumeration. A union member or suppressor the body applies but the prose omits is an O6 finding. When a docstring sentence excludes a named category of input from what the function flags (`X are not dispatch steps`, `Y is not a match`), confirm the axis the prose excludes on is the axis the body's branch condition actually keys on. A body that flags a call when it sits inside an `If.test` guard, paired with prose that excludes by the call's receiver shape (`method-on-local calls inside a branch are not dispatch steps`), is an O6 finding: a guarded method-on-local call is flagged even though the prose lists it as excluded — the exclusion is keyed to the wrong axis. The single-condition shared-fallback shape of this drift a summary that scopes a fallback call to one condition while the body routes to that same call from two or more early-return guards is gated deterministically at Write/Edit time by `check_docstring_fallback_branch_coverage`, so the audit lane focuses on the O6 shapes the gate cannot match. The exception-guard shape of this drift a docstring that promises a malformed payload `resolves to None` while a payload subscript (`payload["key"]`, `float(payload["key"])`) sits outside the try/except whose handler returns None, so a present-but-malformed payload raises rather than resolving to None is gated deterministically at Write/Edit time by `check_docstring_unguarded_malformed_payload_claim`, so the audit lane focuses on the wider Raises/None-on-failure claims the gate cannot match. A `Returns:` that names the mechanism, tool, or output format the function produces (`instructing a StructuredOutput summary`, `returns a YAML document`, `emits a JSON object`) matches the artifact the body actually builds: a prompt body that asks the agent to "Return strictly a JSON object" while the docstring claims it "instruct[s] a StructuredOutput" summary is an O6 finding, because the named tool appears nowhere in the emitted text. See `../../rules/docstring-prose-matches-implementation.md`. |
41
+ | O6 | Free-form `Args:`-adjacent claims | A docstring's `Returns:` / `Raises:` / `Note:` / `Example:` sections make claims (`returns shared-temp only`, `raises ValueError on missing key`). Verify each claim against the body. When a docstring enumerates the inputs a body counts (a "field counts as read when ..." list, a list of conditions treated as a match, a list of cases the body skips), list every union member and every suppressor the body applies (`read_names = a \| b \| c`, each early-return guard) and confirm each appears in the prose enumeration. A union member or suppressor the body applies but the prose omits is an O6 finding. When a docstring sentence excludes a named category of input from what the function flags (`X are not dispatch steps`, `Y is not a match`), confirm the axis the prose excludes on is the axis the body's branch condition actually keys on. A body that flags a call when it sits inside an `If.test` guard, paired with prose that excludes by the call's receiver shape (`method-on-local calls inside a branch are not dispatch steps`), is an O6 finding: a guarded method-on-local call is flagged even though the prose lists it as excluded — the exclusion is keyed to the wrong axis. A thin delegating method whose docstring names its actions and points at the home of the real body lists the same actions the delegated function's own summary lists; when an edit moves one action out of the delegated body, the same edit rewords both summaries (`check_docstring_delegation_summary_enumeration_drift`). A conditional bullet in the delegated prose also names every exception the body honorsthat conditional-completeness slice stays a judgment call for this lane. A `Returns:` that names the mechanism, tool, or output format the function produces (`instructing a StructuredOutput summary`, `returns a YAML document`, `emits a JSON object`) matches the artifact the body actually builds. A dataclass or `TypedDict` field documented in the class `Attributes:` block states what the field means for one record; when the code sets that field the same way for every record (a run-mode flag such as `is_dry_run = not is_execute`), the description states the run-mode meaning, not a per-record outcome (`check_docstring_field_runmode_outcome` covers the single-file shape). A workflow gate-outcome status flag whose in-code prose (a schema property `description`, an architecture `detail`/overview string) describes the outcome as bypassing or skipping matches the branch that handles it: when the code routes that outcome to a blocker (`blocker = ...; break`) that holds the PR in draft, prose that reads "skips without blocking" or "the gate is bypassed" for that outcome is an O6 finding. Many deterministic O6 shapes are gated at Write/Edit time — see **Write-time gate inventory** below — so the audit lane focuses on the free-form shapes the gates cannot match. |
29
42
  | O7 | Module-doc-vs-split-module after refactor | When a refactor moves a responsibility to a sibling module, the originating module's docstring and the receiving module's docstring both describe the home of that responsibility. A module docstring should describe only the responsibilities it owns. |
30
- | O8 | Companion-doc ordering/content vs producer | When a PR changes a producer function's ordering or union, read that skill's companion `SKILL.md` and sibling `.md` docs for any sentence naming the same produced artifact (a file path, a JSON key, a named list). A doc sentence that claims the artifact is `sorted` / `alphabetical` / `in sorted order`, or holds `just the at-risk names` / `only the current set`, while the producer merges stored names with new names and appends — preserving file order, not re-sorting the union — is an O8 finding on both counts (wrong order claim, hidden merged-in entries). The finding stands even when the PR diff never touched the `.md` file, because the behavior change orphaned the doc claim. See `../../rules/docstring-prose-matches-implementation.md`. |
43
+ | O8 | Companion-doc ordering/content vs producer | When a PR changes a producer function's ordering or union, read that skill's companion `SKILL.md` and sibling `.md` docs for any sentence naming the same produced artifact (a file path, a JSON key, a named list). A doc sentence that claims the artifact is `sorted` / `alphabetical` / `in sorted order`, or holds `just the at-risk names` / `only the current set`, while the producer merges stored names with new names and appends — preserving file order, not re-sorting the union — is an O8 finding on both counts (wrong order claim, hidden merged-in entries). The finding stands even when the PR diff never touched the `.md` file, because the behavior change orphaned the doc claim. A producer docstring asserting that no consumer reads its output yet (`producer-only artifact`, `no submission-run consumer reads it yet`) is the deterministic slice of this companion-doc producer/consumer drift (`check_docstring_no_consumer_claim`). The O6 pattern recurs at the companion-doc layer: when a PR routes a gate outcome to a blocker, read every skill's `SKILL.md` and reference `.md` docs that name that gate for a line still calling the outcome a bypass — an O8 finding. |
31
44
  | O9 | Python docstring plainness for a general developer | A changed module / class / public-function docstring's narrative prose — the summary and description before the first `Args:` / `Returns:` / `Raises:` / `Yields:` section — reads plainly and paints a concrete scene a general developer follows on first read. Flag a narrative that stacks abstract machinery nouns into a wall (`the SIGINT install/restore/installability check, the atexit terminal-record registration, and the interrupted-run finalizer`), that defines a thing by what it is not (`the non-promoter-specific machinery`), or that runs one sentence long while joining clauses with an em-dash or a semicolon. The diagram-first shape carries this best: a summary line, then a `::` example block or a doctest that shows a concrete input and its marked outcome, then a couple of short prose lines. The deterministic run-on mark is gated at Write/Edit time by `check_docstring_runon_sentence` in `code_rules_docstrings.py`, and a narrative that runs more than six prose lines with no such block is gated by `check_docstring_prose_wall_without_illustration` in the same module, so this lane carries the judgment the gates cannot: whether a stranger to the code pictures the moment, the input, and the outcome after one read, and whether the diagram truly illustrates — a real input, a marked outcome, an `ok:` / `flag:` contrast a reader learns from. See `../../rules/plain-illustrative-docstrings.md`. |
32
45
 
33
46
  ---
34
47
 
48
+ ## Write-time gate inventory
49
+
50
+ Deterministic slices of Category O that fire at Write/Edit. The free-form rest stays judgment (this rubric + the audit prompt).
51
+
52
+ ### Python — `packages/claude-dev-env/hooks/blocking/code_rules_docstrings.py`
53
+
54
+ | Gate | Drift it blocks |
55
+ |---|---|
56
+ | `check_docstring_args_match_signature` | `Args:` section parameter names vs the signature. |
57
+ | `check_docstring_delegation_summary_enumeration_drift` | Thin wrapper summary enumerates actions the same-named sibling summary omits (both save directions). |
58
+ | `check_docstring_names_absent_type_checking_gate` | Docstring names a `TYPE_CHECKING` gate or `type-checking-gate` helper family while no identifier in the module carries the `type_checking` marker. |
59
+ | `check_docstring_length_constant_superlative_vs_exact_gate` | Module docstring describes an integer `*_LENGTH` constant with a superlative or range word while every consumer compares `len(...)` with `==`/`!=` (exact-length gate). Scans the constant module's package tree. |
60
+ | `check_docstring_fallback_branch_coverage` | Summary scopes a fallback to one condition while the body routes to that fallback from two or more early-return guards. |
61
+ | `check_class_docstring_names_public_methods` | Class docstring is a single summary line while the class exposes two or more public methods the summary never names. |
62
+ | `check_docstring_no_consumer_claim` | Producer docstring asserts no consumer reads its output yet. |
63
+ | `check_docstring_returns_plural_cardinality` | `Returns:` names a dict-key prefix family with a plural noun while the returned dict holds exactly one key in that family. |
64
+ | `check_docstring_args_single_line_scope_vs_span` | `Args:` scopes a finding to one named line while the body scopes through a `range(...)` span-intersection. |
65
+ | `check_docstring_cardinal_count_matches_constant_family` | Docstring states a cardinal count of an outcome family and lists members, while the module references more members of the same `UPPER_SNAKE` family than the count names. Runs on test modules as well as production. |
66
+ | `check_docstring_raises_unraisable_largezipfile` | `Raises:` names `zipfile.LargeZipFile` while the writer opens with `allowZip64` at its default of True. |
67
+ | `check_docstring_no_network_claim_with_metadata_access` | Docstring promises a path returns without touching the network while the body calls path-metadata methods (`is_file`, `is_dir`, `exists`, `stat`, `lstat`). |
68
+ | `check_docstring_step_enumeration_dispatch_coverage` | Step-enumeration docstring omits a two-or-more-token dispatch step the body guards inside a branch. |
69
+ | `check_docstring_unguarded_malformed_payload_claim` | Docstring promises a malformed payload resolves to None while a payload subscript sits outside the try/except whose handler returns None. |
70
+ | `check_docstring_field_runmode_outcome` | `Attributes:` entry for a run-mode flag field (name carrying `dry_run`) whose description carries a per-record write-outcome phrase and no run-mode phrase. |
71
+ | `check_module_docstring_scope_omits_data_schema_constants` | Module summary claims user-facing-text scope while the body also defines data-schema or runtime-config constants. |
72
+ | `check_module_docstring_names_public_checks` | One-line check-registry module docstring omits a public `check_*` function the module dispatches. |
73
+ | `check_docstring_tuple_enumeration_match` | Docstring enumerates inline-code tokens that drift from the literal string tuple the body reads (a listed token the tuple lacks, or a tuple member the prose omits). |
74
+ | `check_docstring_punctuation_mark_enumeration_coverage` | Docstring names some marks of a punctuation-glyph tuple by their English names but omits one the tuple holds. |
75
+ | `check_docstring_no_inline_literal_claim` | Constants-module docstring asserts no literals appear inline in a companion file. |
76
+ | `check_docstring_names_undefined_constant` | Docstring names an `UPPER_SNAKE` constant identifier nothing in the module backs. |
77
+ | `check_docstring_runon_sentence` | Narrative run-on mark (O9 backstop). |
78
+ | `check_docstring_prose_wall_without_illustration` | Narrative longer than six prose lines with no `::` / doctest illustration (O9 backstop). |
79
+
80
+ ### JavaScript / `.mjs` — `packages/claude-dev-env/hooks/blocking/code_rules_imports_logging.py`
81
+
82
+ These are the `.mjs` slice of the same Category O standard. The Python AST docstring gates never inspect JavaScript source.
83
+
84
+ | Gate | Drift it blocks |
85
+ |---|---|
86
+ | `check_js_resume_task_enumeration_coverage` | A `spawn<Role>Agent` JSDoc enumerates sibling `resume<Role>Agent` resume tasks and omits a `task === '<name>'` branch the resume body dispatches on. |
87
+ | `check_js_returns_object_schemaless_branch` | `@returns {Promise<object>}` JSDoc whose body returns the same agent-spawn helper both with a `schema` options object and without one (schema-less branch resolves to a transcript string). |
88
+ | `check_js_sibling_return_object_key_drift` | A `return { ... }` object literal whose key set misses exactly one key of a sibling return in the same function or module scope. Discriminated-union variants and two-or-more-key exit shapes are left alone. |
89
+ | `check_js_bare_flag_return_directive` | A `return <name>: true`/`false` prose directive anywhere in the file that repeats a status flag a stated full-result contract rules out (no proximity or ordering check between the two). |
90
+
91
+ ---
92
+
93
+ ## Free-form judgment checklist (write time and audit)
94
+
95
+ Read the body and the docstring side by side. Apply each check that matches the prose. When the body changes the set of behaviors it applies, the same edit updates the prose enumeration.
96
+
97
+ - **Read-source / match-source unions.** A body that computes `read_names = a | b | c` (or any union of "what counts") names each union member in the prose enumeration.
98
+ - **Suppressor / skip lists.** A body with several early returns that suppress the check names each suppressor in the prose.
99
+ - **Shared fallback routes.** A summary that scopes a fallback call to one condition names every condition that reaches that call. Gated form: `check_docstring_fallback_branch_coverage`.
100
+ - **Step order.** A docstring that says `A then B then C` matches the call order in the body. A step enumeration that names the body's linear steps also names every corrective step the body guards inside an `if`/`elif` branch. Gated form: `check_docstring_step_enumeration_dispatch_coverage`.
101
+ - **Delegation pointer summaries.** A thin delegating method whose docstring names its actions and points at the home of the real body lists the same actions the delegated function's own summary lists. Gated form: `check_docstring_delegation_summary_enumeration_drift`. A conditional bullet in the delegated prose also names every exception the body honors — judgment for this lane.
102
+ - **JS/`.mjs` resume-task, `@returns` object, sibling return keys, bare-flag directives.** See the JavaScript gate inventory above.
103
+ - **Returns-clause cardinality.** A `Returns:` clause that names a dict-key prefix family with a plural noun matches the count of keys in that family in the returned dict literal. Gated form: `check_docstring_returns_plural_cardinality`.
104
+ - **Length-constant superlative vs exact gate.** A module docstring that describes an integer `*_LENGTH` constant with a superlative or range word matches how the code consumes the constant. Gated form: `check_docstring_length_constant_superlative_vs_exact_gate`.
105
+ - **Args single-line scope vs span body.** An `Args:` entry that scopes a finding to one named line matches the line breadth the body scopes by. Gated form: `check_docstring_args_single_line_scope_vs_span`.
106
+ - **Cardinal-count enumerations.** A docstring that states a count of an outcome family and lists those members names every member of that family the module references. Gated form: `check_docstring_cardinal_count_matches_constant_family`.
107
+ - **Raises-clause reachability for `LargeZipFile`.** A `Raises:` clause that names `zipfile.LargeZipFile` matches a writer the body opens with ZIP64 forbidden. Gated form: `check_docstring_raises_unraisable_largezipfile`.
108
+ - **Module summary scope versus data-schema constants.** A module whose one-line docstring scopes its contents to user-facing text names every category of constant the body holds. Gated form: `check_module_docstring_scope_omits_data_schema_constants`.
109
+ - **Field meaning: run mode versus per record.** A dataclass or `TypedDict` field documented in the class `Attributes:` block states what the field means for one record. When the code sets that field the same way for every record, the description states the run-mode meaning. Gated form: `check_docstring_field_runmode_outcome` (single-file shape); assignment in another module stays judgment.
110
+ - **Predicate breadth.** A boolean helper whose prose promises a narrow check accepts only the inputs the prose names — no broader input class the name and prose do not mention.
111
+ - **Exclusion-clause distinguisher.** A docstring sentence that says a named category of input "are not" / "is not" the thing the function flags keys the exclusion to the same axis the body's classification keys on. Read the body's actual branch condition, then state the exclusion on that same axis.
112
+ - **Companion-doc ordering and content claims.** A `SKILL.md` (or sibling `.md`) sentence that names a produced artifact and claims its order or its content matches the producer function's docstring and body for that same artifact. The two move together in one commit, even when the producer edit does not touch the `.md` file.
113
+ - **Gate-outcome status flags.** A workflow gate outcome the body routes to a blocker (`blocker = ...; break`) reads as blocked in every in-code prose string (a schema `description`, an architecture `detail`/overview string) and companion doc, never as a bypass — judgment for this lane.
114
+ - **TYPE_CHECKING gate claim vs code.** A docstring that names a `TYPE_CHECKING` gate-detection step matches a module whose code handles TYPE_CHECKING. Gated form: `check_docstring_names_absent_type_checking_gate`.
115
+
116
+ ---
117
+
118
+ ## Worked example (union enumeration)
119
+
120
+ A `@dataclass` dead-field check builds its set of "field counts as read" sources by union:
121
+
122
+ ```python
123
+ read_names = (
124
+ attribute_read_names
125
+ | dynamic_literal_names
126
+ | _match_pattern_attribute_names(tree)
127
+ | _exported_names(tree)
128
+ )
129
+ ```
130
+
131
+ A docstring that enumerates "attribute read, augmented-assignment target, class-pattern keyword, literal `getattr`/`attrgetter`" but omits the `__all__` source (`_exported_names`) is drifted: a field whose name appears in `__all__` is treated as read, and the prose hides that. The fix adds the missing source to the enumeration so the list matches the union.
132
+
133
+ ---
134
+
35
135
  ## Sample prompt
36
136
 
37
137
  The reusable Variant C template for Category O is in [`../prompts/category-o-docstring-vs-impl-drift.md`](../prompts/category-o-docstring-vs-impl-drift.md). Inline every changed module's docstring (module-level + every helper-function docstring whose function body was touched + every fixture docstring) alongside the symbols defined in the same module under `## Source material`.
@@ -39,3 +139,5 @@ The reusable Variant C template for Category O is in [`../prompts/category-o-doc
39
139
  ## Why Category O matters as its own bucket
40
140
 
41
141
  Signature-shaped claims — parameter names, return types, exceptions in the `Raises:` block — have a gate-time validator (`check_docstring_args_match_signature`) and signature-oriented audit categories to catch them. Free-form narrative prose in docstrings is the other half of the docstring contract: the part that tells a reader what the module is for, what the fixture does, what the predicate means. When that prose drifts from the body, the gate cannot catch it because there is no signature to compare against. Category O forces the audit teammate to list docstring claims and verify each against the body, the same way signature claims are verified against the body.
142
+
143
+ A docstring enumeration earns its place by being trustworthy. A complete list lets a reader reason about the function without scanning the body; a list missing one item is worse than no list, because it asserts completeness it does not have.
@@ -1,5 +1,21 @@
1
+ # Category O audit prompt (Variant C)
2
+
3
+ **Judgment standard (thick source):** `packages/claude-dev-env/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md`
4
+
5
+ This file is the audit **template** only: source-material slots, forced-exhaustion protocol, adversarial probes, cross-bucket questions, output shape, and a worked example. For every sub-bucket's judgment standard, gate inventory, and free-form checklist, read the thick rubric above. Do not treat this prompt as a second full copy of the standard.
6
+
7
+ ## Division of labor
8
+
9
+ | Surface | Role |
10
+ |---|---|
11
+ | Category O rubric | Single thick judgment source (O1–O9, gates, checklists, examples). |
12
+ | **This prompt** | Variant C protocol shell that points at the rubric for judgment. |
13
+ | `packages/claude-dev-env/rules/docstring-prose-matches-implementation.md` | Always-on write-time policy + compact checklist. |
14
+
1
15
  Audit [REPO/ARTIFACT] [TARGET_ID] for **Category O only** (docstring / fixture-prose vs implementation drift). Skip A–N, P. Sub-bucket forced-exhaustion mode: Category O is decomposed into 9 sub-buckets below. Each sub-bucket REQUIRES at least one Shape A finding OR exactly one Shape B proof-of-absence with **at least 3 adversarial probes** specific to that sub-bucket. A sub-bucket returning neither is a protocol gap.
2
16
 
17
+ Apply each sub-bucket's **judgment standard** from the thick rubric. The bullets under each sub-bucket here are protocol probes only.
18
+
3
19
  [ARTIFACT METADATA — include every changed module's docstring AND the exported symbols of that module so the audit can compare claim vs body]
4
20
 
5
21
  - Title / one-line summary: [TITLE]
@@ -20,39 +36,39 @@ ID prefix: `find`.
20
36
  ## Sub-buckets (each requires Shape A finding OR Shape B with ≥3 adversarial probes)
21
37
 
22
38
  **O1. Module-level responsibility verbs** ⭐ canonical O case
23
- - For every changed module, list the verbs the docstring uses (`detects`, `validates`, `enforces`, `recovers`, `parses`, `routes`). For each verb, name the exported symbol that delivers that responsibility. Verbs without a matching exported symbol are O1 findings.
24
- - Adversarial probes: (a) grep for the verb's noun-form in sibling modules — did a refactor move the responsibility out; (b) inspect the module's `__all__` (if present) — does every claimed responsibility appear; (c) check git log for recent splits — does the docstring still describe the pre-split scope.
39
+ - Judgment: thick rubric O1 (responsibility verbs + user-facing-text vs data-schema scope).
40
+ - Adversarial probes: (a) grep for the verb's noun-form in sibling modules — did a refactor place the responsibility elsewhere; (b) inspect the module's `__all__` (if present) — does every claimed responsibility appear; (c) check git log for recent splits — does the docstring still describe the pre-split scope.
25
41
 
26
42
  **O2. Fixture docstring vs sibling-test behavior**
27
- - For every changed fixture (especially `autouse=True` or module-scope), parse the fixture's docstring claims. For each claim, walk every test function in the same module — does any test explicitly opt out of the claimed invariant via a different fixture, `monkeypatch.setattr`, or environment override?
43
+ - Judgment: thick rubric O2.
28
44
  - Adversarial probes: (a) grep for the fixture's invariant-setting call in test bodies — does any test re-call it with a different argument; (b) check for `pytest.mark.parametrize` arguments that reach a code path the fixture claim says is disabled; (c) check for explicit teardown / reset calls inside tests that contradict the fixture's blanket scope.
29
45
 
30
46
  **O3. Predicate-name and -docstring vs body breadth**
31
- - For every changed boolean helper, compare the helper's name and docstring to the body's `return True` branches. Every branch's True path must be consistent with the promised name.
47
+ - Judgment: thick rubric O3.
32
48
  - Adversarial probes: (a) walk each `return True` branch and ask whether the input that reached it satisfies the name's promise; (b) construct an input class outside the named promise that still returns True — that is an O3 finding; (c) check the name against neighboring helpers — is one of them the better home for the broader case.
33
49
 
34
50
  **O4. Step-ordering narrative**
35
- - For every changed helper whose docstring describes processing as `step A then step B then step C`, trace the body and confirm the call order matches.
51
+ - Judgment: thick rubric O4 (includes branch-guarded dispatch; gated form `check_docstring_step_enumeration_dispatch_coverage`).
36
52
  - Adversarial probes: (a) read the body strictly top-to-bottom and label each call A/B/C against the docstring's named steps; (b) check for early returns that reorder visible steps; (c) check for `try/finally` blocks where the finally clause is itself one of the named steps and runs out of declared order.
37
53
 
38
54
  **O5. Named-sentinel / filename references**
39
- - For every docstring mention of a sentinel marker (`# pragma: ...`), environment variable name, filename, or magic string, grep the module body and the broader repo for the named token. Tokens not present anywhere are O5 findings.
55
+ - Judgment: thick rubric O5.
40
56
  - Adversarial probes: (a) grep the exact sentinel string in this module and sibling modules; (b) grep the named filename against the repo's naming convention (underscore vs hyphen); (c) check for case-sensitivity mismatches between the docstring and the body.
41
57
 
42
58
  **O6. Free-form `Args:`-adjacent claims**
43
- - For every docstring `Returns:` / `Raises:` / `Note:` / `Example:` section, extract each claim sentence. Verify each against the body. (The gate-time validator only checks `Args:` parameter names, not these adjacent sections.)
59
+ - Judgment: thick rubric O6 (unions, suppressors, exclusion axis, delegation summaries, Returns/Raises/Note claims, run-mode field meaning). Gate inventory and free-form checklist live in the rubric.
44
60
  - Adversarial probes: (a) check `Returns:` claims against every `return` statement in the body — is the documented return shape the actual return shape; (b) check `Raises:` claims against every `raise` and propagating callee — is every documented raise reachable; (c) check `Example:` snippets — does the snippet actually compile against the signature.
45
61
 
46
62
  **O7. Module-doc-vs-split-module after refactor**
47
- - When the diff includes a module split (one file becomes two), verify both modules' docstrings describe the responsibility each one actually owns after the split.
48
- - Adversarial probes: (a) for each module in the split, list its exported symbols and compare to the docstring's claimed responsibilities; (b) grep the responsibility's verb against the originating module — does the originating docstring still claim what moved; (c) check for cross-module imports that reveal which file hosts each responsibility.
63
+ - Judgment: thick rubric O7.
64
+ - Adversarial probes: (a) for each module in the split, list its exported symbols and compare to the docstring's claimed responsibilities; (b) grep the responsibility's verb against the originating module — does the originating docstring still claim what left; (c) check for cross-module imports that reveal which file hosts each responsibility.
49
65
 
50
66
  **O8. Companion-doc ordering/content vs producer**
51
- - When the diff changes a producer function's ordering or union, read that skill's companion `SKILL.md` and sibling `.md` docs for any sentence naming the same produced artifact (a file path, a JSON key, a named list). A doc sentence that claims the artifact is `sorted` / `alphabetical` / `in sorted order`, or holds `just the at-risk names` / `only the current set`, while the producer merges stored names with new names and appends — preserving file order, not re-sorting the union — is an O8 finding on both counts (wrong order claim, hidden merged-in entries). The finding stands even when the diff never touched the `.md` file, because the behavior change orphaned the doc claim.
67
+ - Judgment: thick rubric O8 (order/content claims vs producer; `check_docstring_no_consumer_claim` for the producer-only assertion slice).
52
68
  - Adversarial probes: (a) for each changed producer, name the artifact it builds and grep the skill's `SKILL.md` and sibling `.md` files for any sentence naming that artifact; (b) walk the producer body's build step — does it sort, or does it merge stored names and append in file order — and compare against the doc's order word (`sorted`, `alphabetical`); (c) check whether the doc's content claim (`just the at-risk names`, `only the current set`) hides merged-in prior entries the producer carries over from the stored file.
53
69
 
54
70
  **O9. Python docstring plainness for a general developer**
55
- - For every changed module / class / public-function docstring, read the narrative prose before the first `Args:` / `Returns:` / `Raises:` / `Yields:` section as a stranger to the code. Flag a narrative that stacks abstract machinery nouns into a single wall, that defines a thing by what it is not (`the non-promoter-specific machinery`), or that runs one sentence past the word limit while joining clauses with an em-dash or a semicolon. The run-on mark is gated at Write/Edit time by `check_docstring_runon_sentence`, so this lane judges the part the gate cannot: whether the prose paints a concrete scene — the moment the code matters, the input it sees, the outcome it produces — that a general developer follows on the first read.
71
+ - Judgment: thick rubric O9 (and `packages/claude-dev-env/rules/plain-illustrative-docstrings.md`). Run-on and prose-wall gates backstop the deterministic marks.
56
72
  - Adversarial probes: (a) read each changed narrative and name the concrete moment, input, and outcome it paints — a narrative that names none is an O9 finding; (b) count the longest sentence's words and check for an em-dash or semicolon join — over the limit with a join is the wall mark the gate also catches; (c) rewrite each "is not" clause as a positive statement — a clause that resists rewriting because the body offers no positive description is an O9 finding.
57
73
 
58
74
  ## Cross-bucket questions to answer at the end
@@ -65,7 +81,7 @@ Q3: Of the changed docstrings, which one most clearly shows a refactor was incom
65
81
 
66
82
  ## Output
67
83
 
68
- Lead: `Total: N (P0=N, P1=N, P2=N)`. For each sub-bucket O1-O9, produce Shape A or Shape B (with ≥3 probes). Each Shape A finding must cite (a) the docstring file:line, (b) the body file:line that contradicts it, and (c) one sentence describing the contradiction in concrete terms. Cross-bucket Q1-Q3 answers after the per-sub-bucket walk. Adversarial second pass: "assume your first pass missed at least 3 module-level docstring claims whose implementation moved during a refactor — find them." Open Questions section for ambiguities. Read-only. No edits, no commits.
84
+ Lead: `Total: N (P0=N, P1=N, P2=N)`. For each sub-bucket O1-O9, produce Shape A or Shape B (with ≥3 probes). Each Shape A finding must cite (a) the docstring file:line, (b) the body file:line that contradicts it, and (c) one sentence describing the contradiction in concrete terms. Cross-bucket Q1-Q3 answers after the per-sub-bucket walk. Adversarial second pass: "assume your first pass missed at least 3 module-level docstring claims whose implementation left during a refactor — find them." Open Questions section for ambiguities. Read-only. No edits, no commits.
69
85
 
70
86
  ---
71
87
 
@@ -77,6 +93,6 @@ PR #522 split `pr_description_command_parser.py` into two modules — the origin
77
93
 
78
94
  Expected findings on PR #522:
79
95
  - **O1 finding:** `pr_description_body_audit.py:8` docstring uses verb `detects`, but the only exported symbol prepares input for a regex scan that fires in a different module. Body line(s) showing `_extract_vague_scan_text` returning normalized text without a detection call.
80
- - **O7 finding:** `pr_description_command_parser.py` module docstring still names PR-number recovery as a responsibility; the split moved that to `pr_description_pr_number.py`. The originating docstring needs an O7-shaped rewrite to drop the moved claim.
96
+ - **O7 finding:** `pr_description_command_parser.py` module docstring still names PR-number recovery as a responsibility; the split placed that in `pr_description_pr_number.py`. The originating docstring needs an O7-shaped rewrite to drop the claim that left.
81
97
  - **O2 finding:** `test_pr_description_enforcer_readability.py` autouse fixture docstring claims readability is globally disabled `for these tests`; sibling tests in the same module explicitly re-enable readability through a different state path.
82
98
  - **O5 finding:** `code_rules_magic_values.py` docstring references a `# pragma: no-tdd-gate` sentinel and a hyphenated `test_code-rules-enforcer.py` filename; neither token exists in the module body or matches the repo's underscore-only test-file naming convention.
package/bin/CLAUDE.md CHANGED
@@ -6,32 +6,95 @@ The installer and its companion modules. Running `npx claude-dev-env` (or `node
6
6
 
7
7
  | File | Purpose |
8
8
  |---|---|
9
- | `install.mjs` | Main installer: discovers install groups, copies content directories (`rules`, `docs`, `commands`, `agents`, `system-prompts`, `scripts`, `_shared`, `audit-rubrics`), merges hooks into `settings.json`, installs skills, prunes retired skills on a full install, runs `git_hooks_installer.mjs` and `install_mypy_ini.mjs` |
9
+ | `install.mjs` | Main installer: discovers install groups, copies content directories (`rules`, `docs`, `commands`, `agents`, `system-prompts`, `scripts`, `_shared`, `audit-rubrics`), merges hooks into `settings.json`, installs skills, prunes retired skill directories, retired hook entries, and stale files under every managed root on a full install, retires older prune backups, runs `git_hooks_installer.mjs` and `install_mypy_ini.mjs` |
10
+ | `install-constants.mjs` | The named values `install.mjs` reads: `SKIPPED_SOURCE_ENTRY_NAMES` and `SKIPPED_SOURCE_FILE_EXTENSIONS` for the build artifacts the source walk leaves behind, `RUN_BACKUP_DIRECTORY_NAME_PATTERN` for the timestamp shape a run backup directory carries, `MANAGED_SKILLS_DIRECTORY_NAME` and `MANAGED_HOOKS_DIRECTORY_NAME` for the directory name each of those trees carries in a package source and under `~/.claude` — read by the copy loops, the hooks.json reads, the git-hook shims, the mypy configuration, and the prunes alike — `SETTINGS_FILE_NAME` for the settings file the merge, the retired-hook prune, and the uninstall purge share, and `MYPY_INI_FILE_NAME` for the home-directory file `install_mypy_ini.mjs` writes |
10
11
  | `ever-shipped-skills.mjs` | Static `EVER_SHIPPED_SKILL_NAMES` set of every top-level skill directory name the package has shipped; the installer subtracts the current skill set from it to prune retired skills left under `~/.claude/skills` |
11
12
  | `expand_home_directory_tokens.mjs` | Expands residual `$HOME` / `${HOME}` / `~/` tokens in settings.json hook and statusLine commands to absolute home paths at install time (literal-safe for homes that contain `$`) |
12
13
  | `git_hooks_installer.mjs` | Installs or updates the `pre-commit`, `pre-push`, and `post-commit` Git hooks in the user's git config; writes hook scripts that delegate to the installed Python hooks |
13
14
  | `install_mypy_ini.mjs` | Writes `~/.mypy.ini` with settings that make mypy find the hooks package and enforce strict type checking |
14
- | `install.test.mjs` | Tests for `install.mjs` — covers conflict detection, interpreter detection, settings merging |
15
+ | `install.test.mjs` | Unit tests for `install.mjs` — covers conflict detection, interpreter detection, settings merging, the settings shapes the installer never wrote — those every hook walk hands back untouched, and the shipped-event value the merge replaces with a warning — the source-artifact skip in `collectFiles`, the case-only rename decision and the `copyTree` copy that acts on it, the retired-hook diff and settings prune, the stale-file prune: the manifest diff, path-key case folding, emptied-parent cleanup, and the warn-and-keep paths, and backup retention: the sweep a moved-content run drives, the empty root a run whose moves failed gives up, and the populated root retention keeps |
16
+ | `install.prune.test.mjs` | End-to-end prune tests that run the real installer against a sandbox `HOME` — retired-skill, retired-hook, and stale-file moves into one timestamped backup, the settings entry a retired hook loses, the top-level paths every root's diff leaves alone, the manifest record a failed move keeps, the full-install and resolved-dependency gates, backup retention, and the uninstall: the `~/.mypy.ini` removal, the containment guard, and nested-directory cleanup |
15
17
  | `git_hooks_installer.test.mjs` | Tests for `git_hooks_installer.mjs` |
16
18
  | `install_mypy_ini.test.mjs` | Tests for `install_mypy_ini.mjs` |
17
19
 
20
+ ## Source build artifacts
21
+
22
+ `collectFiles` walks the package source and skips the artifacts a contributor's tooling writes beside it: the entry names `__pycache__`, `.ruff_cache`, `.pytest_cache`, `.mypy_cache`, `node_modules`, `.DS_Store`, and any file ending `.pyc` or `.pyo`. A skipped directory takes everything under it out of the walk. The `files` negations in `package.json` (`!**/__pycache__/**`, `!**/*.py[cod]`, the cache directories, `!**/*.log`, `!**/*.egg-info/**`) keep the same artifacts out of the published tarball, and `.npmignore` carries those patterns for tooling that reads it — keep the two in step. An `npx` install reads a clean tree; the walk covers a local `node bin/install.mjs` run against a working tree that holds the artifacts.
23
+
24
+ The skip and the cleanup of artifacts an earlier install copied are one code path. A `.pyc` a prior manifest records under any managed root sits outside the set the walk returns, so the next full install reads it as stale, moves it into that run's backup root, and drops it from the manifest the run writes.
25
+
26
+ ## Copying a file whose name changed letter case
27
+
28
+ `copyTree` renames a destination entry that differs from the shipped file name only in letter case to the shipped name, then copies. On a case-insensitive volume `copyFileSync` writes its bytes through whichever entry the filesystem resolves the path to, so a package shipping `README.md` over an installed `Readme.md` would fill the installed entry and leave the earlier spelling standing. The rename runs first because `renameSync` inside one directory is atomic: a run interrupted between the rename and the copy leaves the file present under the shipped name holding the earlier content, which the next install overwrites.
29
+
30
+ The decision reads the destination directory's entry names, cached one listing per directory for the whole copy run. On a case-sensitive volume the two names are two files, so the rename is skipped and each name keeps its own content. `caseOnlyRenameSourceName(shippedName, existingNames, options)` holds the decision, and `options.isCaseInsensitive` carries the platform answer as a value so a test drives either branch on a host of either kind.
31
+
18
32
  ## Retired-skill prune
19
33
 
20
- The full-install prune renames a retired skill directory into a timestamped backup rather than deleting it. Each pruned directory is renamed to `~/.claude/.claude-dev-env-pruned/<timestamp>/<skill-name>/`, a backup root outside `~/.claude/skills` so a backed-up directory is never re-discovered as a skill. Backups accumulate nothing cleans them so a user can recover a directory. A rename that fails leaves the directory in place with a logged warning and never falls back to deletion, so a prune failure costs at most a cosmetic leftover.
34
+ The full-install prune renames a retired skill directory into a timestamped backup rather than deleting it. Each pruned directory is renamed to `~/.claude/.claude-dev-env-pruned/<timestamp>/skills/<skill-name>/`, a backup root outside `~/.claude/skills` so a backed-up directory is never re-discovered as a skill. The `skills/` segment mirrors `~/.claude`, matching the layout the stale-file prune writes, so one recovery point reads as a copy of the tree it came from. One run shares one timestamped root, so a run leaves one recovery point. A rename that fails leaves the directory in place with a logged warning and never falls back to deletion, so a prune failure costs at most a cosmetic leftover.
35
+
36
+ Matching is by directory name alone, so a user-authored directory whose name collides with a retired skill is backed up as if it were that skill. A directory is pruned when the prior install's manifest recorded it or the ever-shipped set names it, and the current install did not just write it. A name absent from all three of those sets, together with `~/.claude/skills/_shared`, is left in place. Recovery of a wrongly-matched directory runs until the next pruning install, which keeps its own backup and retires the rest.
37
+
38
+ ## Stale-file prune
39
+
40
+ A full install also moves aside a file under a managed root that the run leaves unwritten. `copyTree` adds and overwrites but never removes, so every root the installer writes carries the same drift, and the prune covers all of them: `rules`, `docs`, `commands`, `agents`, `system-prompts`, `scripts`, `_shared`, `audit-rubrics`, `skills`, and `hooks` — the names in `MANAGED_TOP_LEVEL_DIRECTORY_NAMES`.
41
+
42
+ Nothing moves unless a prior install recorded it. That single rule is what makes covering ten roots as safe as covering one: the installer reads the file list from `~/.claude/.claude-dev-env-manifest.json`, subtracts every file the run copied across all source roots, and moves what remains.
43
+
44
+ The prune runs once per root, each call confined to its own root. Per-root iteration gives the containment guard and the emptied-parent walk the root that owns each file, and it settles `_shared`: `~/.claude/_shared` and `~/.claude/skills/_shared` are distinct absolute paths, so the `_shared` call and the `skills` call each see their own files and no path enters two diffs. Each root's content lands under `~/.claude/.claude-dev-env-pruned/<timestamp>/<root-name>/<relative>`, so the backup mirrors `~/.claude`. Every prune in a run shares that one timestamped root. A recorded path under no managed root — `~/.claude/CLAUDE.md`, `settings.json`, the manifest itself, and the `~/.mypy.ini` that sits in the home directory beside `~/.claude` — reaches no root's diff and stays where it is. The install summary reports the skills root's own count on the `skills:` line and the sum across roots on its own line.
45
+
46
+ The manifest diff limits the move to files the installer itself wrote. Runtime-generated content — a Python `__pycache__` entry, a ruff cache, a log — and any file a user authored under a managed root stay in place, because no install recorded them. Path comparison ignores letter case on Windows and macOS, so a package shipping `README.md` over an installed `Readme.md` keeps the bytes the run just wrote. A directory or a link standing where the manifest records a file is skipped with a warning, so the mover never renames a whole tree and never follows a link out of `~/.claude`. A directory emptied by a move is removed, walking up to the root the file sat under. A move that fails logs a warning and leaves the file in place, so a prune failure costs at most a stale file. The installer records each such path in the fresh manifest when the file is still on disk, so the file stays inside the next full install's diff and gets another attempt.
47
+
48
+ A missing or unreadable manifest, or one carrying no file list, holds the stale-file prune for that run: with no record of what an install wrote, the run has nothing to diff against.
49
+
50
+ A run writes both manifest keys — the file list and the skill-name list — wholesale from what it just installed only when the prunes ran that run and read the prior record all the way through, so the next diff reads as "the package stopped shipping this". Every other run unions what it wrote onto the prior lists: a scoped `--only` install, a full install holding its prunes behind an unresolved dependency group, and a run whose prune step ends early with a logged warning. The union keeps every entry a later prune needs to spot a stale file or a retired skill, and keeps `--uninstall` able to name the whole tree. The prune itself bounds the lists: a stale path leaves the record on the first full install that moves it aside.
51
+
52
+ ## Retired-hook entries in settings.json
53
+
54
+ A hook script under `~/.claude/hooks` carries a second reference: the `settings.json` entry that runs it. A full install removes that entry in the same run that moves the script aside, and removes it first — a `settings.json` naming a script that has left the hooks directory makes every session start invoke a missing file.
55
+
56
+ The retired set comes from the manifest diff alone: the hook files a prior install recorded that this run leaves unwritten, each taken relative to `~/.claude/hooks`. A script the run still writes stays out of the set, and a path no install of ours recorded never enters it, so a user-authored hook is out of reach of the prune. Each command is matched on the anchored `/.claude/hooks/<relative>` tail the merge uses to tell this installer's entries from a user's, so a command whose path is a retired tail plus a suffix (`retired_gate.py.bak`) names another file and stays.
57
+
58
+ The walk covers every event type the settings file holds rather than the ones the current `hooks.json` names, so an entry under an event type the package stopped shipping is reached too. A matcher group left empty is dropped, and an event type left empty goes with it. The file is written once, and only when an entry left it, so a run that retires no hook leaves `settings.json` byte-identical.
59
+
60
+ Every settings walk recognizes the shapes the installer writes and steps around the rest, so a hand-edited or third-party `settings.json` carries an install through rather than ending it. A matcher group carrying no `hooks` array and a hook entry whose `command` is not a string survive every walk untouched. An event type whose value is not an array of groups survives this prune and the uninstall purge; the merge replaces that value with the group list it ships for that event type, warning with the event type named so the user can recover the value from their own history. An event type the package ships no groups for keeps whatever value the file holds.
61
+
62
+ ## Backup retention
63
+
64
+ A run that moves content into `~/.claude/.claude-dev-env-pruned/<timestamp>/` then retires the other run backups, so the directory holds the one recovery point closest to what sits on disk. The retired-skill prune and the stale-file prune each report how many moves succeeded, and their sum is the signal the sweep answers to. The sweep removes a direct child whose name matches the installer's timestamp shape (`2026-07-25T18-04-11-923Z`), which leaves anything else under the pruned-backup directory in place, along with the directory itself. A removal that fails logs a warning and the sweep carries on, so retention never ends an install. The install output names the count when the sweep removes anything.
65
+
66
+ A run that moves nothing sweeps nothing, so every recovery point the user holds stays where it is. `moveIntoRunBackup` creates the directories leading to a backup path before it renames, so a run whose every move fails — the antivirus scanner or open editor case — leaves that timestamped root standing empty. Retention clears it with `rmdirSync` alone, depth first, so a directory holding anything survives every step.
67
+
68
+ ## Uninstall
69
+
70
+ `--uninstall` reads the manifest and removes each file it records.
71
+
72
+ Each record passes a containment guard first: the path resolves under `~/.claude`, or it names the `~/.mypy.ini` the install writes in the home directory. Every other record is skipped with a warning and counted. Skipping keeps one malformed record — hand-edited, or written by an installer that ran against a different home — from stranding the user with a half-removed install. The purge removes every legitimate record, clears the manifest, and reports the skipped count.
73
+
74
+ Removing a file leaves its directory a candidate for cleanup. Once the file loop ends, the purge walks up from each such directory to the managed top-level directory the file sits under (`MANAGED_TOP_LEVEL_DIRECTORY_NAMES`), removing each directory it finds empty. That reaches a nested tree such as `skills/<name>/scripts/`. A record under no managed root gets no walk, so `~/.claude` itself is never a stop root and a directory the installer never wrote stays. A separate pass drops each managed top-level directory the purge empties.
21
75
 
22
- Matching is by directory name alone, so a user-authored directory whose name collides with a retired skill is backed up as if it were that skill. A directory is pruned when the prior install's manifest recorded it or the ever-shipped set names it, and the current install did not just write it. A name absent from all three of those sets, together with `~/.claude/skills/_shared`, is left in place.
76
+ ## Prune gates
23
77
 
24
- The prune is skipped for the whole run with a logged notice naming the unresolved group when any declared dependency group fails to resolve. An unresolved dependency contributes no skills to the installed set, so a live skill that a dependency package supplies would look retired; holding the prune until every dependency resolves keeps such a skill from being backed up.
78
+ Every prune runs behind the same two gates: a full install, and every declared dependency group resolved. When any dependency group fails to resolve, all of them are skipped for the whole run with a logged notice naming the unresolved group. An unresolved dependency contributes no skills to the installed set, so a live skill that a dependency package supplies would look retired and its files would look stale; holding every prune until each dependency resolves keeps that skill's files in place and keeps their manifest records, so a run with every dependency resolved can still prune them.
25
79
 
26
80
  ## Key exports from install.mjs
27
81
 
28
82
  | Export | Description |
29
83
  |---|---|
30
84
  | `CONTENT_DIRECTORIES` | Array of package subdirectory names copied verbatim to `~/.claude/` |
85
+ | `MANAGED_TOP_LEVEL_DIRECTORY_NAMES` | The content directories plus `skills` and `hooks`; the stale-file prune walks it to give each root its own diff, and the uninstall purge reads it to find the root a recorded file belongs to |
86
+ | `collectFiles(directory)` | Lists every file under a source directory, skipping the build-artifact names and extensions in `install-constants.mjs` |
31
87
  | `pythonCandidatesForPlatform(platform)` | Returns ordered Python interpreter candidates to probe; `py -3` first on Windows to avoid Microsoft Store alias issues |
32
88
  | `isWindowsStorePythonStub(path)` | Returns true when the path resolves to the non-spawnable WindowsApps stub |
33
89
  | `interpreterCommandFromPath(path)` | Formats an absolute interpreter path as a settings.json hook command prefix |
34
90
  | `collectPackageSourceConflicts(dir)` | Returns any unmerged git conflicts in the package source; installer aborts when any exist |
91
+ | `pruneStaleInstalledFiles(priorFiles, currentFiles, destinationRoot, backupRoot, options)` | Moves each manifest-recorded file under the destination root that the run leaves unwritten into the run's backup root; returns `{ prunedCount, failedPaths }`. `options.isCaseInsensitive` drives path-key case folding, defaulting to this host's filesystem; `options.managedHomeDirectory` sets the home the containment guard tests against, defaulting to `~/.claude` |
92
+ | `copyTree(sourceBase, destBase, options)` | Copies every file under a source directory, renaming a destination entry that differs from the shipped name only in letter case to the shipped name first; returns `{ created, updated, paths }`. `options.isCaseInsensitive` drives that rename, defaulting to this host's filesystem |
93
+ | `caseOnlyRenameSourceName(shippedName, existingNames, options)` | Returns the existing directory entry a shipped file name would overwrite through a case-only spelling difference, or null; `options.isCaseInsensitive` defaults to this host's filesystem |
94
+ | `retiredManagedHookRelativePaths(priorFiles, currentFiles, hooksRoot)` | Returns the hook script paths a prior install recorded under the hooks root that this run leaves unwritten, each relative to that root |
95
+ | `pruneRetiredHookEntriesFromSettings(settingsPath, retiredPaths)` | Removes each settings.json entry running a retired managed hook script, writing the file only when an entry left it; returns the removed count |
96
+ | `retainNewestRunBackupOnly(runBackupRoot, didRunMoveContent)` | Retires every run backup sitting beside the run's own when `didRunMoveContent` holds; clears the run's empty root with `rmdirSync` when it does not |
97
+ | `comparisonKeyForPath(path, options)` | Builds the key two paths are compared through: resolved, forward-slashed, and lowercased when `options.isCaseInsensitive` holds — which defaults to true on Windows and macOS |
35
98
 
36
99
  ## Install groups
37
100
 
@@ -39,6 +39,7 @@ export const EVER_SHIPPED_SKILL_NAMES = new Set([
39
39
  'grok-spawn',
40
40
  'grokify',
41
41
  'implement',
42
+ 'issue-tracker',
42
43
  'log-audit',
43
44
  'logifix',
44
45
  'monitor-open-prs',
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Named values the installer reads, kept out of the code that uses them.
3
+ *
4
+ * `install.mjs` holds the install pipeline; the tables below hold the data that
5
+ * pipeline compares against, so a value has one home and one spelling.
6
+ */
7
+
8
+ /**
9
+ * Entry names the source walk leaves behind, whatever directory they sit in.
10
+ *
11
+ * Each name belongs to a tool that writes beside the source it reads: Python
12
+ * bytecode caches, the ruff, pytest, and mypy caches, an installed
13
+ * `node_modules` tree, and the macOS Finder's `.DS_Store` marker. A contributor
14
+ * who runs the test suites and then runs `node bin/install.mjs` copies from a
15
+ * source tree carrying all of them, so skipping the names at the walk keeps them
16
+ * out of `~/.claude` and out of the install manifest.
17
+ */
18
+ export const SKIPPED_SOURCE_ENTRY_NAMES = new Set([
19
+ '__pycache__',
20
+ '.ruff_cache',
21
+ '.pytest_cache',
22
+ '.mypy_cache',
23
+ 'node_modules',
24
+ '.DS_Store',
25
+ ]);
26
+
27
+ /**
28
+ * File extensions the source walk leaves behind, compared in lower case.
29
+ *
30
+ * Python writes a `.pyc` beside a module inside `__pycache__` and a `.pyo` under
31
+ * an optimized run, and either can land outside a cache directory, so the
32
+ * extension check stands alongside the name check.
33
+ */
34
+ export const SKIPPED_SOURCE_FILE_EXTENSIONS = new Set(['.pyc', '.pyo']);
35
+
36
+ /**
37
+ * The name shape a run backup directory carries under
38
+ * `~/.claude/.claude-dev-env-pruned/`.
39
+ *
40
+ * The installer names each run backup from an ISO timestamp with every `:` and
41
+ * `.` rewritten as `-`, for example `2026-07-25T18-04-11-923Z`. The retention
42
+ * sweep removes only a direct child matching this shape, so a directory any
43
+ * other tool or person left in the backup directory stays where it is.
44
+ */
45
+ export const RUN_BACKUP_DIRECTORY_NAME_PATTERN =
46
+ /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z$/;
47
+
48
+ /**
49
+ * The directory name skill directories carry in a package source and under
50
+ * `~/.claude`.
51
+ *
52
+ * The copy loop reads `<package-root>/skills` and writes `~/.claude/skills`, the
53
+ * dependency walk reads `<dependency-root>/skills`, the retired-skill prune reads
54
+ * the installed directory, and the per-root stale-file prune names it as one of
55
+ * its roots. One spelling serves every one of them, so the copy destination and
56
+ * the prune target stay the same directory.
57
+ */
58
+ export const MANAGED_SKILLS_DIRECTORY_NAME = 'skills';
59
+
60
+ /**
61
+ * The directory name hook scripts carry in a package source and under
62
+ * `~/.claude`.
63
+ *
64
+ * The copy loop reads `<package-root>/hooks` and writes `~/.claude/hooks`, the
65
+ * hooks.json reads sit under the same name in each package source, the git-hook
66
+ * shims and the mypy configuration point at the installed directory, the
67
+ * retired-hook diff takes its relative paths against it, and the per-root
68
+ * stale-file prune names it as one of its roots.
69
+ */
70
+ export const MANAGED_HOOKS_DIRECTORY_NAME = 'hooks';
71
+
72
+ /**
73
+ * The `~/.claude` file name that holds the user's harness settings.
74
+ *
75
+ * The hook merge writes it, the retired-hook prune rewrites it, and the uninstall
76
+ * purge rewrites it, so all three reach the file through one name.
77
+ */
78
+ export const SETTINGS_FILE_NAME = 'settings.json';
79
+
80
+ /**
81
+ * The home-directory file name `install_mypy_ini.mjs` writes.
82
+ *
83
+ * mypy reads its configuration from the home directory, so this is the one file
84
+ * the installer writes outside `~/.claude`. The install records the path and the
85
+ * uninstall containment guard names it as a permitted location, so `--uninstall`
86
+ * removes the file the install created.
87
+ */
88
+ export const MYPY_INI_FILE_NAME = '.mypy.ini';