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,167 +0,0 @@
1
- #!/usr/bin/env python3
2
- """PreToolUse hook: keep verified-commit exemption messages accurate.
3
-
4
- The verified-commit gate exempts a branch surface only when every changed path
5
- matches one of two narrow rules, implemented in ``is_verification_exempt_diff``:
6
-
7
- 1. file extension in ``DOCS_ONLY_EXTENSIONS`` (docs and image extensions), or
8
- 2. a ``.py`` file whose docstring/comment-stripped AST is unchanged.
9
-
10
- A comment-only change to a non-Python, non-doc file (for example ``.sh``,
11
- ``.json``, ``.yaml``) is therefore NOT exempt: comments are ignored for
12
- exemption purposes only inside Python files via the AST path. A corrective or
13
- guard message that claims comment-only or docs-only surfaces are blanket exempt
14
- overstates the rule and misleads users into expecting such a change to skip
15
- verification.
16
-
17
- This hook fires on Write/Edit of any verified-commit constants module and denies
18
- content whose exemption-claim wording asserts a blanket comment-only or docs-only
19
- exemption. It guards the message constants at authoring time, before the change
20
- reaches review.
21
- """
22
-
23
- import json
24
- import os
25
- import re
26
- import sys
27
- from pathlib import Path
28
-
29
- _hooks_dir = str(Path(__file__).resolve().parent.parent)
30
- if _hooks_dir not in sys.path:
31
- sys.path.insert(0, _hooks_dir)
32
-
33
- from hooks_constants.hook_block_logger import log_hook_block # noqa: E402
34
-
35
-
36
- def is_guarded_file(file_path: str) -> bool:
37
- """Return True for any verified-commit constants module carrying messages."""
38
- all_guarded_file_names = frozenset({"verified_commit_constants.py"})
39
- return os.path.basename(file_path) in all_guarded_file_names
40
-
41
-
42
- def join_adjacent_string_literals(written_text: str) -> str:
43
- """Collapse a closing quote, inter-literal whitespace, and an opening quote.
44
-
45
- Write/Edit content carries the message constant as it appears in source: a
46
- long sentence split across adjacent Python string literals. Between two
47
- literals sit a closing quote, a newline, indentation, and an opening quote.
48
- Replacing that run with a single space rejoins the prose so a phrase wrapped
49
- across a literal boundary — for example ``exempt "`` then ``"automatically``
50
- — reads as one continuous clause for matching.
51
- """
52
- inter_literal_noise_pattern = re.compile(r"[\"']\s*[\"']")
53
- return inter_literal_noise_pattern.sub(" ", written_text)
54
-
55
-
56
- def claims_blanket_comment_exemption(written_text: str) -> bool:
57
- """Return True when the text claims comment surfaces are blanket-exempt.
58
-
59
- Rejoins source-wrapped string literals first, then matches only a genuine
60
- blanket form in which a comment surface is the grammatical subject of
61
- "exempt automatically". A comment surface is the bare noun ``comments`` or
62
- the ``comment-only`` category — never the ``comment`` stem inside an
63
- unrelated word such as ``commentary`` or the ``comment-stripped`` AST
64
- qualifier.
65
-
66
- Two blanket shapes match. The direct shape names a comment surface as the
67
- immediate subject of "(are|is) exempt automatically" across a bridge that
68
- crosses no period, semicolon, or comma, so an intervening predicate (for
69
- example "Comments are handled, and docs are exempt automatically") leaves
70
- docs, not comments, as the exemption subject and does not match. The
71
- enumerated shape names a comment item inside an "...-only surfaces are
72
- exempt automatically" list (for example "Docs-, comment-, and test-only
73
- surfaces are exempt automatically").
74
-
75
- An accurate sentence whose true exemption subject is docs, or that qualifies
76
- comments as the stripped input to the AST comparison, does not match. This
77
- isolates the overstated form the verified-commit gate does not honor for
78
- non-Python files.
79
- """
80
- comment_surface = r"comments?-only|comments?\b(?!-)"
81
- blanket_direct_claim = (
82
- "(?:" + comment_surface + r")(?:[^.;,]*?\b)?(?:are|is)\s+exempt\s+automatically"
83
- )
84
- blanket_enumerated_claim = (
85
- "(?:" + comment_surface + "|comment-)"
86
- r"[^.;]*?-only\s+surfaces\s+are\s+exempt\s+automatically"
87
- )
88
- blanket_exemption_claim_pattern = re.compile(
89
- "(?:" + blanket_direct_claim + ")|(?:" + blanket_enumerated_claim + ")",
90
- re.IGNORECASE,
91
- )
92
- joined_text = join_adjacent_string_literals(written_text)
93
- return bool(blanket_exemption_claim_pattern.search(joined_text))
94
-
95
-
96
- def build_corrective_message() -> str:
97
- """Return the deny reason explaining the real, narrower exemption rules."""
98
- accurate_exemption_phrasing = (
99
- "Docs and images are exempt by extension, and Python files whose "
100
- "docstring- and comment-stripped AST is unchanged; a comment-only "
101
- "change to a non-Python file still needs a verdict."
102
- )
103
- return (
104
- "BLOCKED [verified-commit-message-accuracy]: this exemption message "
105
- "claims comment-only surfaces are exempt automatically, but the "
106
- "verified-commit gate exempts comments only inside Python files (via the "
107
- "docstring/comment stripped AST path). A comment-only change to a "
108
- "non-Python file is NOT exempt and still needs a verifier verdict, so "
109
- "the blanket wording misleads users.\n\nDescribe the real exemption "
110
- "rules instead, for example:\n " + accurate_exemption_phrasing
111
- )
112
-
113
-
114
- def extract_written_text(all_written_fields: dict[str, str]) -> str:
115
- """Return the Write ``content`` and Edit ``new_string`` joined for scanning."""
116
- return (
117
- all_written_fields.get("content", "")
118
- + "\n"
119
- + all_written_fields.get("new_string", "")
120
- )
121
-
122
-
123
- def main() -> None:
124
- all_write_edit_tools = frozenset({"Write", "Edit"})
125
- try:
126
- hook_input = json.load(sys.stdin)
127
- except json.JSONDecodeError:
128
- sys.exit(0)
129
-
130
- tool_name = hook_input.get("tool_name", "")
131
- if tool_name not in all_write_edit_tools:
132
- sys.exit(0)
133
-
134
- tool_input = hook_input.get("tool_input", {})
135
- file_path = tool_input.get("file_path", "")
136
- if not file_path:
137
- sys.exit(0)
138
-
139
- if not is_guarded_file(file_path):
140
- sys.exit(0)
141
-
142
- written_text = extract_written_text(tool_input)
143
- if not claims_blanket_comment_exemption(written_text):
144
- sys.exit(0)
145
-
146
- corrective_message = build_corrective_message()
147
- deny_response = {
148
- "hookSpecificOutput": {
149
- "hookEventName": "PreToolUse",
150
- "permissionDecision": "deny",
151
- "permissionDecisionReason": corrective_message,
152
- }
153
- }
154
- log_hook_block(
155
- calling_hook_name="verified_commit_message_accuracy_blocker.py",
156
- hook_event="PreToolUse",
157
- block_reason=corrective_message,
158
- tool_name=tool_name,
159
- offending_input_preview=file_path,
160
- )
161
- print(json.dumps(deny_response))
162
- sys.stdout.flush()
163
- sys.exit(0)
164
-
165
-
166
- if __name__ == "__main__":
167
- main()
@@ -1,280 +0,0 @@
1
- """SubagentStop hook: mint a commit-gate verdict when code-verifier finishes.
2
-
3
- Only this hook writes verdict files — the main session is denied writes to
4
- the verdict directory, so a session cannot fabricate a passing verdict. The
5
- SubagentStop payload names the stopping subagent's own transcript
6
- (``agent_transcript_path``), which sits beside a harness-written
7
- ``agent-<id>.meta.json`` sidecar naming the spawning ``agentType``. The hook
8
- reads that type from the sidecar, so it resolves identically in interactive,
9
- background, and worktree-switched sessions; only the harness writes the
10
- sidecar, so the type it records cannot be forged. When that type is
11
- ``code-verifier``, the hook pulls the verdict block out of the agent's own
12
- transcript (``agent_transcript_path``), recomputes the live change-surface
13
- hash for the session repository, and writes the verdict bound to that hash.
14
- The companion ``verified_commit_gate.py`` (PreToolUse) then allows ``git
15
- commit`` / ``git push`` only while the work tree still matches the verified
16
- state.
17
-
18
- The verifier's final message must end with a fenced block::
19
-
20
- ```verdict
21
- {"all_pass": true, "findings": []}
22
- ```
23
-
24
- A missing or unparseable block mints nothing, which leaves the gate closed.
25
- """
26
-
27
- from __future__ import annotations
28
-
29
- import json
30
- import re
31
- import sys
32
- from pathlib import Path
33
-
34
- blocking_directory = str(Path(__file__).resolve().parent)
35
- if blocking_directory not in sys.path:
36
- sys.path.insert(0, blocking_directory)
37
-
38
- from verified_commit_config_bootstrap import ( # noqa: E402
39
- register_verified_commit_constants,
40
- )
41
-
42
- register_verified_commit_constants()
43
-
44
- from config.verified_commit_constants import (
45
- MINTING_AGENT_TYPE,
46
- VERDICT_KEY_MANIFEST_SHA256,
47
- )
48
- from verification_verdict_store import (
49
- branch_surface_manifest,
50
- empty_surface_hash,
51
- manifest_sha256,
52
- resolve_merge_base,
53
- resolve_repo_root,
54
- write_verdict,
55
- )
56
-
57
-
58
- def assistant_text_blocks(transcript_path: str) -> list[str]:
59
- """Collect every assistant text block from a transcript JSONL file.
60
-
61
- Args:
62
- transcript_path: Path to the subagent's transcript.
63
-
64
- Returns:
65
- The text of each assistant content block, in transcript order;
66
- empty when the file is missing or holds no assistant text.
67
- """
68
- collected_blocks: list[str] = []
69
- try:
70
- transcript_lines = (
71
- Path(transcript_path).read_text(encoding="utf-8", errors="replace").splitlines()
72
- )
73
- except OSError:
74
- return collected_blocks
75
- for each_line in transcript_lines:
76
- try:
77
- transcript_entry = json.loads(each_line)
78
- except json.JSONDecodeError:
79
- continue
80
- if not isinstance(transcript_entry, dict):
81
- continue
82
- if transcript_entry.get("type") != "assistant":
83
- continue
84
- message_body = transcript_entry.get("message")
85
- if not isinstance(message_body, dict):
86
- continue
87
- content_blocks = message_body.get("content")
88
- if not isinstance(content_blocks, list):
89
- continue
90
- for each_block in content_blocks:
91
- if isinstance(each_block, dict) and each_block.get("type") == "text":
92
- block_text = each_block.get("text")
93
- if isinstance(block_text, str):
94
- collected_blocks.append(block_text)
95
- return collected_blocks
96
-
97
-
98
- def last_verdict_in_blocks(all_text_blocks: list[str]) -> dict | None:
99
- """Extract the final verdict fence from assistant text blocks.
100
-
101
- Args:
102
- all_text_blocks: Assistant text blocks in transcript order.
103
-
104
- Returns:
105
- The parsed verdict mapping carrying a boolean ``all_pass`` and a
106
- list ``findings``, or None when no block holds a wellformed fence.
107
- """
108
- verdict_fence_pattern = re.compile(r"```verdict\s*\n(.*?)```", re.DOTALL)
109
- fence_bodies: list[str] = []
110
- for each_block in all_text_blocks:
111
- fence_bodies.extend(verdict_fence_pattern.findall(each_block))
112
- for each_fence_body in reversed(fence_bodies):
113
- try:
114
- verdict_record = json.loads(each_fence_body)
115
- except json.JSONDecodeError:
116
- continue
117
- if not isinstance(verdict_record, dict):
118
- continue
119
- if not isinstance(verdict_record.get("all_pass"), bool):
120
- continue
121
- if not isinstance(verdict_record.get("findings"), list):
122
- continue
123
- return verdict_record
124
- return None
125
-
126
-
127
- def _agent_type_from_meta_sidecar(agent_transcript_path: str) -> str | None:
128
- """Read the spawning agentType from a subagent transcript's sidecar.
129
-
130
- Each subagent transcript ``agent-<id>.jsonl`` sits beside a harness-written
131
- ``agent-<id>.meta.json`` naming the spawning ``agentType``. Reading the type
132
- from this sidecar binds it to the stopping subagent itself, so it resolves
133
- identically in interactive, background, and worktree-switched sessions and
134
- needs no parent-transcript scan or flush retry. Only the harness writes this
135
- sidecar, so a session cannot forge the type it reports.
136
-
137
- Args:
138
- agent_transcript_path: The stopping subagent's own transcript path from
139
- the SubagentStop payload.
140
-
141
- Returns:
142
- The recorded ``agentType``, or None when the path is empty, the sidecar
143
- is absent, unreadable, or unparseable, it does not hold a JSON object,
144
- or it names no string ``agentType``.
145
- """
146
- if not agent_transcript_path:
147
- return None
148
- transcript_file = Path(agent_transcript_path)
149
- sidecar_file = transcript_file.with_name(f"{transcript_file.stem}.meta.json")
150
- try:
151
- sidecar_record = json.loads(sidecar_file.read_text(encoding="utf-8"))
152
- except (OSError, UnicodeDecodeError, json.JSONDecodeError):
153
- return None
154
- if not isinstance(sidecar_record, dict):
155
- return None
156
- recorded_type = sidecar_record.get("agentType")
157
- return recorded_type if isinstance(recorded_type, str) else None
158
-
159
-
160
- def resolved_subagent_type(subagent_stop_payload: dict) -> str | None:
161
- """Recover the spawning agent type for a SubagentStop payload.
162
-
163
- The stopping subagent's own transcript (``agent_transcript_path``) sits
164
- beside a harness-written ``agent-<id>.meta.json`` sidecar naming its
165
- ``agentType``. The type comes solely from that sidecar, which only the
166
- harness writes — the main session can neither write the sidecar nor forge
167
- the agentType it records. A verdict fence in the transcript is never used to
168
- infer the type, because the main session controls the prompt of any
169
- Agent-tool subagent it spawns and could instruct one to print such a fence.
170
-
171
- Args:
172
- subagent_stop_payload: The SubagentStop hook payload.
173
-
174
- Returns:
175
- The agent type the sidecar records, or None when the
176
- ``agent_transcript_path`` is empty, the sidecar is absent, unreadable,
177
- or unparseable, it does not hold a JSON object, or it names no string
178
- ``agentType``.
179
- """
180
- return _agent_type_from_meta_sidecar(
181
- subagent_stop_payload.get("agent_transcript_path", "")
182
- )
183
-
184
-
185
- def _attested_or_recomputed_hash(verdict_record: dict, repo_root: str) -> str | None:
186
- """Choose the surface hash the minted verdict binds to.
187
-
188
- A code-verifier that verifies a work tree other than the stop event's cwd
189
- attests the surface it checked by emitting ``manifest_sha256`` in its
190
- verdict fence (computed against the verified work tree via the
191
- ``--manifest-hash`` CLI). Binding the minted verdict to that attested hash
192
- keeps the verdict tied to the code actually verified rather than the
193
- subagent's cwd, so a verdict earned for one work tree clears a commit in a
194
- sibling work tree of the same surface. When the fence attests no hash, the
195
- minter recomputes one from the cwd work tree, which is correct whenever the
196
- verifier ran in the work tree it verified.
197
-
198
- Returns None (mints nothing) in two empty-surface cases:
199
-
200
- - The attested hash equals ``empty_surface_hash()`` — the verifier called
201
- the store CLI on a wrong (empty) work tree and the hash it got back
202
- represents nothing.
203
- - The recompute branch produces an empty manifest — the cwd work tree also
204
- has no changed or untracked files versus the merge base.
205
-
206
- Args:
207
- verdict_record: The parsed verdict fence from the verifier transcript.
208
- repo_root: The work-tree root resolved from the stop event's cwd, used
209
- for the recompute fallback.
210
-
211
- Returns:
212
- The attested ``manifest_sha256`` when the fence carries a non-empty
213
- string one that is not the empty-surface sentinel; the cwd work tree's
214
- recomputed surface hash when the fence attests nothing and the surface
215
- is non-empty; or None when the attested hash is the empty-surface
216
- sentinel, the surface manifest is empty, or no upstream base resolves.
217
- """
218
- attested_manifest_sha256 = verdict_record.get(VERDICT_KEY_MANIFEST_SHA256)
219
- if isinstance(attested_manifest_sha256, str) and attested_manifest_sha256:
220
- if attested_manifest_sha256 == empty_surface_hash():
221
- return None
222
- return attested_manifest_sha256
223
- merge_base_sha = resolve_merge_base(repo_root)
224
- if merge_base_sha is None:
225
- return None
226
- surface_manifest_text = branch_surface_manifest(repo_root, merge_base_sha)
227
- if not surface_manifest_text:
228
- return None
229
- return manifest_sha256(surface_manifest_text)
230
-
231
-
232
- def mint_for_payload(subagent_stop_payload: dict) -> Path | None:
233
- """Mint a verdict file for a code-verifier stop event.
234
-
235
- Args:
236
- subagent_stop_payload: The SubagentStop hook payload.
237
-
238
- Returns:
239
- The verdict file path when minted; None when the payload is not a
240
- code-verifier stop, the transcript holds no verdict, the cwd is not a
241
- work tree, or — for a verdict that attests no ``manifest_sha256`` of
242
- its own — that work tree has no upstream base to recompute the surface
243
- hash from.
244
- """
245
- if resolved_subagent_type(subagent_stop_payload) != MINTING_AGENT_TYPE:
246
- return None
247
- agent_transcript_path = subagent_stop_payload.get("agent_transcript_path", "")
248
- if not agent_transcript_path:
249
- return None
250
- verdict_record = last_verdict_in_blocks(assistant_text_blocks(agent_transcript_path))
251
- if verdict_record is None:
252
- return None
253
- repo_root = resolve_repo_root(subagent_stop_payload.get("cwd", "."))
254
- if repo_root is None:
255
- return None
256
- bound_manifest_sha256 = _attested_or_recomputed_hash(verdict_record, repo_root)
257
- if bound_manifest_sha256 is None:
258
- return None
259
- return write_verdict(
260
- repo_root,
261
- bound_manifest_sha256,
262
- verdict_record["all_pass"],
263
- verdict_record["findings"],
264
- str(subagent_stop_payload.get("agent_id", "")),
265
- )
266
-
267
-
268
- def main() -> None:
269
- """Read the SubagentStop payload and mint a verdict when one applies."""
270
- try:
271
- subagent_stop_payload = json.load(sys.stdin)
272
- except json.JSONDecodeError:
273
- return
274
- if not isinstance(subagent_stop_payload, dict):
275
- return
276
- mint_for_payload(subagent_stop_payload)
277
-
278
-
279
- if __name__ == "__main__":
280
- main()
@@ -1,64 +0,0 @@
1
- """Configuration constants for the code_verifier_spawn_preflight_gate hook.
2
-
3
- The gate denies an ``Agent`` or ``Task`` spawn whose ``subagent_type`` is
4
- ``code-verifier`` when the branch carries a merge conflict against its base
5
- ref, a CODE_RULES violation on a line added in the working tree since the merge
6
- base (committed on the branch or uncommitted), or a CODE_RULES engine
7
- import/load failure. It runs those pre-flight checks before the expensive
8
- verification spawn and addresses its deny reason to the spawning agent so that
9
- agent fixes the named issues and re-spawns. Environmental git failures stay
10
- fail-open; engine import/load failure is fail-closed. Every literal the hook
11
- body reads lives here; ``AGENT_TOOL_NAME`` comes from
12
- ``pr_converge_bugteam_enforcer_constants`` and joins ``TASK_TOOL_NAME`` in
13
- ``ALL_CODE_VERIFIER_SPAWN_TOOL_NAMES`` so both spawn tool names share one source.
14
- """
15
-
16
- from __future__ import annotations
17
-
18
- from pathlib import Path
19
-
20
- from hooks_constants.pr_converge_bugteam_enforcer_constants import AGENT_TOOL_NAME
21
-
22
- CODE_VERIFIER_SUBAGENT_TYPE: str = "code-verifier"
23
- TASK_TOOL_NAME: str = "Task"
24
- ALL_CODE_VERIFIER_SPAWN_TOOL_NAMES: frozenset[str] = frozenset(
25
- {AGENT_TOOL_NAME, TASK_TOOL_NAME}
26
- )
27
-
28
- ALL_MERGE_TREE_COMMAND_FLAGS: tuple[str, ...] = (
29
- "merge-tree",
30
- "--write-tree",
31
- "--name-only",
32
- )
33
- MERGE_TREE_CONFLICT_EXIT_CODE: int = 1
34
- MERGE_TREE_CLEAN_EXIT_CODE: int = 0
35
- MERGE_TREE_TIMEOUT_SECONDS: int = 30
36
-
37
- ALL_MERGE_HEAD_PROBE_FLAGS: tuple[str, ...] = ("rev-parse", "--verify", "--quiet", "MERGE_HEAD")
38
- ALL_UNMERGED_PATHS_DIFF_FLAGS: tuple[str, ...] = ("diff", "--name-only", "--diff-filter=U")
39
-
40
- ALL_NAME_ONLY_WORKTREE_DIFF_FLAGS: tuple[str, ...] = (
41
- "-c",
42
- "core.quotePath=false",
43
- "diff",
44
- "--name-only",
45
- "--no-renames",
46
- )
47
- ALL_UNIFIED_ZERO_DIFF_FLAGS: tuple[str, ...] = ("diff", "--unified=0")
48
-
49
- DENY_REASON_LEAD: str = (
50
- "BLOCKED [code-verifier-spawn-preflight]: a code-verifier spawn "
51
- "(Agent or Task tool, subagent_type code-verifier) is blocked because "
52
- "the branch is not in a committable state. "
53
- "Fix these, then re-spawn the code-verifier:"
54
- )
55
- MERGE_CONFLICT_SECTION_HEADER: str = "Merge conflicts vs {base_ref}:"
56
- CODE_RULES_SECTION_HEADER: str = "CODE_RULES violations on changed lines:"
57
- ENGINE_LOAD_FAILURE_SECTION: str = (
58
- "CODE_RULES engine failed to load:\n"
59
- " The CODE_RULES validate_content engine could not be imported or loaded "
60
- "(package import/load failure). Repair the enforcer so load_validate_content "
61
- "succeeds, then re-spawn code-verifier."
62
- )
63
-
64
- GATE_SCRIPTS_RELATIVE_PATH: Path = Path("_shared") / "pr-loop" / "scripts"
@@ -1,53 +0,0 @@
1
- """Command-parse patterns for the convergence gate's repo binding.
2
-
3
- The convergence gate keys its evidence lookup to the PR that the
4
- ``gh pr ready`` command names. These patterns pull an explicit owner/repo
5
- out of the command — and, for a full PR URL, the PR number too — so the
6
- gate binds to the repository the command targets rather than the
7
- session working directory's repository when the two differ.
8
-
9
- The owner/repo and URL patterns run only over the ``gh pr ready`` segment,
10
- clipped at the first command separator, so a flag or URL that belongs to a
11
- chained command does not bind the gate to the wrong PR::
12
-
13
- gh pr ready 161 && gh pr comment 999 --repo other-owner/other-repo
14
- ^^^^^^^^^^^^^^^^ clipped here ^
15
- -> ready segment scanned; the chained --repo is out of scope
16
-
17
- gh pr ready 161 --repo sample-owner/target-repo
18
- ^^^^^^ ^^^^^^^^^^^^ ^^^^^^^^^^^
19
- flag owner repo -> REPO_OVERRIDE_FLAG_PATTERN
20
- gh pr ready https://github.com/sample-owner/target-repo/pull/161
21
- ^^^^^^^^^^^^ ^^^^^^^^^^^ ^^^
22
- owner repo number
23
- -> PR_URL_OWNER_REPO_NUMBER_PATTERN
24
- """
25
-
26
- GH_PR_READY_ANCHOR_PATTERN: str = r"\bgh\s+pr\s+ready\b(?![^&|;\n]*--undo)"
27
-
28
- COMMAND_SEPARATOR_PATTERN: str = r"&&|\|\||;|\||\n|(?<![>&])&(?![>&])"
29
-
30
- BASH_LINE_CONTINUATION_PATTERN: str = r"\\\r?\n[ \t]*"
31
-
32
- PR_URL_OWNER_REPO_NUMBER_PATTERN: str = (
33
- r"https?://[^/]+/(?P<owner>[\w.-]+)/(?P<repo>[\w.-]+)/pull/(?P<number>\d+)"
34
- )
35
-
36
- REPO_OVERRIDE_FLAG_PATTERN: str = (
37
- r"(?:^|\s)(?:--repo(?:=|\s+)|-R(?:=|\s+)?)"
38
- r"(?:https?://[^/\s]+/|git@[^:\s]+:|(?:[\w.-]+/)*)"
39
- r"(?P<owner>[\w.-]+)/(?P<repo>[\w.-]+)"
40
- )
41
-
42
- ALL_GH_PR_VIEW_NUMBER_COMMAND: tuple[str, ...] = (
43
- "gh",
44
- "pr",
45
- "view",
46
- "--json",
47
- "number",
48
- "--jq",
49
- ".number",
50
- )
51
- GH_REPO_FLAG: str = "--repo"
52
- REPO_SLUG_TEMPLATE: str = "{owner}/{repo}"
53
- GIT_URL_SUFFIX: str = ".git"
@@ -1,55 +0,0 @@
1
- """Configuration constants for the pr_converge_bugteam_enforcer hook pair.
2
-
3
- The enforcer denies ``Agent({subagent_type: "clean-coder"})`` invocations that
4
- substitute audit-shaped work for the formal ``Skill({skill: "bugteam"})`` call
5
- during Step 5 BUGTEAM of the pr-converge loop. The tracker records every
6
- formal ``Skill({skill: "bugteam"})`` invocation so the enforcer can confirm
7
- the Skill fired this tick at the current HEAD before allowing follow-on
8
- clean-coder spawns.
9
- """
10
-
11
- from __future__ import annotations
12
-
13
- AGENT_TOOL_NAME: str = "Agent"
14
- SKILL_TOOL_NAME: str = "Skill"
15
-
16
- CLEAN_CODER_SUBAGENT_TYPE: str = "clean-coder"
17
- BUGTEAM_SKILL_NAME: str = "bugteam"
18
-
19
- PR_CONVERGE_STATE_FILENAME: str = "pr-converge-state.json"
20
- CLAUDE_JOB_DIR_ENV_VAR: str = "CLAUDE_JOB_DIR"
21
-
22
- BUGTEAM_PHASE: str = "BUGTEAM"
23
-
24
- STATE_FIELD_PHASE: str = "phase"
25
- STATE_FIELD_CURRENT_HEAD: str = "current_head"
26
- STATE_FIELD_TICK_COUNT: str = "tick_count"
27
- STATE_FIELD_BUGTEAM_SKILL_INVOKED_AT_HEAD: str = "bugteam_skill_invoked_at_head"
28
- STATE_FIELD_BUGTEAM_SKILL_INVOKED_AT_TICK: str = "bugteam_skill_invoked_at_tick"
29
-
30
- ALL_AUDIT_PROMPT_SUBSTRINGS: tuple[str, ...] = (
31
- "audit",
32
- "findings",
33
- "bugteam",
34
- "a-j categor",
35
- "code-quality",
36
- "verify_clean",
37
- "converge",
38
- )
39
-
40
- ENFORCER_CORRECTIVE_MESSAGE: str = (
41
- "BLOCKED [pr-converge-bugteam-enforcer]: Step 5 BUGTEAM advances ONLY after "
42
- '`Skill({skill: "bugteam", args: "<PR URL>"})` fires this tick at the '
43
- 'current HEAD. Substituting an `Agent({subagent_type: "clean-coder"})` '
44
- "audit call for the formal Skill invocation is a protocol violation — the "
45
- "formal Skill writes the artifact `check_convergence.py`'s `bugteam_clean_at` "
46
- "gate looks for, and a substituted audit silently bypasses that gate.\n\n"
47
- "`qbug` is NOT an accepted substitute — `bugteam` is the only allowed skill "
48
- "at this step.\n\n"
49
- 'Run `Skill({skill: "bugteam", args: "<PR URL>"})` first. Follow-on '
50
- "clean-coder fix spawns are allowed once the formal Skill has registered at "
51
- "the current HEAD and tick."
52
- )
53
-
54
- STATE_FILE_ATOMIC_WRITE_SUFFIX: str = ".tmp"
55
- STATE_FILE_JSON_INDENT_SPACES: int = 2
@@ -1,67 +0,0 @@
1
- """Shared state-loading helpers for the pr_converge_bugteam_enforcer hook pair.
2
-
3
- Both the enforcer (``pr_converge_bugteam_enforcer.py``) and the tracker
4
- (``pr_converge_bugteam_skill_tracker.py``) read the per-job
5
- ``$CLAUDE_JOB_DIR/pr-converge-state.json`` file from the same per-job
6
- directory. This module hosts the byte-identical ``load_state_dictionary``
7
- and ``resolve_state_path`` helpers so each hook imports a single canonical
8
- definition.
9
- """
10
-
11
- from __future__ import annotations
12
-
13
- import json
14
- import os
15
- from pathlib import Path
16
-
17
- from hooks_constants.pr_converge_bugteam_enforcer_constants import (
18
- CLAUDE_JOB_DIR_ENV_VAR,
19
- PR_CONVERGE_STATE_FILENAME,
20
- )
21
-
22
-
23
- def load_state_dictionary(state_path: Path) -> dict[str, object] | None:
24
- """Return the parsed pr-converge state, or None when absent or unparseable.
25
-
26
- Args:
27
- state_path: Absolute path to ``pr-converge-state.json``.
28
-
29
- Returns:
30
- The decoded state dictionary, or None when the file is missing,
31
- malformed, empty, or not a JSON object at the root.
32
- """
33
- if not state_path.is_file():
34
- return None
35
- try:
36
- raw_text = state_path.read_text(encoding="utf-8")
37
- except OSError:
38
- return None
39
- if not raw_text.strip():
40
- return None
41
- try:
42
- parsed_state = json.loads(raw_text)
43
- except json.JSONDecodeError:
44
- return None
45
- if not isinstance(parsed_state, dict):
46
- return None
47
- return parsed_state
48
-
49
-
50
- def resolve_state_path() -> Path | None:
51
- """Return the absolute path to the per-job ``pr-converge-state.json``.
52
-
53
- Reads ``$CLAUDE_JOB_DIR`` from the environment and joins
54
- ``PR_CONVERGE_STATE_FILENAME`` onto it. The path is returned even when
55
- the file does not yet exist on disk; callers are expected to call
56
- ``load_state_dictionary`` (or ``state_path.is_file()``) to check for
57
- presence.
58
-
59
- Returns:
60
- Absolute path to ``$CLAUDE_JOB_DIR/pr-converge-state.json``, or
61
- ``None`` when the ``CLAUDE_JOB_DIR`` environment variable is unset
62
- or empty (no single-PR pr-converge job is currently active).
63
- """
64
- job_directory = os.environ.get(CLAUDE_JOB_DIR_ENV_VAR, "")
65
- if not job_directory:
66
- return None
67
- return Path(job_directory) / PR_CONVERGE_STATE_FILENAME