claude-dev-env 2.11.0 → 2.13.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 (203) hide show
  1. package/_shared/advisor/reference/sol-rung.md +5 -2
  2. package/_shared/advisor/reference/warm-up.md +1 -0
  3. package/_shared/advisor/scripts/codex_sol_advisor.py +96 -31
  4. package/_shared/advisor/scripts/config/advisor_scripts_constants/sol_advisor_constants.py +5 -0
  5. package/_shared/advisor/scripts/tests/test_codex_sol_advisor.py +173 -11
  6. package/_shared/pr-loop/scripts/code_rules_gate.py +2 -3
  7. package/_shared/pr-loop/scripts/test_code_rules_gate.py +0 -26
  8. package/agents/CLAUDE.md +1 -2
  9. package/agents/pr-description-writer.md +11 -81
  10. package/bin/install.mjs +48 -9
  11. package/bin/install.prune.test.mjs +26 -0
  12. package/bin/install.settings-defaults.test.mjs +60 -33
  13. package/bin/install.test.mjs +2 -4
  14. package/commands/CLAUDE.md +1 -0
  15. package/commands/sr-loop.md +52 -0
  16. package/docs/CLAUDE.md +0 -1
  17. package/docs/references/CLAUDE.md +0 -1
  18. package/hooks/CLAUDE.md +1 -1
  19. package/hooks/blocking/CLAUDE.md +2 -27
  20. package/hooks/blocking/config/CLAUDE.md +1 -13
  21. package/hooks/blocking/conventional_pr_title_gate.py +1 -2
  22. package/hooks/blocking/send_user_file_open_locally_blocker.py +1 -1
  23. package/hooks/blocking/stop_dispatcher.py +4 -4
  24. package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +0 -3
  25. package/hooks/blocking/test_pre_tool_use_dispatcher.py +6 -7
  26. package/hooks/blocking/test_send_user_file_open_locally_blocker.py +4 -3
  27. package/hooks/blocking/test_shared_stdin_adoption.py +0 -2
  28. package/hooks/diagnostic/CLAUDE.md +3 -3
  29. package/hooks/diagnostic/hook_log_extractor.py +2 -36
  30. package/hooks/diagnostic/hook_log_stop_wrapper.py +6 -155
  31. package/hooks/diagnostic/test_hook_log_extractor.py +8 -21
  32. package/hooks/diagnostic/test_hook_log_stop_wrapper.py +3 -331
  33. package/hooks/git-hooks/git_hooks_constants/__init__.py +0 -6
  34. package/hooks/git-hooks/pre_push.py +3 -85
  35. package/hooks/git-hooks/test_pre_push.py +0 -130
  36. package/hooks/hooks.json +0 -35
  37. package/hooks/hooks_constants/CLAUDE.md +0 -9
  38. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +0 -11
  39. package/hooks/hooks_constants/convergence_branch_constants.py +0 -1
  40. package/hooks/hooks_constants/fable_spawn_gate_constants.py +3 -4
  41. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +0 -8
  42. package/hooks/hooks_constants/send_user_file_open_locally_blocker_constants.py +1 -6
  43. package/hooks/hooks_constants/stop_dispatcher_constants.py +0 -1
  44. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +1 -26
  45. package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +0 -5
  46. package/hooks/hooks_constants/test_stop_dispatcher_constants.py +0 -1
  47. package/hooks/lifecycle/CLAUDE.md +1 -3
  48. package/package.json +1 -1
  49. package/rules/CLAUDE.md +0 -2
  50. package/rules/gh-cli-conventions.md +0 -1
  51. package/rules/git-workflow.md +1 -3
  52. package/scripts/CLAUDE.md +0 -1
  53. package/scripts/_code_review_test_support.py +0 -95
  54. package/scripts/dev_env_scripts_constants/code_review_constants.py +9 -98
  55. package/scripts/dev_env_scripts_constants/test_code_review_constants.py +1 -44
  56. package/scripts/invoke_code_review.py +8 -451
  57. package/scripts/test_invoke_code_review.py +5 -61
  58. package/scripts/test_invoke_code_review_cli.py +1 -45
  59. package/scripts/test_invoke_code_review_contract.py +1 -118
  60. package/settings.json +1 -10
  61. package/skills/CLAUDE.md +8 -0
  62. package/skills/autoconverge/reference/CLAUDE.md +1 -1
  63. package/skills/autoconverge/reference/convergence.md +7 -7
  64. package/skills/autoconverge/reference/gotchas.md +3 -10
  65. package/skills/autoconverge/workflow/converge.contract.test.mjs +53 -1388
  66. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +4 -3
  67. package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +33 -493
  68. package/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +15 -97
  69. package/skills/autoconverge/workflow/converge.mjs +201 -238
  70. package/skills/autoconverge/workflow/converge.precatch.test.mjs +1 -1
  71. package/skills/bugteam/CONSTRAINTS.md +1 -1
  72. package/skills/bugteam/SKILL.md +9 -0
  73. package/skills/bugteam/reference/audit-and-teammates.md +10 -6
  74. package/skills/bugteam/test_skill_additions.py +9 -0
  75. package/skills/comments/SKILL.md +65 -0
  76. package/skills/descriptions/SKILL.md +51 -0
  77. package/skills/emergencies/SKILL.md +42 -0
  78. package/skills/imagegen/SKILL.md +21 -0
  79. package/skills/imagegen/scripts/config/__init__.py +1 -0
  80. package/skills/imagegen/scripts/config/constants.py +28 -0
  81. package/skills/imagegen/scripts/imagegen.py +45 -0
  82. package/skills/imagegen/scripts/imagegen_core.py +528 -0
  83. package/skills/imagegen/scripts/test_imagegen.py +204 -0
  84. package/skills/orchestrator/SKILL.md +2 -4
  85. package/skills/plan-to-pr/SKILL.md +6 -7
  86. package/skills/plan-to-pr/reference/final-validation-tasks.md +1 -1
  87. package/skills/plan-to-pr/reference/process-inventory.md +2 -2
  88. package/skills/plan-to-pr/reference/review-loop.md +2 -2
  89. package/skills/plan-to-pr/reference/run-record.schema.json +1 -1
  90. package/skills/plan-to-pr/reference/task-seeds.md +1 -1
  91. package/skills/plan-to-pr/reference/task-ticket.md +3 -4
  92. package/skills/plan-to-pr/scripts/config/constants.py +1 -2
  93. package/skills/plan-to-pr/scripts/test_validate_protocol.py +1 -1
  94. package/skills/plan-to-pr/test_skill_contract.py +2 -2
  95. package/skills/pr-converge/SKILL.md +13 -16
  96. package/skills/pr-converge/reference/progress-checklist.md +6 -5
  97. package/skills/pr-converge/reference/state-schema.md +1 -27
  98. package/skills/pr-loop-cloud-transport/SKILL.md +1 -1
  99. package/skills/pr-loop-cloud-transport/reference/identity-and-hooks.md +3 -3
  100. package/skills/pr-loop-lifecycle/SKILL.md +1 -1
  101. package/skills/pr-loop-lifecycle/reference/teardown-publish-permissions.md +1 -1
  102. package/skills/prototype/SKILL.md +4 -4
  103. package/skills/prototype/reference/honest-limitations.md +4 -4
  104. package/skills/prototype/reference/promotion-tasks.md +2 -2
  105. package/skills/prototype/workflows/promotion.md +4 -6
  106. package/skills/reviews/SKILL.md +85 -0
  107. package/skills/small-cl/SKILL.md +52 -0
  108. package/skills/usage-pause/SKILL.md +1 -1
  109. package/skills/usage-pause/scripts/resolve_usage_window.py +32 -4
  110. package/skills/usage-pause/scripts/test_resolve_usage_window.py +26 -0
  111. package/skills/usage-pause/scripts/usage_pause_constants/resolve_usage_window_constants.py +3 -1
  112. package/agents/code-verifier.md +0 -74
  113. package/docs/PR_DESCRIPTION_GUIDE.md +0 -157
  114. package/docs/references/code-review-enforcement.md +0 -132
  115. package/hooks/blocking/code_review_enforcement_config_bootstrap.py +0 -53
  116. package/hooks/blocking/code_review_gate_deny.py +0 -74
  117. package/hooks/blocking/code_review_pr_create_gate.py +0 -198
  118. package/hooks/blocking/code_review_push_gate.py +0 -145
  119. package/hooks/blocking/code_review_stamp_directory_write_blocker.py +0 -348
  120. package/hooks/blocking/code_review_stamp_store.py +0 -233
  121. package/hooks/blocking/code_review_stamp_write_blocker_parts/__init__.py +0 -7
  122. package/hooks/blocking/code_review_stamp_write_blocker_parts/conftest.py +0 -15
  123. package/hooks/blocking/code_review_stamp_write_blocker_parts/obfuscated_stamp_path_reference.py +0 -212
  124. package/hooks/blocking/code_review_stamp_write_blocker_parts/split_directory_change_into_stamp.py +0 -138
  125. package/hooks/blocking/code_review_stamp_write_blocker_parts/test_obfuscated_stamp_path_reference.py +0 -49
  126. package/hooks/blocking/code_review_stamp_write_blocker_parts/test_split_directory_change_into_stamp.py +0 -38
  127. package/hooks/blocking/code_verifier_spawn_preflight_gate.py +0 -483
  128. package/hooks/blocking/config/code_review_enforcement_constants.py +0 -143
  129. package/hooks/blocking/config/test_code_review_enforcement_constants.py +0 -166
  130. package/hooks/blocking/config/verified_commit_constants.py +0 -160
  131. package/hooks/blocking/config/verified_commit_context_constants.py +0 -21
  132. package/hooks/blocking/config/verified_commit_gate_output_constants.py +0 -14
  133. package/hooks/blocking/conftest.py +0 -123
  134. package/hooks/blocking/convergence_gate_blocker.py +0 -310
  135. package/hooks/blocking/pr_converge_bugteam_enforcer.py +0 -170
  136. package/hooks/blocking/pr_description_body_audit.py +0 -148
  137. package/hooks/blocking/pr_description_command_parser.py +0 -237
  138. package/hooks/blocking/pr_description_enforcer.py +0 -227
  139. package/hooks/blocking/pr_description_pr_number.py +0 -155
  140. package/hooks/blocking/pr_description_proof_of_work.py +0 -385
  141. package/hooks/blocking/pr_description_readability.py +0 -364
  142. package/hooks/blocking/reviewer_spawn_gate.py +0 -182
  143. package/hooks/blocking/test_code_review_enforcement_config_bootstrap.py +0 -62
  144. package/hooks/blocking/test_code_review_gate_deny.py +0 -54
  145. package/hooks/blocking/test_code_review_pr_create_gate.py +0 -199
  146. package/hooks/blocking/test_code_review_push_gate.py +0 -205
  147. package/hooks/blocking/test_code_review_stamp_directory_write_blocker.py +0 -199
  148. package/hooks/blocking/test_code_review_stamp_store.py +0 -205
  149. package/hooks/blocking/test_code_verifier_spawn_preflight_gate.py +0 -710
  150. package/hooks/blocking/test_code_verifier_tools_contract.py +0 -28
  151. package/hooks/blocking/test_convergence_gate_blocker.py +0 -428
  152. package/hooks/blocking/test_pr_converge_bugteam_enforcer.py +0 -311
  153. package/hooks/blocking/test_pr_converge_bugteam_enforcer_state_tolerance.py +0 -184
  154. package/hooks/blocking/test_pr_description_enforcer.py +0 -221
  155. package/hooks/blocking/test_pr_description_enforcer_body_audit.py +0 -247
  156. package/hooks/blocking/test_pr_description_enforcer_body_rules.py +0 -493
  157. package/hooks/blocking/test_pr_description_enforcer_command_parser.py +0 -366
  158. package/hooks/blocking/test_pr_description_enforcer_pr_number.py +0 -159
  159. package/hooks/blocking/test_pr_description_enforcer_proof_gate.py +0 -175
  160. package/hooks/blocking/test_pr_description_enforcer_readability.py +0 -443
  161. package/hooks/blocking/test_pr_description_proof_of_work.py +0 -313
  162. package/hooks/blocking/test_reviewer_spawn_gate.py +0 -230
  163. package/hooks/blocking/test_verdict_directory_write_blocker.py +0 -804
  164. package/hooks/blocking/test_verification_verdict_store.py +0 -974
  165. package/hooks/blocking/test_verified_commit_config_bootstrap.py +0 -67
  166. package/hooks/blocking/test_verified_commit_docs_delta.py +0 -176
  167. package/hooks/blocking/test_verified_commit_gate.py +0 -581
  168. package/hooks/blocking/test_verified_commit_gate_additional_context.py +0 -134
  169. package/hooks/blocking/test_verified_commit_message_accuracy_blocker.py +0 -131
  170. package/hooks/blocking/test_verifier_verdict_minter.py +0 -299
  171. package/hooks/blocking/tests/test_verified_commit_gate.py +0 -41
  172. package/hooks/blocking/verdict_directory_write_blocker.py +0 -687
  173. package/hooks/blocking/verification_verdict_store.py +0 -1039
  174. package/hooks/blocking/verified_commit_config_bootstrap.py +0 -63
  175. package/hooks/blocking/verified_commit_gate.py +0 -113
  176. package/hooks/blocking/verified_commit_gate_parts/CLAUDE.md +0 -28
  177. package/hooks/blocking/verified_commit_gate_parts/__init__.py +0 -1
  178. package/hooks/blocking/verified_commit_gate_parts/command_tokenization.py +0 -174
  179. package/hooks/blocking/verified_commit_gate_parts/deny_payload.py +0 -53
  180. package/hooks/blocking/verified_commit_gate_parts/deny_reason.py +0 -80
  181. package/hooks/blocking/verified_commit_gate_parts/directory_resolution.py +0 -170
  182. package/hooks/blocking/verified_commit_gate_parts/gated_invocations.py +0 -217
  183. package/hooks/blocking/verified_commit_gate_parts/tests/conftest.py +0 -10
  184. package/hooks/blocking/verified_commit_gate_parts/tests/test_command_tokenization.py +0 -94
  185. package/hooks/blocking/verified_commit_gate_parts/tests/test_deny_payload.py +0 -17
  186. package/hooks/blocking/verified_commit_gate_parts/tests/test_deny_reason.py +0 -38
  187. package/hooks/blocking/verified_commit_gate_parts/tests/test_directory_resolution.py +0 -71
  188. package/hooks/blocking/verified_commit_gate_parts/tests/test_gated_invocations.py +0 -96
  189. package/hooks/blocking/verified_commit_message_accuracy_blocker.py +0 -167
  190. package/hooks/blocking/verifier_verdict_minter.py +0 -280
  191. package/hooks/hooks_constants/code_verifier_spawn_preflight_gate_constants.py +0 -64
  192. package/hooks/hooks_constants/convergence_gate_blocker_constants.py +0 -53
  193. package/hooks/hooks_constants/pr_converge_bugteam_enforcer_constants.py +0 -55
  194. package/hooks/hooks_constants/pr_converge_bugteam_enforcer_state.py +0 -67
  195. package/hooks/hooks_constants/pr_description_enforcer_constants.py +0 -167
  196. package/hooks/hooks_constants/pr_description_proof_of_work_constants.py +0 -107
  197. package/hooks/hooks_constants/reviewer_spawn_gate_constants.py +0 -41
  198. package/hooks/hooks_constants/test_pr_description_enforcer_constants.py +0 -292
  199. package/hooks/lifecycle/pr_converge_bugteam_skill_tracker.py +0 -198
  200. package/hooks/lifecycle/test_pr_converge_bugteam_skill_tracker.py +0 -283
  201. package/rules/proof-of-work-pr-comments.md +0 -22
  202. package/rules/verified-commit-gate-skip.md +0 -4
  203. package/scripts/Show-Asset.ps1 +0 -106
@@ -1,385 +0,0 @@
1
- """Audit proof-of-work PR comments and gate gh pr ready on a passing one.
2
-
3
- A proof comment shows the work behind a PR: the command run on real data,
4
- the measured outcomes, the parent issue it advances, an image when the
5
- change is visual, and the gaps the offline proof cannot cover. This module
6
- detects proof-shaped comment bodies, audits the five parts, and blocks
7
- ``gh pr ready`` while no passing proof comment exists on the PR. Every gh
8
- query fails open, so a tooling failure never blocks a command.
9
- """
10
-
11
- from __future__ import annotations
12
-
13
- import json
14
- import logging
15
- import subprocess
16
- import sys
17
- from pathlib import Path
18
-
19
- _hooks_dir = str(Path(__file__).resolve().parent.parent)
20
- if _hooks_dir not in sys.path:
21
- sys.path.insert(0, _hooks_dir)
22
-
23
- from blocking.convergence_gate_blocker import ( # noqa: E402
24
- _repo_flag_arguments,
25
- _resolve_named_identity,
26
- )
27
- from blocking.pr_description_body_audit import _iter_section_headers # noqa: E402
28
- from hooks_constants.pr_description_enforcer_constants import ( # noqa: E402
29
- BULLET_MARKER_PATTERN,
30
- FENCED_CODE_BLOCK_PATTERN,
31
- TABLE_ROW_LINE_PATTERN,
32
- )
33
- from hooks_constants.pr_description_proof_of_work_constants import ( # noqa: E402
34
- ALL_HONEST_GAP_PHRASES,
35
- ALL_PR_DIFF_SUBCOMMANDS,
36
- ALL_PROOF_HEADING_KEYWORDS,
37
- ALL_VISUAL_FILE_SUFFIXES,
38
- COMMENT_BODY_JSON_FIELD,
39
- DIGIT_PATTERN,
40
- GH_API_SUBCOMMAND,
41
- GH_COMMAND_TIMEOUT_SECONDS,
42
- GH_EXECUTABLE,
43
- GH_PAGINATE_FLAG,
44
- GH_SLURP_FLAG,
45
- HEX_COLOR_ADDED_LINE_PATTERN,
46
- IMAGE_EMBED_PATTERN,
47
- ISSUE_REFERENCE_PATTERN,
48
- MAX_DIFF_SCAN_CHARS,
49
- PLAN_LINKAGE_KEYWORD_PATTERN,
50
- PR_COMMENTS_API_PATH_TEMPLATE,
51
- PR_DIFF_NAME_ONLY_FLAG,
52
- PR_READY_GATE_MESSAGE_TEMPLATE,
53
- PR_READY_INVOCATION_PATTERN,
54
- PR_READY_UNDO_FLAG,
55
- PROOF_PART_COMMAND_MESSAGE,
56
- PROOF_PART_HONEST_GAPS_MESSAGE,
57
- PROOF_PART_MEASURED_MESSAGE,
58
- PROOF_PART_PLAN_LINKAGE_MESSAGE,
59
- PROOF_PART_VISUAL_MESSAGE,
60
- )
61
- from hooks_constants.setup_project_paths_constants import UTF8_ENCODING # noqa: E402
62
-
63
- logger = logging.getLogger(__name__)
64
-
65
-
66
- def is_pr_ready_command(command: str) -> bool:
67
- """Decide whether a shell command marks a PR ready for review.
68
-
69
- Args:
70
- command: The raw shell command captured by the hook.
71
-
72
- Returns:
73
- True for a ``gh pr ready`` command without ``--undo``; False for
74
- every other command, including the undo form that returns a PR to
75
- draft and any command that only mentions the phrase ``gh pr ready``
76
- inside a quoted argument or commit message.
77
- """
78
- if PR_READY_UNDO_FLAG in command:
79
- return False
80
- return PR_READY_INVOCATION_PATTERN.search(command) is not None
81
-
82
-
83
- def is_proof_shaped_body(body: str) -> bool:
84
- """Decide whether a comment body presents itself as a proof of work.
85
-
86
- Args:
87
- body: The comment body markdown text.
88
-
89
- Returns:
90
- True when any markdown heading names proof or verification; False
91
- when no heading does.
92
- """
93
- for each_header in _iter_section_headers(body):
94
- header_lower = each_header.lower()
95
- if any(each_keyword in header_lower for each_keyword in ALL_PROOF_HEADING_KEYWORDS):
96
- return True
97
- return False
98
-
99
-
100
- def audit_proof_comment_body(body: str, pr_number: int | None) -> list[str]:
101
- """Audit a proof-shaped comment body for the five proof parts.
102
-
103
- Args:
104
- body: The comment body markdown text.
105
- pr_number: The PR number named in the command, or None when the
106
- command targets the current branch's PR.
107
-
108
- Returns:
109
- One message per missing proof part. Empty when the body carries a
110
- command block, a measured-value element, a plan-linkage sentence,
111
- an honest-gaps statement, and (when the PR diff is visual) an
112
- image embed.
113
- """
114
- is_visual_change = _detect_visual_change(pr_number, None)
115
- return _missing_proof_parts(body, is_visual_change)
116
-
117
-
118
- def evaluate_pr_ready_gate(command: str) -> str | None:
119
- """Decide whether a gh pr ready command may proceed.
120
-
121
- Every gh read the gate runs binds to the repository the command names —
122
- a full PR URL or a ``--repo``/``-R`` flag — so a command targeting one
123
- repository from a working tree checked out to another audits the named
124
- repository's PR, not the working tree's.
125
-
126
- Args:
127
- command: The raw shell command captured by the hook.
128
-
129
- Returns:
130
- A denial reason stating the five-part proof standard when the PR
131
- carries no passing proof comment, or None when a passing proof
132
- comment exists or any gh query fails.
133
- """
134
- target_identity = _resolve_named_identity(command, None)
135
- if target_identity is None:
136
- return None
137
- all_target_repo, resolved_pr_number = target_identity
138
- all_comment_bodies = _fetch_pr_comment_bodies(resolved_pr_number, all_target_repo)
139
- if all_comment_bodies is None:
140
- return None
141
- is_visual_change = _detect_visual_change(resolved_pr_number, all_target_repo)
142
- for each_body in all_comment_bodies:
143
- if not is_proof_shaped_body(each_body):
144
- continue
145
- if not _missing_proof_parts(each_body, is_visual_change):
146
- return None
147
- return PR_READY_GATE_MESSAGE_TEMPLATE.format(pr_number=resolved_pr_number)
148
-
149
-
150
- def _run_gh_command(all_gh_arguments: list[str]) -> str | None:
151
- """Run one gh command and return its stdout.
152
-
153
- Args:
154
- all_gh_arguments: The gh arguments after the executable name.
155
-
156
- Returns:
157
- The command stdout on a zero exit, or None when gh is missing,
158
- times out, or exits non-zero — the caller fails open on None.
159
- """
160
- try:
161
- completed_process = subprocess.run(
162
- [GH_EXECUTABLE, *all_gh_arguments],
163
- capture_output=True,
164
- text=True,
165
- encoding=UTF8_ENCODING,
166
- errors="replace",
167
- timeout=GH_COMMAND_TIMEOUT_SECONDS,
168
- check=False,
169
- )
170
- except (OSError, subprocess.TimeoutExpired):
171
- logger.warning("gh command failed to run: %s", all_gh_arguments)
172
- return None
173
- if completed_process.returncode != 0:
174
- logger.warning("gh command exited %d: %s", completed_process.returncode, all_gh_arguments)
175
- return None
176
- return completed_process.stdout
177
-
178
-
179
- def _build_comments_api_path(pr_number: int, all_target_repo: tuple[str, str] | None) -> str:
180
- """Build the issues-comments API path for the named repository's PR.
181
-
182
- Args:
183
- pr_number: The PR number whose comments to read.
184
- all_target_repo: The (owner, repo) pair the command names, or None to
185
- leave gh's ``{owner}``/``{repo}`` placeholders for gh to expand
186
- from the current directory's repository.
187
-
188
- Returns:
189
- The ``repos/<owner>/<repo>/issues/<pr_number>/comments`` path with
190
- the named owner and repo filled in, or the placeholder form when no
191
- repository is named.
192
- """
193
- comments_api_path = PR_COMMENTS_API_PATH_TEMPLATE.format(pr_number=pr_number)
194
- if all_target_repo is None:
195
- return comments_api_path
196
- owner, repo = all_target_repo
197
- return comments_api_path.format(owner=owner, repo=repo)
198
-
199
-
200
- def _fetch_pr_comment_bodies(
201
- pr_number: int, all_target_repo: tuple[str, str] | None
202
- ) -> list[str] | None:
203
- """Read every comment body on a PR through the paginated issues API.
204
-
205
- The query runs ``gh api --paginate --slurp`` so every page lands in one
206
- JSON array of pages, and the flattening happens here after the full
207
- pagination rather than per page.
208
-
209
- Args:
210
- pr_number: The PR number whose comments to read.
211
- all_target_repo: The (owner, repo) pair the command names, or None to
212
- read the current directory's repository.
213
-
214
- Returns:
215
- The comment bodies across all pages, or None when gh fails or the
216
- output is not the slurped page-array shape — the caller fails open
217
- on None.
218
- """
219
- comments_api_path = _build_comments_api_path(pr_number, all_target_repo)
220
- api_stdout = _run_gh_command(
221
- [GH_API_SUBCOMMAND, comments_api_path, GH_PAGINATE_FLAG, GH_SLURP_FLAG]
222
- )
223
- if api_stdout is None:
224
- return None
225
- try:
226
- all_comment_pages = json.loads(api_stdout)
227
- except json.JSONDecodeError:
228
- return None
229
- if not isinstance(all_comment_pages, list):
230
- return None
231
- all_comment_bodies: list[str] = []
232
- for each_page in all_comment_pages:
233
- if not isinstance(each_page, list):
234
- continue
235
- for each_comment_record in each_page:
236
- if isinstance(each_comment_record, dict):
237
- all_comment_bodies.append(str(each_comment_record.get(COMMENT_BODY_JSON_FIELD, "")))
238
- return all_comment_bodies
239
-
240
-
241
- def _detect_visual_change(
242
- pr_number: int | None, all_target_repo: tuple[str, str] | None
243
- ) -> bool:
244
- """Decide whether the PR diff carries visual work.
245
-
246
- A diff is visual when a changed file carries an image, HTML, or
247
- stylesheet suffix, or when an added line carries a six-digit hex color
248
- value.
249
-
250
- Args:
251
- pr_number: The PR number to diff, or None to diff the current
252
- branch's PR.
253
- all_target_repo: The (owner, repo) pair the command names, or None to
254
- diff the current directory's repository.
255
-
256
- Returns:
257
- True when the diff is visual; False when it is not or when any gh
258
- query fails — an unknowable diff never adds the visual requirement.
259
- """
260
- all_number_arguments = [str(pr_number)] if pr_number is not None else []
261
- all_repo_arguments = _repo_flag_arguments(all_target_repo)
262
- name_only_stdout = _run_gh_command(
263
- [
264
- *ALL_PR_DIFF_SUBCOMMANDS,
265
- *all_number_arguments,
266
- *all_repo_arguments,
267
- PR_DIFF_NAME_ONLY_FLAG,
268
- ]
269
- )
270
- if name_only_stdout is None:
271
- return False
272
- for each_changed_path in name_only_stdout.splitlines():
273
- if Path(each_changed_path.strip()).suffix.lower() in ALL_VISUAL_FILE_SUFFIXES:
274
- return True
275
- diff_stdout = _run_gh_command(
276
- [*ALL_PR_DIFF_SUBCOMMANDS, *all_number_arguments, *all_repo_arguments]
277
- )
278
- if diff_stdout is None:
279
- return False
280
- return HEX_COLOR_ADDED_LINE_PATTERN.search(diff_stdout[:MAX_DIFF_SCAN_CHARS]) is not None
281
-
282
-
283
- def _missing_proof_parts(body: str, is_visual_change: bool) -> list[str]:
284
- """Collect the proof parts a comment body is missing.
285
-
286
- Args:
287
- body: The comment body markdown text.
288
- is_visual_change: Whether the PR diff carries visual work, which
289
- adds the image-embed requirement.
290
-
291
- Returns:
292
- One message per missing part, in the standard's order: command
293
- block, measured values, plan linkage, visual element, honest gaps.
294
- """
295
- prose_without_fences = FENCED_CODE_BLOCK_PATTERN.sub("", body)
296
- all_missing_parts: list[str] = []
297
- if not _has_command_block(body):
298
- all_missing_parts.append(PROOF_PART_COMMAND_MESSAGE)
299
- if not _has_measured_value(prose_without_fences):
300
- all_missing_parts.append(PROOF_PART_MEASURED_MESSAGE)
301
- if not _has_plan_linkage(prose_without_fences):
302
- all_missing_parts.append(PROOF_PART_PLAN_LINKAGE_MESSAGE)
303
- if is_visual_change and not _has_visual_element(prose_without_fences):
304
- all_missing_parts.append(PROOF_PART_VISUAL_MESSAGE)
305
- if not _has_honest_gaps(prose_without_fences):
306
- all_missing_parts.append(PROOF_PART_HONEST_GAPS_MESSAGE)
307
- return all_missing_parts
308
-
309
-
310
- def _has_command_block(body: str) -> bool:
311
- """Report whether the body carries a fenced code block with content.
312
-
313
- Args:
314
- body: The comment body markdown text.
315
-
316
- Returns:
317
- True when at least one fenced code block holds a non-empty line
318
- between its fence lines.
319
- """
320
- for each_fence_match in FENCED_CODE_BLOCK_PATTERN.finditer(body):
321
- all_inner_lines = each_fence_match.group(0).splitlines()[1:-1]
322
- if any(each_line.strip() for each_line in all_inner_lines):
323
- return True
324
- return False
325
-
326
-
327
- def _has_measured_value(prose_without_fences: str) -> bool:
328
- """Report whether the prose carries a measured-value element.
329
-
330
- Args:
331
- prose_without_fences: The body text with fenced code blocks removed.
332
-
333
- Returns:
334
- True when a pipe-table row or a bullet line carries a digit.
335
- """
336
- for each_line in prose_without_fences.splitlines():
337
- if not DIGIT_PATTERN.search(each_line):
338
- continue
339
- if TABLE_ROW_LINE_PATTERN.match(each_line) or BULLET_MARKER_PATTERN.match(each_line):
340
- return True
341
- return False
342
-
343
-
344
- def _has_plan_linkage(prose_without_fences: str) -> bool:
345
- """Report whether the prose links the PR to its parent plan.
346
-
347
- Args:
348
- prose_without_fences: The body text with fenced code blocks removed.
349
-
350
- Returns:
351
- True when one line carries both an issue reference (``#123``) and a
352
- linkage word (issue, phase, plan, parent, advances, milestone, or
353
- part of).
354
- """
355
- for each_line in prose_without_fences.splitlines():
356
- has_issue_reference = ISSUE_REFERENCE_PATTERN.search(each_line) is not None
357
- if has_issue_reference and PLAN_LINKAGE_KEYWORD_PATTERN.search(each_line) is not None:
358
- return True
359
- return False
360
-
361
-
362
- def _has_visual_element(prose_without_fences: str) -> bool:
363
- """Report whether the prose embeds an image.
364
-
365
- Args:
366
- prose_without_fences: The body text with fenced code blocks removed.
367
-
368
- Returns:
369
- True when a markdown image embed (``![...](...)``) is present.
370
- """
371
- return IMAGE_EMBED_PATTERN.search(prose_without_fences) is not None
372
-
373
-
374
- def _has_honest_gaps(prose_without_fences: str) -> bool:
375
- """Report whether the prose states what the proof cannot show.
376
-
377
- Args:
378
- prose_without_fences: The body text with fenced code blocks removed.
379
-
380
- Returns:
381
- True when the prose carries a gap phrase (gap, limitation, cannot,
382
- does not show, not shown, unverified, or not covered).
383
- """
384
- prose_lower = prose_without_fences.lower()
385
- return any(each_phrase in prose_lower for each_phrase in ALL_HONEST_GAP_PHRASES)