claude-dev-env 1.83.0 → 1.85.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 (186) hide show
  1. package/_shared/pr-loop/audit-contract.md +24 -12
  2. package/_shared/pr-loop/scripts/_claude_permissions_common.py +16 -5
  3. package/_shared/pr-loop/scripts/code_rules_gate.py +2 -3
  4. package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -1
  5. package/_shared/pr-loop/scripts/pr_loop_shared_constants/terminology_sweep_constants.py +8 -0
  6. package/_shared/pr-loop/scripts/reviews_disabled.py +2 -0
  7. package/_shared/pr-loop/scripts/terminology_sweep.py +85 -39
  8. package/_shared/pr-loop/scripts/tests/test__claude_permissions_common.py +212 -0
  9. package/_shared/pr-loop/scripts/tests/test_agent_config_carveout.py +18 -0
  10. package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +18 -0
  11. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +37 -0
  12. package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +14 -0
  13. package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +94 -4
  14. package/agents/clean-coder.md +3 -0
  15. package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +1 -1
  16. package/bin/install.mjs +16 -8
  17. package/bin/install.test.mjs +17 -6
  18. package/commands/CLAUDE.md +0 -1
  19. package/docs/CODE_RULES.md +1 -1
  20. package/hooks/blocking/CLAUDE.md +0 -4
  21. package/hooks/blocking/claude_md_orphan_file_blocker.py +1 -1
  22. package/hooks/blocking/code_rules_constants_config.py +5 -0
  23. package/hooks/blocking/code_rules_docstrings.py +133 -19
  24. package/hooks/blocking/code_rules_enforcer.py +4 -0
  25. package/hooks/blocking/code_rules_imports_logging.py +10 -6
  26. package/hooks/blocking/code_rules_magic_values.py +5 -0
  27. package/hooks/blocking/code_rules_naming_collection.py +15 -8
  28. package/hooks/blocking/code_rules_paired_test.py +3 -2
  29. package/hooks/blocking/code_rules_shared.py +34 -0
  30. package/hooks/blocking/code_rules_string_magic.py +1 -1
  31. package/hooks/blocking/code_rules_unused_imports.py +2 -2
  32. package/hooks/blocking/duplicate_rmtree_helper_blocker.py +4 -4
  33. package/hooks/blocking/test_code_rules_enforcer_docstring_args_span_scope.py +29 -0
  34. package/hooks/blocking/test_code_rules_enforcer_docstring_prose_wall_illustration.py +197 -0
  35. package/hooks/blocking/test_code_rules_enforcer_docstring_runon_sentence.py +27 -0
  36. package/hooks/blocking/test_code_rules_enforcer_naive_datetime.py +24 -0
  37. package/hooks/blocking/test_code_rules_enforcer_split_constants_config.py +33 -0
  38. package/hooks/blocking/test_code_rules_enforcer_split_shared.py +23 -0
  39. package/hooks/blocking/test_code_rules_magic_values.py +54 -0
  40. package/hooks/blocking/test_duplicate_rmtree_helper_blocker.py +0 -6
  41. package/hooks/blocking/test_pr_converge_bugteam_enforcer_state_tolerance.py +184 -0
  42. package/hooks/blocking/test_shared_stdin_adoption.py +5 -30
  43. package/hooks/hooks_constants/CLAUDE.md +0 -3
  44. package/hooks/hooks_constants/blocking_check_limits.py +4 -0
  45. package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -1
  46. package/hooks/hooks_constants/duplicate_rmtree_helper_blocker_constants.py +0 -1
  47. package/hooks/hooks_constants/post_tool_use_dispatcher_constants.py +0 -5
  48. package/hooks/validation/post_tool_use_dispatcher.py +1 -1
  49. package/hooks/validation/test_post_tool_use_dispatcher.py +51 -35
  50. package/hooks/workflow/CLAUDE.md +2 -8
  51. package/package.json +1 -1
  52. package/rules/CLAUDE.md +1 -1
  53. package/rules/docstring-prose-matches-implementation.md +1 -1
  54. package/rules/env-var-table-code-drift.md +1 -1
  55. package/rules/package-inventory-stale-entry.md +1 -1
  56. package/rules/paired-test-coverage.md +1 -1
  57. package/rules/plain-illustrative-docstrings.md +35 -1
  58. package/skills/CLAUDE.md +6 -1
  59. package/skills/_shared/pr-loop/CLAUDE.md +1 -0
  60. package/skills/_shared/pr-loop/scripts/CLAUDE.md +1 -0
  61. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/CLAUDE.md +1 -0
  62. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/handoff_constants.py +45 -0
  63. package/skills/_shared/pr-loop/scripts/test_write_handoff.py +201 -0
  64. package/skills/_shared/pr-loop/scripts/write_handoff.py +309 -0
  65. package/skills/autoconverge/SKILL.md +82 -58
  66. package/skills/autoconverge/reference/closing-report.md +6 -6
  67. package/skills/autoconverge/reference/convergence.md +17 -15
  68. package/skills/autoconverge/reference/gotchas.md +6 -3
  69. package/skills/autoconverge/workflow/autoconverge_report_constants/render_report_constants.py +2 -9
  70. package/skills/autoconverge/workflow/converge.contract.test.mjs +87 -13
  71. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +1 -1
  72. package/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +16 -9
  73. package/skills/autoconverge/workflow/converge.mjs +71 -74
  74. package/skills/autoconverge/workflow/render_report.py +7 -11
  75. package/skills/bugteam/CLAUDE.md +1 -1
  76. package/skills/bugteam/PROMPTS.md +7 -6
  77. package/skills/bugteam/SKILL.md +25 -72
  78. package/skills/bugteam/reference/CLAUDE.md +1 -3
  79. package/skills/bugteam/reference/README.md +1 -1
  80. package/skills/bugteam/reference/audit-and-teammates.md +1 -1
  81. package/skills/bugteam/reference/obstacles/fix-publish-summary.md +1 -1
  82. package/skills/bugteam/reference/team-setup.md +8 -7
  83. package/skills/bugteam/scripts/CLAUDE.md +0 -6
  84. package/skills/bugteam/scripts/README.md +0 -4
  85. package/skills/bugteam/scripts/bugteam_scripts_constants/CLAUDE.md +0 -1
  86. package/skills/code/SKILL.md +2 -0
  87. package/skills/copilot-review/CLAUDE.md +1 -1
  88. package/skills/copilot-review/SKILL.md +25 -23
  89. package/skills/findbugs/CLAUDE.md +2 -2
  90. package/skills/findbugs/SKILL.md +22 -83
  91. package/skills/fixbugs/SKILL.md +2 -4
  92. package/skills/log-audit/CLAUDE.md +20 -0
  93. package/skills/log-audit/SKILL.md +68 -0
  94. package/skills/log-audit/reference/CLAUDE.md +9 -0
  95. package/skills/log-audit/reference/charter.md +52 -0
  96. package/skills/log-audit/scripts/CLAUDE.md +27 -0
  97. package/skills/log-audit/scripts/cluster_recurrences.py +261 -0
  98. package/skills/log-audit/scripts/collect_log_window.py +199 -0
  99. package/skills/log-audit/scripts/log_audit_constants/CLAUDE.md +12 -0
  100. package/skills/log-audit/scripts/log_audit_constants/cluster_recurrences_constants.py +23 -0
  101. package/skills/log-audit/scripts/log_audit_constants/collect_log_window_constants.py +24 -0
  102. package/skills/log-audit/scripts/log_audit_constants/mine_copilot_findings_constants.py +49 -0
  103. package/skills/log-audit/scripts/mine_copilot_findings.py +302 -0
  104. package/skills/log-audit/scripts/test_cluster_recurrences.py +160 -0
  105. package/skills/log-audit/scripts/test_collect_log_window.py +111 -0
  106. package/skills/log-audit/scripts/test_mine_copilot_findings.py +126 -0
  107. package/skills/monitor-open-prs/SKILL.md +2 -2
  108. package/skills/post-audit-findings/SKILL.md +84 -0
  109. package/skills/pr-converge/CLAUDE.md +2 -0
  110. package/skills/pr-converge/SKILL.md +99 -60
  111. package/skills/pr-converge/reference/CLAUDE.md +1 -1
  112. package/skills/pr-converge/reference/convergence-gates.md +16 -19
  113. package/skills/pr-converge/reference/examples.md +5 -5
  114. package/skills/pr-converge/reference/fix-protocol.md +16 -43
  115. package/skills/pr-converge/reference/obstacles/fix-publish-summary.md +1 -1
  116. package/skills/pr-converge/reference/per-tick.md +24 -45
  117. package/skills/pr-converge/reference/state-schema.md +27 -0
  118. package/skills/pr-converge/scripts/README.md +3 -5
  119. package/skills/pr-fix-protocol/SKILL.md +70 -0
  120. package/skills/pr-loop-lifecycle/SKILL.md +73 -0
  121. package/skills/{bugteam → pr-loop-lifecycle}/reference/teardown-publish-permissions.md +22 -24
  122. package/skills/pr-scope-resolve/SKILL.md +48 -0
  123. package/skills/qbug/CLAUDE.md +4 -4
  124. package/skills/qbug/SKILL.md +46 -144
  125. package/skills/qbug/test_qbug_skill_audit_schema.py +2 -2
  126. package/skills/qbug/test_qbug_skill_post_fix_audit.py +1 -1
  127. package/skills/reviewer-gates/SKILL.md +96 -0
  128. package/skills/session-log/CLAUDE.md +7 -7
  129. package/skills/session-log/SKILL.md +27 -44
  130. package/skills/test_markdown_link_integrity.py +103 -0
  131. package/commands/doc-gist.md +0 -16
  132. package/hooks/blocking/_md_to_html_blocker_test_support.py +0 -65
  133. package/hooks/blocking/conftest.py +0 -30
  134. package/hooks/blocking/md_path_exemptions.py +0 -224
  135. package/hooks/blocking/md_to_html_blocker.py +0 -155
  136. package/hooks/blocking/test_md_to_html_blocker_exemptions.py +0 -434
  137. package/hooks/blocking/test_md_to_html_blocker_extensions.py +0 -157
  138. package/hooks/blocking/test_md_to_html_blocker_path_resolution.py +0 -336
  139. package/hooks/hooks_constants/doc_gist_auto_publish_constants.py +0 -18
  140. package/hooks/hooks_constants/html_companion_constants.py +0 -20
  141. package/hooks/hooks_constants/md_to_html_blocker_constants.py +0 -76
  142. package/hooks/hooks_constants/test_md_to_html_blocker_constants.py +0 -125
  143. package/hooks/workflow/doc_gist_auto_publish.py +0 -144
  144. package/hooks/workflow/md_to_html_companion.py +0 -358
  145. package/hooks/workflow/test_doc_gist_auto_publish.py +0 -117
  146. package/hooks/workflow/test_md_to_html_companion.py +0 -613
  147. package/skills/bugteam/reference/audit-contract.md +0 -163
  148. package/skills/bugteam/scripts/_bugteam_permissions_common.py +0 -455
  149. package/skills/bugteam/scripts/bugteam_scripts_constants/claude_permissions_common_constants.py +0 -69
  150. package/skills/bugteam/scripts/grant_project_claude_permissions.py +0 -280
  151. package/skills/bugteam/scripts/revoke_project_claude_permissions.py +0 -266
  152. package/skills/bugteam/scripts/test__bugteam_permissions_common.py +0 -160
  153. package/skills/bugteam/scripts/test_agent_config_carveout.py +0 -356
  154. package/skills/bugteam/scripts/test_bugteam_permissions_common.py +0 -140
  155. package/skills/doc-gist/CLAUDE.md +0 -25
  156. package/skills/doc-gist/SKILL.md +0 -97
  157. package/skills/doc-gist/references/CLAUDE.md +0 -9
  158. package/skills/doc-gist/references/examples/01-exploration-code-approaches.html +0 -453
  159. package/skills/doc-gist/references/examples/02-exploration-visual-designs.html +0 -515
  160. package/skills/doc-gist/references/examples/03-code-review-pr.html +0 -638
  161. package/skills/doc-gist/references/examples/04-code-understanding.html +0 -491
  162. package/skills/doc-gist/references/examples/05-design-system.html +0 -629
  163. package/skills/doc-gist/references/examples/06-component-variants.html +0 -605
  164. package/skills/doc-gist/references/examples/07-prototype-animation.html +0 -455
  165. package/skills/doc-gist/references/examples/08-prototype-interaction.html +0 -396
  166. package/skills/doc-gist/references/examples/09-slide-deck.html +0 -592
  167. package/skills/doc-gist/references/examples/10-svg-illustrations.html +0 -492
  168. package/skills/doc-gist/references/examples/11-status-report.html +0 -528
  169. package/skills/doc-gist/references/examples/12-incident-report.html +0 -596
  170. package/skills/doc-gist/references/examples/13-flowchart-diagram.html +0 -395
  171. package/skills/doc-gist/references/examples/14-research-feature-explainer.html +0 -381
  172. package/skills/doc-gist/references/examples/15-research-concept-explainer.html +0 -368
  173. package/skills/doc-gist/references/examples/16-implementation-plan.html +0 -702
  174. package/skills/doc-gist/references/examples/17-pr-writeup.html +0 -595
  175. package/skills/doc-gist/references/examples/18-editor-triage-board.html +0 -573
  176. package/skills/doc-gist/references/examples/19-editor-feature-flags.html +0 -663
  177. package/skills/doc-gist/references/examples/20-editor-prompt-tuner.html +0 -722
  178. package/skills/doc-gist/references/examples/21-decision-signoff.html +0 -546
  179. package/skills/doc-gist/references/examples/CLAUDE.md +0 -25
  180. package/skills/doc-gist/references/examples/README.md +0 -5
  181. package/skills/doc-gist/scripts/CLAUDE.md +0 -27
  182. package/skills/doc-gist/scripts/doc_gist_scripts_constants/CLAUDE.md +0 -10
  183. package/skills/doc-gist/scripts/doc_gist_scripts_constants/gist_upload_constants.py +0 -16
  184. package/skills/doc-gist/scripts/gist_upload.py +0 -177
  185. package/skills/doc-gist/scripts/test_gist_upload.py +0 -51
  186. /package/skills/{doc-gist/scripts/doc_gist_scripts_constants → log-audit/scripts/log_audit_constants}/__init__.py +0 -0
@@ -99,6 +99,7 @@ ALL_PUNCTUATION_MARK_GLYPH_PROSE_NAMES: dict[str, tuple[str, ...]] = {
99
99
  MAX_DOCSTRING_STEP_DISPATCH_ISSUES: int = 5
100
100
  MINIMUM_NAMED_LINEAR_STEPS_FOR_DISPATCH_CHECK: int = 2
101
101
  MINIMUM_TOKENS_FOR_DISPATCH_CALLEE: int = 2
102
+ MINIMUM_RANGE_ARGS_FOR_SPAN: int = 2
102
103
  MAX_DOCSTRING_UNDEFINED_CONSTANT_ISSUES: int = 3
103
104
  MAX_DOCSTRING_RETURNS_PLURAL_CARDINALITY_ISSUES: int = 5
104
105
  SINGLE_DICT_KEY_COUNT_FOR_PLURAL_CARDINALITY_DRIFT: int = 1
@@ -218,6 +219,9 @@ DOCSTRING_RUNON_SENTENCE_WORD_LIMIT: int = 30
218
219
  MAX_DOCSTRING_RUNON_SENTENCE_ISSUES: int = 5
219
220
  ALL_DOCSTRING_RUNON_JOINER_MARKERS: tuple[str, ...] = ("—", " -- ", ";")
220
221
  DOCSTRING_RUNON_SENTENCE_BOUNDARY_PATTERN: re.Pattern[str] = re.compile(r"(?<=[.!?])\s+")
222
+ DOCSTRING_NARRATIVE_PROSE_LINE_LIMIT: int = 6
223
+ DOCSTRING_NARRATIVE_LINE_JOIN_SEPARATOR: str = " "
224
+ MAX_DOCSTRING_PROSE_WALL_ISSUES: int = 5
221
225
 
222
226
  ALL_DOCSTRING_NO_CONSUMER_CLAIM_PHRASES: tuple[str, ...] = (
223
227
  "no consumer reads",
@@ -22,8 +22,8 @@ ALL_CONFIG_DIRECTORY_NAMES = frozenset(
22
22
  "pr_converge_skill_constants",
23
23
  "pr_converge_scripts_constants",
24
24
  "bugteam_scripts_constants",
25
- "doc_gist_scripts_constants",
26
25
  "implement_scripts_constants",
26
+ "log_audit_constants",
27
27
  "dev_env_scripts_constants",
28
28
  }
29
29
  )
@@ -19,7 +19,6 @@ ALL_EXEMPT_PATH_FRAGMENTS: tuple[str, ...] = (
19
19
  "windows_safe_rmtree.py",
20
20
  "windows_filesystem.py",
21
21
  "session_env_cleanup.py",
22
- "_md_to_html_blocker_test_support.py",
23
22
  "teardown_worktrees.py",
24
23
  )
25
24
 
@@ -62,9 +62,4 @@ ALL_POST_HOSTED_HOOK_ENTRIES: tuple[PostHostedHookEntry, ...] = (
62
62
  script_relative_path="workflow/auto_formatter.py",
63
63
  is_blocking=False,
64
64
  ),
65
- PostHostedHookEntry(
66
- script_relative_path="workflow/doc_gist_auto_publish.py",
67
- extra_argument_relative_paths=(PLUGIN_ROOT_PLACEHOLDER,),
68
- is_blocking=False,
69
- ),
70
65
  )
@@ -168,7 +168,7 @@ class PostDispatcherDecision:
168
168
  all_block_reasons: All block reasons from blocking hooks, in run order.
169
169
  all_non_block_stdout: Stdout from hooks that did not emit a block
170
170
  decision, concatenated in run order. Preserved so informational
171
- output (such as the doc-gist htmlpreview URL) reaches the harness
171
+ output (such as an auto-formatter notice) reaches the harness
172
172
  on both the allow and block paths.
173
173
  """
174
174
 
@@ -5,11 +5,11 @@ hook as its own subprocess (the production path), records each hook's block
5
5
  decision, computes the expected aggregate, then runs the dispatcher on the same
6
6
  payload and asserts an equal block-or-allow decision and the union of reasons.
7
7
 
8
- Three focused tests pin the side-effecting behavior the dispatcher must not
8
+ Two focused tests pin the side-effecting behavior the dispatcher must not
9
9
  change: the formatter formats only on a Write of a file git does not yet track
10
- and never blocks; the type-checker still blocks on a real type error when run
11
- through the dispatcher; and non-block stdout from side-effect hooks (such as
12
- the doc-gist htmlpreview URL) survives on both the allow and block paths.
10
+ and never blocks; and the type-checker still blocks on a real type error when
11
+ run through the dispatcher. Two more tests pin that non-block stdout from a
12
+ side-effect hook survives on both the allow and block paths.
13
13
 
14
14
  Crash and early-exit tests exercise the aggregator directly: an early hook
15
15
  crash before mypy does not drop mypy's block, a non-blocking hook crash leaves
@@ -20,6 +20,7 @@ from __future__ import annotations
20
20
 
21
21
  import importlib.util
22
22
  import json
23
+ import os
23
24
  import subprocess
24
25
  import sys
25
26
  from pathlib import Path
@@ -33,9 +34,6 @@ _VALIDATION_DIR_STR = str(Path(__file__).resolve().parent)
33
34
  if _VALIDATION_DIR_STR not in sys.path:
34
35
  sys.path.insert(0, _VALIDATION_DIR_STR)
35
36
 
36
- from hooks_constants.doc_gist_auto_publish_constants import ( # noqa: E402, I001
37
- HOOK_SUBPROCESS_TIMEOUT_SECONDS as DOC_GIST_TIMEOUT_SECONDS,
38
- )
39
37
  from hooks_constants.post_tool_use_dispatcher_constants import ( # noqa: E402, I001
40
38
  ALL_POST_HOSTED_HOOK_ENTRIES,
41
39
  BLOCK_DECISION,
@@ -95,6 +93,26 @@ def _resolve_extra_arguments(each_entry: PostHostedHookEntry) -> list[str]:
95
93
  return resolved_arguments
96
94
 
97
95
 
96
+ def _environment_without_git_hook_variables() -> dict[str, str]:
97
+ """Return the process environment with git's hook-scoped variables removed.
98
+
99
+ Git exports variables such as GIT_DIR and GIT_INDEX_FILE to hook
100
+ processes. When this suite runs under a git hook (the pre-commit gate
101
+ replays staged test files), those variables leak into the hook
102
+ subprocesses and point their project discovery at the wrong checkout.
103
+ Stripping them restores the cwd-based discovery the hooks use in
104
+ production.
105
+
106
+ Returns:
107
+ A copy of the environment without any GIT_-prefixed variable.
108
+ """
109
+ return {
110
+ each_name: each_value
111
+ for each_name, each_value in os.environ.items()
112
+ if not each_name.startswith("GIT_")
113
+ }
114
+
115
+
98
116
  def _run_hook_subprocess(
99
117
  each_entry: PostHostedHookEntry, payload_text: str
100
118
  ) -> subprocess.CompletedProcess[str]:
@@ -116,6 +134,7 @@ def _run_hook_subprocess(
116
134
  capture_output=True,
117
135
  text=True,
118
136
  encoding="utf-8",
137
+ env=_environment_without_git_hook_variables(),
119
138
  )
120
139
 
121
140
 
@@ -135,6 +154,7 @@ def _run_dispatcher(payload_text: str) -> subprocess.CompletedProcess[str]:
135
154
  capture_output=True,
136
155
  text=True,
137
156
  encoding="utf-8",
157
+ env=_environment_without_git_hook_variables(),
138
158
  )
139
159
 
140
160
 
@@ -267,10 +287,10 @@ def _post_tool_use_dispatcher_harness_timeout_seconds() -> int:
267
287
  def _hosted_hooks_worst_case_internal_seconds() -> int:
268
288
  """Return the summed worst-case internal subprocess budget of the hosted hooks.
269
289
 
270
- The three hosted hooks run sequentially in one dispatcher process. The
271
- type-checker, the formatter (its slower JS path), and the doc-gist publisher
272
- each carry their own internal subprocess timeout; their sum is the dispatcher
273
- process's worst-case runtime when each hook runs to its own ceiling.
290
+ The hosted hooks run sequentially in one dispatcher process. The
291
+ type-checker and the formatter (its slower JS path) each carry their own
292
+ internal subprocess timeout; their sum is the dispatcher process's
293
+ worst-case runtime when each hook runs to its own ceiling.
274
294
 
275
295
  Returns:
276
296
  The summed worst-case internal subprocess seconds across the hosted hooks.
@@ -281,21 +301,17 @@ def _hosted_hooks_worst_case_internal_seconds() -> int:
281
301
  auto_formatter.PYTHON_FORMAT_TIMEOUT_SECONDS,
282
302
  auto_formatter.JS_FORMAT_TIMEOUT_SECONDS,
283
303
  )
284
- return (
285
- mypy_validator.MYPY_TIMEOUT_SECONDS
286
- + slowest_formatter_seconds
287
- + DOC_GIST_TIMEOUT_SECONDS
288
- )
304
+ return mypy_validator.MYPY_TIMEOUT_SECONDS + slowest_formatter_seconds
289
305
 
290
306
 
291
307
  def test_dispatcher_harness_timeout_clears_summed_hosted_hook_budgets() -> None:
292
308
  """The dispatcher harness timeout exceeds the summed worst-case hosted-hook budget.
293
309
 
294
- The three hosted hooks run sequentially under one harness timeout. When the
310
+ The hosted hooks run sequentially under one harness timeout. When the
295
311
  harness timeout does not clear their summed worst-case internal budgets, a
296
312
  near-full slow type-check run consumes the budget and the harness kills the
297
- dispatcher before the formatter and doc-gist publisher get their turn. The
298
- harness timeout must exceed the sum so every hosted hook runs to completion.
313
+ dispatcher before the formatter gets its turn. The harness timeout must
314
+ exceed the sum so every hosted hook runs to completion.
299
315
  """
300
316
  harness_timeout_seconds = _post_tool_use_dispatcher_harness_timeout_seconds()
301
317
  summed_internal_seconds = _hosted_hooks_worst_case_internal_seconds()
@@ -303,7 +319,7 @@ def test_dispatcher_harness_timeout_clears_summed_hosted_hook_budgets() -> None:
303
319
  "PostToolUse dispatcher harness timeout "
304
320
  f"({harness_timeout_seconds}s) must exceed the summed worst-case internal "
305
321
  f"budgets of the hosted hooks ({summed_internal_seconds}s), or a slow "
306
- "type-check run starves the formatter and doc-gist publisher."
322
+ "type-check run starves the formatter."
307
323
  )
308
324
 
309
325
 
@@ -315,14 +331,14 @@ def test_clean_edit_of_plain_text_allows() -> None:
315
331
 
316
332
 
317
333
  def test_clean_write_of_nonexistent_path_allows() -> None:
318
- """Dispatcher allows a Write whose path does not exist (mypy and doc-gist skip)."""
334
+ """Dispatcher allows a Write whose path does not exist (mypy skips)."""
319
335
  missing_path = str(_VALIDATION_DIR / "does_not_exist_dispatcher_probe.txt")
320
336
  payload_text = _write_payload(missing_path, "hello world\n")
321
337
  _assert_dispatcher_matches_individual_hooks(payload_text)
322
338
 
323
339
 
324
- def test_edit_of_non_html_skips_doc_gist_allows() -> None:
325
- """Dispatcher allows an Edit of an existing non-HTML file with no sentinel."""
340
+ def test_edit_of_non_html_allows() -> None:
341
+ """Dispatcher allows an Edit of an existing non-HTML file."""
326
342
  existing_path = str(Path(__file__).resolve())
327
343
  payload_text = _edit_payload(existing_path, "old", "new")
328
344
  _assert_dispatcher_matches_individual_hooks(payload_text)
@@ -433,10 +449,10 @@ def test_formatter_formats_only_untracked_write_and_never_blocks(tmp_path: Path)
433
449
  def test_non_block_stdout_preserved_in_aggregator_allow_path() -> None:
434
450
  """Aggregator preserves non-block hook stdout on the allow path.
435
451
 
436
- A side-effect hook (such as doc_gist_auto_publish) writes informational
437
- text to stdout without emitting a block decision. The aggregator must carry
438
- that text into all_non_block_stdout so the dispatcher can write it to the
439
- real stdout on the allow path.
452
+ A side-effect hook writes informational text to stdout without emitting a
453
+ block decision. The aggregator must carry that text into
454
+ all_non_block_stdout so the dispatcher can write it to the real stdout on
455
+ the allow path.
440
456
  """
441
457
  informational_text = "https://htmlpreview.github.io/?https://gist.github.com/abc/123"
442
458
  all_results = [
@@ -459,7 +475,7 @@ def test_non_block_stdout_preserved_in_aggregator_allow_path() -> None:
459
475
  def test_non_block_stdout_preserved_in_aggregator_block_path() -> None:
460
476
  """Aggregator preserves non-block hook stdout even when another hook blocks.
461
477
 
462
- When mypy_validator blocks and doc_gist_auto_publish wrote informational
478
+ When mypy_validator blocks and a side-effect hook wrote informational
463
479
  text, both the block reason and the informational text survive in the
464
480
  aggregated decision so _emit_block_decision can forward both to stdout.
465
481
  """
@@ -512,9 +528,9 @@ def test_early_hook_crash_does_not_drop_later_blocking_hook_block() -> None:
512
528
  def test_non_blocking_hook_crash_leaves_decision_allow() -> None:
513
529
  """A crash in a non-blocking hook does not change an allow to a block.
514
530
 
515
- A side-effect hook such as auto_formatter or doc_gist_auto_publish carries
516
- is_blocking=False. Its crash must not surface a blocking signal — the
517
- aggregated decision stays allow.
531
+ A side-effect hook such as auto_formatter carries is_blocking=False. Its
532
+ crash must not surface a blocking signal — the aggregated decision stays
533
+ allow.
518
534
  """
519
535
  all_results = [
520
536
  PostHostedHookResult(captured_stdout="", did_crash=True, is_blocking=False),
@@ -566,12 +582,12 @@ def test_block_path_emits_single_parseable_json_object_on_stdout() -> None:
566
582
  )
567
583
  try:
568
584
  payload_text = _write_payload(str(type_error_file), type_error_file.read_text())
569
- direct_block, _direct_reason = _parse_block_decision(
570
- _run_hook_subprocess(ALL_POST_HOSTED_HOOK_ENTRIES[0], payload_text)
571
- )
585
+ completed_direct = _run_hook_subprocess(ALL_POST_HOSTED_HOOK_ENTRIES[0], payload_text)
586
+ direct_block, _direct_reason = _parse_block_decision(completed_direct)
572
587
  assert direct_block, (
573
588
  "Precondition failed: mypy_validator did not block a real type error directly. "
574
- "Is mypy installed and the file inside the git project?"
589
+ "Is mypy installed and the file inside the git project?\n"
590
+ f"stdout: {completed_direct.stdout!r}\nstderr: {completed_direct.stderr!r}"
575
591
  )
576
592
 
577
593
  dispatcher_result = _run_dispatcher(payload_text)
@@ -1,22 +1,16 @@
1
1
  # hooks/workflow
2
2
 
3
- PostToolUse hooks that trigger side-effects after Claude writes a file. They do not block writes; they produce companion artifacts or publish content automatically.
3
+ PostToolUse hooks that run after Claude writes a file. They do not block the write; they run follow-up side effects reformatting the written file and resetting the investigation tracker.
4
4
 
5
5
  ## Key files
6
6
 
7
7
  | File | Event | What it does |
8
8
  |---|---|---|
9
9
  | `auto_formatter.py` | PostToolUse (Write/Edit) | Runs the project's auto-formatter (ruff, prettier, etc.) on the written file and sends a desktop notification when formatting changes are applied |
10
- | `doc_gist_auto_publish.py` | PostToolUse (Write on `.html` files) | When an `.html` file has the `<!-- @publish-as-gist -->` marker, uploads it as a secret GitHub Gist and prints the htmlpreview URL into the harness output |
11
- | `md_to_html_companion.py` | PostToolUse (Write/Edit on `.md` files) | Generates a styled `.html` companion file from the `.md` source with dark-mode styling |
12
10
  | `investigation_tracker_reset.py` | PostToolUse | Resets the investigation tracker state after a tool call |
13
11
  | `test_auto_formatter.py` | — | Tests for `auto_formatter.py` |
14
- | `test_doc_gist_auto_publish.py` | — | Tests for `doc_gist_auto_publish.py` |
15
- | `test_md_to_html_companion.py` | — | Tests for `md_to_html_companion.py` |
16
12
 
17
13
  ## Conventions
18
14
 
19
- - All three main hooks exit 0 even on failure — they log warnings to stderr but do not break Claude's flow.
20
- - `doc_gist_auto_publish.py` checks for the `<!-- @publish-as-gist -->` marker before doing any work; HTML files without it are ignored.
21
- - Constants (marker text, blocked URL schemes, gist upload script path) live in `hooks_constants/doc_gist_auto_publish_constants.py` and `hooks_constants/html_companion_constants.py`.
15
+ - `auto_formatter.py` exits 0 even on failure — it logs warnings to stderr but does not break Claude's flow.
22
16
  - Tests run with `python -m pytest workflow/test_<name>.py`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "1.83.0",
3
+ "version": "1.85.0",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {
package/rules/CLAUDE.md CHANGED
@@ -33,7 +33,7 @@ Rule files installed into `~/.claude/rules/` by `bin/install.mjs`. Claude Code l
33
33
  | `package-inventory-stale-entry.md` | A new production code file added to a directory carries an entry in that directory's `README.md`/`CLAUDE.md` file inventory |
34
34
  | `paired-test-coverage.md` | A public function omitted by a module's established paired test suite must get a behavioral test |
35
35
  | `parallel-tools.md` | Make all independent tool calls in a single response |
36
- | `plain-illustrative-docstrings.md` | Public docstring narrative reads plainly and paints a concrete scene a general developer follows on first read; a run-on backstop hook plus Category O9 audit enforce it |
36
+ | `plain-illustrative-docstrings.md` | Public docstring narrative reads plainly and shows behavior with a diagram block (a `::` example or a doctest), painting a concrete scene a general developer follows on first read; a run-on backstop hook, a prose-wall backstop hook, and Category O9 audit enforce it |
37
37
  | `plain-language.md` | Everyday words, short active sentences, lead with the answer |
38
38
  | `prompt-workflow-context-controls.md` | Keep prompt-workflow instruction layers small and stable; load heavy skills on demand |
39
39
  | `research-mode.md` | Three anti-hallucination constraints: say "I don't know", verify with citations, quote for factual grounding |
@@ -30,7 +30,7 @@ Read the body and the docstring side by side:
30
30
  - **Returns-clause cardinality.** A `Returns:` clause that names a dict-key prefix family with a plural noun (`the sheen stops`) matches the count of keys in that family in the returned dict literal. When the dict holds one key in the family (`sheen_mid`), the noun is singular (`the sheen stop`); a plural noun there claims two or more entries the dict does not hold. The `check_docstring_returns_plural_cardinality` gate blocks the single-key-with-plural-noun form of this drift at Write/Edit time.
31
31
  - **Length-constant superlative vs exact gate.** A module docstring that describes an integer `*_LENGTH` constant with a superlative or range word (`the longest color string the swatch accepts`, `no longer than`) matches how the code consumes the constant. When the only consumer compares `len(...)` against the constant with `==`/`!=` — an exact-length gate where every other length is rejected, not accepted at a shorter length — the superlative prose claims a range of accepted lengths the code never allows. State the exact required length (`the exact #AARRGGBB length`), not a longest/range form. The `check_docstring_length_constant_superlative_vs_exact_gate` gate blocks this drift at Write/Edit time, scanning the constant module's package tree so it sees a consumer that lives in a sibling module; a constant genuinely used as a ceiling (`len(x) <= LIMIT`) is left alone.
32
32
  - **Args single-line scope vs span body.** An `Args:` entry that scopes a finding to one named line (`a finding blocks only when its block-anchor line is among the changed lines`) matches the line breadth the body scopes by. When the body builds a `range(...)` span over the finding's source lines and scopes it through a span-intersection scoper that blocks when any line of the span is among the changed lines, the single-line Args wording understates the scope: an edit touching a non-anchor line of the span still blocks. State the Args entry on the same span breadth the body uses (`a finding blocks when any line of its block span is among the changed lines`). The `check_docstring_args_single_line_scope_vs_span` gate blocks the single-line-Args-over-span-body form of this drift at Write/Edit time.
33
- - **Cardinal-count enumerations.** A docstring that states a count of an outcome family (`the four outcome branches`) and lists those members names every member of that family the module references. When the module imports and exercises a fifth `OUTCOME_*` constant the summary leaves out, the count and the list both under-describe the code. The `check_docstring_cardinal_count_matches_constant_family` gate blocks this drift — a cardinal-count docstring that names two or more members of a referenced `UPPER_SNAKE` constant family, leaves at least one referenced member out, and states a count below the family size — at Write/Edit time, on test modules as well as production modules.
33
+ - **Cardinal-count enumerations.** A docstring that states a count of an outcome family (`the four outcome branches`) and lists those members names every member of that family the module references. When the module imports and exercises a fifth `OUTCOME_*` constant the summary leaves out, the count and the list both under-describe the code. The `check_docstring_cardinal_count_matches_constant_family` gate blocks this drift — a cardinal-count docstring that names two or more members of a referenced `UPPER_SNAKE` constant family, leaves at least one referenced member out, and states a count that differs from the family size (a count above it and one below it both trip) — at Write/Edit time, on test modules as well as production modules.
34
34
  - **Raises-clause reachability for `LargeZipFile`.** A `Raises:` clause that names `zipfile.LargeZipFile` matches a writer the body opens with ZIP64 forbidden. `zipfile` raises `LargeZipFile` only when an entry needs ZIP64 and `allowZip64` is False; a function that opens its `zipfile.ZipFile` writer in a write mode (`w`/`a`/`x`) with `allowZip64` at its default of True allows ZIP64 and never raises it, so the clause documents an unreachable exception. Drop the entry, or pass `allowZip64=False` when forbidding ZIP64 is the goal. The `check_docstring_raises_unraisable_largezipfile` gate blocks the default-ZIP64-writer form of this drift at Write/Edit time; a writer that forbids ZIP64 on any open, a read-only open, and a function that opens no writer are all left alone.
35
35
  - **Module summary scope versus data-schema constants.** A module whose one-line docstring scopes its contents to user-facing text (`User-facing strings: CLI flag names, help text, and log messages`) 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 — the module-responsibility drift the repo flags. 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, and fires only when the summary claims a user-facing-text scope and names no data-schema or runtime-config category, so a summary that already names `field keys`, `schema`, or `runtime config` passes.
36
36
  - **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 (a run-mode flag such as `is_dry_run = not is_execute` at each write site), the description states the run-mode meaning, not a per-record outcome. A field named `is_dry_run` documented as `True when no STP was written` reads as a per-record write result, but the value tracks the run mode, so a record that writes no file during an execute run still stores `False`. State the run-mode meaning the assignment gives the field. The `check_docstring_field_runmode_outcome` gate blocks the single-file shape of this drift at Write/Edit time — an `Attributes:` entry for a run-mode flag field (a name carrying `dry_run`) whose description carries a per-record write-outcome phrase and no run-mode phrase. The assignment that sets the field sits in another module, out of reach of that single-file gate, so any shape the gate cannot key on stays an O6 audit-lane judgment finding.
@@ -22,7 +22,7 @@ The `env_var_table_code_drift_blocker.py` hook runs on every Write, Edit, and Mu
22
22
  3. Resolves the named code file under the repository root (the nearest `.git`-bearing ancestor of the markdown file) and reads its source.
23
23
  4. Blocks the write when the file resolves yet its source never references the variable name. For an Edit, drift the file already held on an untouched row is excluded, so only drift the edit introduces is reported.
24
24
 
25
- The check stays quiet for a row whose code file resolves nowhere under the repository root (it cannot prove the drift), a row whose second cell holds no code-file path, a table row inside a fenced code block, and any test file.
25
+ The check stays quiet for a row whose code file resolves nowhere under the repository root (it cannot prove the drift), a row whose second cell holds no code-file path, and a table row inside a fenced code block.
26
26
 
27
27
  ## Why this is a hook, not a lint pass
28
28
 
@@ -10,7 +10,7 @@ paths:
10
10
 
11
11
  # New Production File Absent From Its Package Inventory
12
12
 
13
- **When this applies:** Any Write that creates a new production code file (`.py`, `.mjs`, `.js`, `.ts`, `.ps1`, `.sh`) in a directory whose sibling `README.md` or `CLAUDE.md` already names two or more of the directory's files in backticks, or in a skill's `scripts/` subdirectory whose parent `SKILL.md` Layout table already names two or more of those scripts.
13
+ **When this applies:** Any Write that creates a new production code file (`.py`, `.mjs`, `.js`, `.ts`, `.ps1`, `.sh`) in a directory whose sibling `README.md` or `CLAUDE.md` already names two or more of the directory's files in backticks, in a directory whose own `SKILL.md` names two or more of the directory's files, or in a skill's `scripts/` subdirectory whose parent `SKILL.md` Layout table already names two or more of those scripts.
14
14
 
15
15
  ## Rule
16
16
 
@@ -23,7 +23,7 @@ Two complementary checks in `code_rules_paired_test.py` (both dispatched from `c
23
23
  `check_public_function_missing_paired_test` runs on a production Python write or edit and flags a public function when all of these hold:
24
24
 
25
25
  1. The target is production code — not a test module, hook infrastructure, config module, migration, workflow registry, or `__init__.py`.
26
- 2. A stem-matched test file (`test_<stem>.py` or `<stem>_test.py`) exists for the module.
26
+ 2. A stem-matched test file exists for the module — `test_<stem>.py` or `<stem>_test.py` beside the module, or `test_<stem>.py` under an ancestor `tests/` directory.
27
27
  3. That suite already exercises the module — referencing at least one public function the module defines, or referencing one of its private (underscore-prefixed) helper functions by name — the signature of a maintained per-module suite rather than a placeholder or unrelated test file.
28
28
  4. The public function is referenced by no test file in the directory that holds the stem-matched test.
29
29
 
@@ -19,6 +19,39 @@ Two shapes break the standard. Hold the prose clear of both:
19
19
  - **No machinery nouns stacked into a wall.** A sentence that chains abstract machinery terms (`the SIGINT install/restore/installability check, the atexit terminal-record registration, and the interrupted-run finalizer`) names parts without painting a scene. Name what the reader sees and why it matters.
20
20
  - **No defining by negation.** Prose that explains a thing by what it is not (`the non-promoter-specific machinery`) leaves the reader without a picture. Say what the thing is.
21
21
 
22
+ ## Shape: a summary line, then a diagram
23
+
24
+ The clearest way to be illustrative is to show a worked example, not describe one. A docstring that lands well reads in four parts:
25
+
26
+ 1. **One summary line** that says what the code does.
27
+ 2. **A diagram block** that carries the explanation by sight — a reStructuredText literal block (a line ending in `::`, then an indented example) or a doctest (`>>>`). Give the concrete input, mark the outcome, and add `ok:` / `flag:` contrast lines where a pass-and-fail pair makes the point. Keep the diagram clear of a bare number or an ALL-CAPS `NAME = value` line, which read as a magic value or a stray constant to a line-based lint pass.
28
+ 3. **A couple of short narrative lines** after the block — two or three at most.
29
+ 4. **The Google `Args:` / `Returns:` sections.**
30
+
31
+ Keep the wording neutral in the diagram and the prose: two names "contradict" or "clash", two names "agree". Skip words that pass judgment.
32
+
33
+ Canonical example:
34
+
35
+ ```
36
+ Flag a boolean assignment whose target and callee assert opposite polarity.
37
+
38
+ ::
39
+
40
+ is_inside_allowed = _point_hits_any_forbidden(...)
41
+ ^^^^^^^ ^^^^^^^^^
42
+ allowed vs. forbidden ⚠ the two names clash
43
+ ok: is_inside_allowed = _point_inside_allowed_region(...)
44
+ flag: is_inside_allowed = _point_hits_any_forbidden(...)
45
+
46
+ The target token and the callee token contradict each other, so the reader
47
+ cannot tell which name states the truth. Rename the callee to a neutral form
48
+ the two names agree on at every call site.
49
+ ```
50
+
51
+ The live version of this docstring sits on `check_polarity_name_contradiction` in `packages/claude-dev-env/hooks/blocking/code_rules_naming_collection.py`.
52
+
53
+ A short narrative with no diagram is fine when a couple of plain sentences carry the whole picture. The diagram earns its place once the explanation grows past what two or three lines hold — the moment a wall of prose starts to form.
54
+
22
55
  ## What to check before you write the docstring
23
56
 
24
57
  Read the narrative back as a stranger would:
@@ -53,7 +86,8 @@ one — and you're debugging blind.
53
86
 
54
87
  Two surfaces carry this standard:
55
88
 
56
- - **Hook (the run-on backstop).** `check_docstring_runon_sentence` in `packages/claude-dev-env/hooks/blocking/code_rules_docstrings.py` flags the one mechanical mark of a wall: a single narrative sentence that is both over the word limit and joined by an em-dash or a semicolon. A hook cannot judge whether prose paints a picture, so it catches only this structural mark.
89
+ - **Hook (the run-on backstop).** `check_docstring_runon_sentence` in `packages/claude-dev-env/hooks/blocking/code_rules_docstrings.py` flags the one mechanical mark of a wall: a single narrative sentence that is both over the word limit and joined by an em-dash or a semicolon. A hook cannot judge whether prose paints a picture, so it catches only this structural mark. It reads the narrative through a shared partition that sets aside any `::` literal block and any doctest, so a diagram's own arrows and dashes never count against the sentence.
90
+ - **Hook (the prose-wall backstop).** `check_docstring_prose_wall_without_illustration` in the same module flags a narrative that runs more than six prose lines with no diagram block. It marks the wall so the writer shows the behavior with a `::` example or a doctest and trims the prose to a few short lines. It cannot judge whether the diagram illustrates well; that stays with the audit lane.
57
91
  - **Audit (the judgment lane).** Category O sub-bucket O9 in `packages/claude-dev-env/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md` carries the illustrative-and-brief judgment the hook cannot. The audit teammate reads each changed docstring's narrative and asks whether a general developer follows it on the first read.
58
92
 
59
93
  ## Why
package/skills/CLAUDE.md CHANGED
@@ -35,6 +35,11 @@ Skills install to `~/.claude/skills/<skill-name>/` via `packages/claude-dev-env/
35
35
  - `pr-consistency-audit` — cross-file consistency check on a PR diff
36
36
  - `copilot-review` — requests and polls a GitHub Copilot review
37
37
  - `findbugs` / `fixbugs` — find bugs then fix them in separate passes
38
+ - `reviewer-gates` — availability gates for external reviewers (opt-out parse, Copilot quota, Bugbot trigger/detect)
39
+ - `pr-scope-resolve` — one resolution ladder for a PR-loop skill's audit/fix target
40
+ - `pr-fix-protocol` — fix, reply, and resolve reviewer findings; the unresolved-thread sweep
41
+ - `post-audit-findings` — publishes an audit pass as one GitHub PR review
42
+ - `pr-loop-lifecycle` — opens and closes a PR-loop run (grant, teardown, PR description, revoke, report)
38
43
  - `code` — strict-mode code generation session
39
44
 
40
45
  **Research and discovery**
@@ -62,5 +67,5 @@ Skills install to `~/.claude/skills/<skill-name>/` via `packages/claude-dev-env/
62
67
  - `pre-compact` — compact-safe session handoff
63
68
  - `qbug` — required baseline PR audit; one clean-coder subagent loops audit → fix → commit → push until clean or stuck
64
69
  - `skill-builder` — complete skill-building lifecycle
65
- - `doc-gist` — uploads an HTML file as a secret gist
66
70
  - `auditing-claude-config` — audits a Claude Code setup for context-budget waste and produces a migration table with savings
71
+ - `log-audit` — background agent that audits this repo's own logs for recurring errors and timing regressions and files grouped fixes
@@ -21,6 +21,7 @@ Shared infrastructure for the PR audit-fix loop used by `bugteam` and `pr-conver
21
21
  | `packages/claude-dev-env/skills/_shared/pr-loop/scripts/write_fix_outcomes.py` | Writes the per-loop fix outcome XML into the workspace. |
22
22
  | `packages/claude-dev-env/skills/_shared/pr-loop/scripts/preflight_worktree.py` | Verifies the working directory is a healthy worktree for the target PR's repo. |
23
23
  | `packages/claude-dev-env/skills/_shared/pr-loop/scripts/teardown_worktrees.py` | Removes loop worktrees on clean exit. |
24
+ | `packages/claude-dev-env/skills/_shared/pr-loop/scripts/write_handoff.py` | Writes durable resume-handoff files under `~/.claude/runtime/pr-loop/<run-name>/` at each converge checkpoint. |
24
25
  | `packages/claude-dev-env/skills/_shared/pr-loop/scripts/_path_resolver.py` | Resolves workspace and worktree paths from PR metadata. |
25
26
  | `packages/claude-dev-env/skills/_shared/pr-loop/scripts/_cli_utils.py` | Shared CLI argument parsing helpers. |
26
27
  | `packages/claude-dev-env/skills/_shared/pr-loop/scripts/_xml_utils.py` | XML serialization helpers. |
@@ -13,6 +13,7 @@ Python scripts that run the PR audit-fix loop at runtime. Both `bugteam` and `pr
13
13
  | `write_fix_outcomes.py` | Writes per-loop fix outcome XML into the workspace. |
14
14
  | `preflight_worktree.py` | Verifies the working directory is a healthy git worktree for the target PR's repo. Supports `--mode strict` to abort when the repo does not match. |
15
15
  | `teardown_worktrees.py` | Removes per-PR worktrees after a clean loop exit. |
16
+ | `write_handoff.py` | Writes durable resume-handoff files under the run's `~/.claude/runtime/pr-loop` directory at each converge checkpoint. |
16
17
  | `_path_resolver.py` | Resolves workspace and worktree paths from PR owner, repo, and number. |
17
18
  | `_cli_utils.py` | Shared CLI argument parsing helpers (argparse wrappers). |
18
19
  | `_xml_utils.py` | XML serialization helpers for outcome files. |
@@ -9,6 +9,7 @@ Python package of named constants for the `pr-loop` shared scripts. All constant
9
9
  | `__init__.py` | Package marker. |
10
10
  | `path_resolver_constants.py` | Path template strings and format constants: workspace directory naming, worktree directory name, diff patch filename pattern, outcome XML filename patterns, fix status values, audit constraint texts, audit category entries, fix execution steps, and fix constraint texts. |
11
11
  | `preflight_constants.py` | Constants used by `preflight_worktree.py` for exit codes and output marker strings. |
12
+ | `handoff_constants.py` | Filename, path-segment, and template constants for the durable handoff writer. |
12
13
 
13
14
  ## Usage
14
15
 
@@ -0,0 +1,45 @@
1
+ """Filename, path-segment, and template constants for the durable handoff writer.
2
+
3
+ Consumed by `write_handoff.py`, which writes each pr-loop run's resume-handoff
4
+ files under `~/.claude/runtime/pr-loop/<run-name>/`.
5
+ """
6
+
7
+ ALL_HANDOFF_DIR_SEGMENTS = (".claude", "runtime", "pr-loop")
8
+ HANDOFF_JSON_FILENAME = "handoff.json"
9
+ HANDOFF_MARKDOWN_FILENAME = "HANDOFF.md"
10
+ STATE_COPY_FILENAME = "state-copy.json"
11
+ ATOMIC_STAGING_SUFFIX = ".tmp"
12
+ HANDOFF_JSON_INDENT = 2
13
+ COMPLETED_STEPS_SEPARATOR = ","
14
+ STEP_LINE_SEPARATOR = "\n"
15
+
16
+ NO_STATE_SNAPSHOT_LINE = (
17
+ "No state snapshot was captured. Rebuild loop state from the PR before resuming."
18
+ )
19
+ DEFAULT_NEXT_STEP_LINE = "Continue from the resume command above."
20
+ NO_STEPS_DONE_LINE = "- (none recorded yet)"
21
+
22
+ HANDOFF_MARKDOWN_TEMPLATE = """# Resume handoff — PR {pr_number} ({head_ref})
23
+
24
+ Goal: drive PR {pr_number} to convergence. This run stopped at phase `{phase}`.
25
+
26
+ ## Resume command
27
+
28
+ Run this exactly:
29
+
30
+ ```
31
+ {resume_command}
32
+ ```
33
+
34
+ ## State file to trust
35
+
36
+ {state_line}
37
+
38
+ ## Steps already done
39
+
40
+ {steps_block}
41
+
42
+ ## Next step
43
+
44
+ {next_step}
45
+ """