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,198 +0,0 @@
1
- #!/usr/bin/env python3
2
- """PreToolUse hook: record formal bugteam Skill invocations into pr-converge state.
3
-
4
- Companion to ``pr_converge_bugteam_enforcer``. On every
5
- ``Skill({skill: "bugteam"})`` invocation, this hook stamps
6
- ``$CLAUDE_JOB_DIR/pr-converge-state.json`` with
7
- ``bugteam_skill_invoked_at_head = current_head`` and
8
- ``bugteam_skill_invoked_at_tick = tick_count`` so the enforcer can confirm
9
- the formal Skill fired this tick at the current HEAD before allowing any
10
- follow-on clean-coder audit-shaped Agent spawn.
11
-
12
- ``qbug`` invocations are deliberately ignored — qbug is not an accepted
13
- substitute for the formal bugteam Skill at Step 5.
14
-
15
- The hook never blocks: it returns exit 0 in every branch so the Skill call
16
- proceeds unchanged.
17
- """
18
-
19
- from __future__ import annotations
20
-
21
- import json
22
- import os
23
- import sys
24
- import tempfile
25
- from pathlib import Path
26
- from typing import TextIO
27
-
28
- _hooks_dir = str(Path(__file__).resolve().parent.parent)
29
- if _hooks_dir not in sys.path:
30
- sys.path.insert(0, _hooks_dir)
31
-
32
- from hooks_constants.pr_converge_bugteam_enforcer_constants import ( # noqa: E402
33
- BUGTEAM_SKILL_NAME,
34
- SKILL_TOOL_NAME,
35
- STATE_FIELD_BUGTEAM_SKILL_INVOKED_AT_HEAD,
36
- STATE_FIELD_BUGTEAM_SKILL_INVOKED_AT_TICK,
37
- STATE_FIELD_CURRENT_HEAD,
38
- STATE_FIELD_TICK_COUNT,
39
- STATE_FILE_ATOMIC_WRITE_SUFFIX,
40
- STATE_FILE_JSON_INDENT_SPACES,
41
- )
42
- from hooks_constants.pr_converge_bugteam_enforcer_state import ( # noqa: E402
43
- load_state_dictionary,
44
- resolve_state_path,
45
- )
46
-
47
-
48
- def _atomic_write_state(state_path: Path, state_by_field: dict[str, object]) -> None:
49
- """Serialize state to disk atomically via tempfile + rename.
50
-
51
- Args:
52
- state_path: Destination ``pr-converge-state.json`` path.
53
- state_by_field: Updated state mapping each field name to its value.
54
- """
55
- parent_directory = state_path.parent
56
- parent_directory.mkdir(parents=True, exist_ok=True)
57
- encoded_text = json.dumps(state_by_field, indent=STATE_FILE_JSON_INDENT_SPACES, sort_keys=True)
58
- with tempfile.NamedTemporaryFile(
59
- mode="w",
60
- encoding="utf-8",
61
- dir=str(parent_directory),
62
- delete=False,
63
- suffix=STATE_FILE_ATOMIC_WRITE_SUFFIX,
64
- ) as temporary_handle:
65
- temporary_handle.write(encoded_text)
66
- temporary_path = Path(temporary_handle.name)
67
- try:
68
- os.replace(str(temporary_path), str(state_path))
69
- except OSError:
70
- Path(temporary_path).unlink(missing_ok=True)
71
- raise
72
-
73
-
74
- def _emit_missing_state_warning(output_stream: TextIO) -> None:
75
- """Write the missing-state warning to the provided stream.
76
-
77
- Args:
78
- output_stream: Writable text stream — production code passes
79
- ``sys.stderr``; tests pass a ``StringIO`` to capture the message.
80
- """
81
- output_stream.write(
82
- "pr_converge_bugteam_skill_tracker: state file lacks current_head or "
83
- "tick_count; bugteam invocation NOT recorded\n"
84
- )
85
- output_stream.flush()
86
-
87
-
88
- def _record_bugteam_skill_invocation(
89
- state_by_field: dict[str, object],
90
- ) -> dict[str, object] | None:
91
- """Return a copy of state with bugteam-Skill invocation fields stamped, or None on no-op.
92
-
93
- The two stamp fields are owned exclusively by this tracker. Concurrent
94
- writes from the orchestrator never touch them, so the read-modify-write
95
- window cannot lose an orchestrator update on these specific keys.
96
-
97
- When ``current_head`` (str) or ``tick_count`` (int) is missing or wrong-typed,
98
- the function emits a stderr warning via ``_emit_missing_state_warning`` and
99
- returns ``None`` so the caller skips the disk write entirely. Skipping the
100
- no-op write narrows the read-modify-write window against the orchestrator —
101
- concurrent updates to non-stamp fields (``phase``, ``tick_count``, etc.)
102
- cannot be silently lost by a tracker rewrite that changes nothing.
103
-
104
- Args:
105
- state_by_field: Existing pr-converge state mapping each field name to
106
- its value.
107
-
108
- Returns:
109
- New dictionary identical to ``state_by_field`` plus
110
- ``bugteam_skill_invoked_at_head`` set to ``current_head`` and
111
- ``bugteam_skill_invoked_at_tick`` set to ``tick_count`` when both
112
- source fields are present and well-typed; ``None`` when either source
113
- field is missing or wrong-typed, signaling the caller to skip the
114
- atomic write.
115
- """
116
- current_head = state_by_field.get(STATE_FIELD_CURRENT_HEAD)
117
- current_tick = state_by_field.get(STATE_FIELD_TICK_COUNT)
118
- if not isinstance(current_head, str) or not isinstance(current_tick, int):
119
- _emit_missing_state_warning(sys.stderr)
120
- return None
121
- updated_state: dict[str, object] = dict(state_by_field)
122
- updated_state[STATE_FIELD_BUGTEAM_SKILL_INVOKED_AT_HEAD] = current_head
123
- updated_state[STATE_FIELD_BUGTEAM_SKILL_INVOKED_AT_TICK] = current_tick
124
- return updated_state
125
-
126
-
127
- def _is_formal_bugteam_skill_invocation(payload_by_field: dict[str, object]) -> bool:
128
- """Return True when this hook invocation matches the formal bugteam Skill.
129
-
130
- Args:
131
- payload_by_field: The full PreToolUse hook payload (already JSON-parsed),
132
- keyed by top-level field name.
133
-
134
- Returns:
135
- True when ``tool_name == "Skill"`` and ``tool_input["skill"]
136
- == "bugteam"``. Returns False for qbug and every other skill.
137
- """
138
- if payload_by_field.get("tool_name", "") != SKILL_TOOL_NAME:
139
- return False
140
- tool_input = payload_by_field.get("tool_input", {})
141
- if not isinstance(tool_input, dict):
142
- return False
143
- return tool_input.get("skill", "") == BUGTEAM_SKILL_NAME
144
-
145
-
146
- def _emit_state_write_error(state_write_error: OSError, output_stream: TextIO) -> None:
147
- """Write the state-write failure message to the provided stream.
148
-
149
- Args:
150
- state_write_error: The OSError raised by ``_atomic_write_state``.
151
- output_stream: Writable text stream — production code passes
152
- ``sys.stderr``; tests pass a ``StringIO`` to capture the message.
153
- """
154
- output_stream.write(
155
- f"pr_converge_bugteam_skill_tracker: state write failed; "
156
- f"stamp not recorded: {state_write_error}\n"
157
- )
158
- output_stream.flush()
159
-
160
-
161
- def main() -> None:
162
- """Tracker entry point for the PreToolUse:Skill hook.
163
-
164
- Reads the PreToolUse payload from stdin, records a formal
165
- ``Skill({skill: "bugteam"})`` invocation in the pr-converge state file,
166
- and always returns 0 — including on a state-write failure, since a
167
- non-zero PreToolUse exit would block the very Skill invocation this
168
- hook exists to record. State-write failures are surfaced via
169
- ``_emit_state_write_error`` to stderr so the operator still sees the
170
- protocol-corruption signal.
171
- """
172
- try:
173
- hook_payload = json.load(sys.stdin)
174
- except json.JSONDecodeError:
175
- return
176
- if not isinstance(hook_payload, dict):
177
- return
178
- if not _is_formal_bugteam_skill_invocation(hook_payload):
179
- return
180
- state_path = resolve_state_path()
181
- if state_path is None:
182
- return
183
- parsed_state = load_state_dictionary(state_path)
184
- if parsed_state is None:
185
- return
186
- updated_state = _record_bugteam_skill_invocation(parsed_state)
187
- if updated_state is None:
188
- return
189
- try:
190
- _atomic_write_state(state_path, updated_state)
191
- except OSError as state_write_error:
192
- _emit_state_write_error(state_write_error, sys.stderr)
193
- return
194
- return
195
-
196
-
197
- if __name__ == "__main__":
198
- main()
@@ -1,283 +0,0 @@
1
- """Unit tests for the pr_converge_bugteam_skill_tracker PreToolUse hook.
2
-
3
- Covers the bugteam-only update path: only Skill({skill: "bugteam"}) updates
4
- the invocation fields in $CLAUDE_JOB_DIR/pr-converge-state.json (the file
5
- named by PR_CONVERGE_STATE_FILENAME). qbug, other skills, and missing-state
6
- cases all return exit 0 without modifying state.
7
- """
8
-
9
- from __future__ import annotations
10
-
11
- import importlib.util
12
- import io
13
- import json
14
- import pathlib
15
- import sys
16
- from typing import Any
17
- from unittest import mock
18
-
19
- import pytest
20
-
21
- _HOOK_DIR = pathlib.Path(__file__).parent
22
- _HOOKS_TREE = _HOOK_DIR.parent
23
- for each_path in (str(_HOOK_DIR), str(_HOOKS_TREE)):
24
- if each_path not in sys.path:
25
- sys.path.insert(0, each_path)
26
-
27
- hook_spec = importlib.util.spec_from_file_location(
28
- "pr_converge_bugteam_skill_tracker",
29
- _HOOK_DIR / "pr_converge_bugteam_skill_tracker.py",
30
- )
31
- assert hook_spec is not None
32
- assert hook_spec.loader is not None
33
- hook_module = importlib.util.module_from_spec(hook_spec)
34
- hook_spec.loader.exec_module(hook_module)
35
-
36
- from hooks_constants.pr_converge_bugteam_enforcer_constants import (
37
- CLAUDE_JOB_DIR_ENV_VAR,
38
- PR_CONVERGE_STATE_FILENAME,
39
- )
40
-
41
- _HEAD_SHA = "abc123def456abc123def456abc123def456abcd"
42
- _TICK_COUNT = 4
43
-
44
-
45
- def _write_state(state_directory: pathlib.Path, state: dict[str, Any]) -> pathlib.Path:
46
- state_path = state_directory / PR_CONVERGE_STATE_FILENAME
47
- state_path.write_text(json.dumps(state), encoding="utf-8")
48
- return state_path
49
-
50
-
51
- def _baseline_state() -> dict[str, Any]:
52
- return {
53
- "phase": "BUGTEAM",
54
- "current_head": _HEAD_SHA,
55
- "tick_count": _TICK_COUNT,
56
- "bugteam_skill_invoked_at_head": None,
57
- "bugteam_skill_invoked_at_tick": None,
58
- }
59
-
60
-
61
- def _read_state(state_directory: pathlib.Path) -> dict[str, Any]:
62
- state_path = state_directory / PR_CONVERGE_STATE_FILENAME
63
- return json.loads(state_path.read_text(encoding="utf-8"))
64
-
65
-
66
- def _run_main_with_io(input_text: str) -> None:
67
- with mock.patch("sys.stdin", io.StringIO(input_text)):
68
- with mock.patch("sys.stdout", new_callable=io.StringIO):
69
- try:
70
- hook_module.main()
71
- except SystemExit:
72
- pass
73
-
74
-
75
- def _run_main_capturing_stderr(input_text: str) -> str:
76
- with mock.patch("sys.stdin", io.StringIO(input_text)):
77
- with mock.patch("sys.stdout", new_callable=io.StringIO):
78
- with mock.patch("sys.stderr", new_callable=io.StringIO) as captured_stderr:
79
- try:
80
- hook_module.main()
81
- except SystemExit:
82
- pass
83
- return captured_stderr.getvalue()
84
-
85
-
86
- @pytest.fixture()
87
- def claude_job_directory(tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch) -> pathlib.Path:
88
- monkeypatch.setenv(CLAUDE_JOB_DIR_ENV_VAR, str(tmp_path))
89
- return tmp_path
90
-
91
-
92
- def test_should_record_invocation_when_bugteam_skill_fires(
93
- claude_job_directory: pathlib.Path,
94
- ) -> None:
95
- _write_state(claude_job_directory, _baseline_state())
96
- skill_payload: dict[str, Any] = {
97
- "tool_name": "Skill",
98
- "tool_input": {"skill": "bugteam", "args": "https://github.com/o/r/pull/1"},
99
- }
100
- _run_main_with_io(json.dumps(skill_payload))
101
- updated_state = _read_state(claude_job_directory)
102
- assert updated_state["bugteam_skill_invoked_at_head"] == _HEAD_SHA
103
- assert updated_state["bugteam_skill_invoked_at_tick"] == _TICK_COUNT
104
-
105
-
106
- def test_should_not_record_invocation_when_qbug_skill_fires(
107
- claude_job_directory: pathlib.Path,
108
- ) -> None:
109
- _write_state(claude_job_directory, _baseline_state())
110
- qbug_payload: dict[str, Any] = {
111
- "tool_name": "Skill",
112
- "tool_input": {"skill": "qbug"},
113
- }
114
- _run_main_with_io(json.dumps(qbug_payload))
115
- updated_state = _read_state(claude_job_directory)
116
- assert updated_state["bugteam_skill_invoked_at_head"] is None
117
- assert updated_state["bugteam_skill_invoked_at_tick"] is None
118
-
119
-
120
- def test_should_ignore_unrelated_skills(
121
- claude_job_directory: pathlib.Path,
122
- ) -> None:
123
- _write_state(claude_job_directory, _baseline_state())
124
- other_payload: dict[str, Any] = {
125
- "tool_name": "Skill",
126
- "tool_input": {"skill": "agent-prompt"},
127
- }
128
- _run_main_with_io(json.dumps(other_payload))
129
- updated_state = _read_state(claude_job_directory)
130
- assert updated_state["bugteam_skill_invoked_at_head"] is None
131
-
132
-
133
- def test_should_ignore_non_skill_tools(
134
- claude_job_directory: pathlib.Path,
135
- ) -> None:
136
- _write_state(claude_job_directory, _baseline_state())
137
- agent_payload: dict[str, Any] = {
138
- "tool_name": "Agent",
139
- "tool_input": {"subagent_type": "clean-coder", "prompt": "fix this"},
140
- }
141
- _run_main_with_io(json.dumps(agent_payload))
142
- updated_state = _read_state(claude_job_directory)
143
- assert updated_state["bugteam_skill_invoked_at_head"] is None
144
-
145
-
146
- def test_should_no_op_when_state_file_absent(
147
- claude_job_directory: pathlib.Path,
148
- ) -> None:
149
- skill_payload: dict[str, Any] = {
150
- "tool_name": "Skill",
151
- "tool_input": {"skill": "bugteam"},
152
- }
153
- _run_main_with_io(json.dumps(skill_payload))
154
- assert not (claude_job_directory / PR_CONVERGE_STATE_FILENAME).exists()
155
-
156
-
157
- def test_should_no_op_when_state_json_is_malformed(
158
- claude_job_directory: pathlib.Path,
159
- ) -> None:
160
- state_path = claude_job_directory / PR_CONVERGE_STATE_FILENAME
161
- state_path.write_text("{not valid json", encoding="utf-8")
162
- skill_payload: dict[str, Any] = {
163
- "tool_name": "Skill",
164
- "tool_input": {"skill": "bugteam"},
165
- }
166
- _run_main_with_io(json.dumps(skill_payload))
167
- assert state_path.read_text(encoding="utf-8") == "{not valid json"
168
-
169
-
170
- def test_should_preserve_other_state_fields_on_update(
171
- claude_job_directory: pathlib.Path,
172
- ) -> None:
173
- baseline_state = _baseline_state()
174
- baseline_state["bugbot_clean_at"] = _HEAD_SHA
175
- baseline_state["copilot_wait_count"] = 2
176
- _write_state(claude_job_directory, baseline_state)
177
- skill_payload: dict[str, Any] = {
178
- "tool_name": "Skill",
179
- "tool_input": {"skill": "bugteam"},
180
- }
181
- _run_main_with_io(json.dumps(skill_payload))
182
- updated_state = _read_state(claude_job_directory)
183
- assert updated_state["bugbot_clean_at"] == _HEAD_SHA
184
- assert updated_state["copilot_wait_count"] == 2
185
- assert updated_state["bugteam_skill_invoked_at_head"] == _HEAD_SHA
186
-
187
-
188
- def test_should_no_op_when_claude_job_dir_env_var_absent(
189
- monkeypatch: pytest.MonkeyPatch,
190
- ) -> None:
191
- monkeypatch.delenv(CLAUDE_JOB_DIR_ENV_VAR, raising=False)
192
- skill_payload: dict[str, Any] = {
193
- "tool_name": "Skill",
194
- "tool_input": {"skill": "bugteam"},
195
- }
196
- _run_main_with_io(json.dumps(skill_payload))
197
-
198
-
199
- def test_should_no_op_when_payload_is_malformed_json(
200
- claude_job_directory: pathlib.Path,
201
- ) -> None:
202
- baseline_state = _baseline_state()
203
- _write_state(claude_job_directory, baseline_state)
204
- _run_main_with_io("not valid json {{{")
205
- updated_state = _read_state(claude_job_directory)
206
- assert updated_state["bugteam_skill_invoked_at_head"] is None
207
-
208
-
209
- def test_should_leave_state_unchanged_when_current_head_is_missing(
210
- claude_job_directory: pathlib.Path,
211
- ) -> None:
212
- state_without_head = _baseline_state()
213
- del state_without_head["current_head"]
214
- _write_state(claude_job_directory, state_without_head)
215
- skill_payload: dict[str, Any] = {
216
- "tool_name": "Skill",
217
- "tool_input": {"skill": "bugteam"},
218
- }
219
- captured_stderr = _run_main_capturing_stderr(json.dumps(skill_payload))
220
- updated_state = _read_state(claude_job_directory)
221
- assert "current_head" not in updated_state
222
- assert updated_state.get("bugteam_skill_invoked_at_head") is None
223
- assert updated_state.get("bugteam_skill_invoked_at_tick") is None
224
- assert "current_head or tick_count" in captured_stderr
225
-
226
-
227
- def test_should_leave_state_unchanged_when_tick_count_is_missing(
228
- claude_job_directory: pathlib.Path,
229
- ) -> None:
230
- state_without_tick = _baseline_state()
231
- del state_without_tick["tick_count"]
232
- _write_state(claude_job_directory, state_without_tick)
233
- skill_payload: dict[str, Any] = {
234
- "tool_name": "Skill",
235
- "tool_input": {"skill": "bugteam"},
236
- }
237
- captured_stderr = _run_main_capturing_stderr(json.dumps(skill_payload))
238
- updated_state = _read_state(claude_job_directory)
239
- assert "tick_count" not in updated_state
240
- assert updated_state.get("bugteam_skill_invoked_at_head") is None
241
- assert updated_state.get("bugteam_skill_invoked_at_tick") is None
242
- assert "current_head or tick_count" in captured_stderr
243
-
244
-
245
- def test_should_preserve_prior_stamp_when_current_head_becomes_missing(
246
- claude_job_directory: pathlib.Path,
247
- ) -> None:
248
- prior_head = "feedface0000feedface0000feedface0000feed"
249
- prior_tick = 3
250
- state_with_prior_stamp = _baseline_state()
251
- state_with_prior_stamp["bugteam_skill_invoked_at_head"] = prior_head
252
- state_with_prior_stamp["bugteam_skill_invoked_at_tick"] = prior_tick
253
- del state_with_prior_stamp["current_head"]
254
- _write_state(claude_job_directory, state_with_prior_stamp)
255
- skill_payload: dict[str, Any] = {
256
- "tool_name": "Skill",
257
- "tool_input": {"skill": "bugteam"},
258
- }
259
- _run_main_capturing_stderr(json.dumps(skill_payload))
260
- updated_state = _read_state(claude_job_directory)
261
- assert updated_state["bugteam_skill_invoked_at_head"] == prior_head
262
- assert updated_state["bugteam_skill_invoked_at_tick"] == prior_tick
263
-
264
-
265
- def test_should_skip_atomic_write_when_state_missing_required_fields(
266
- claude_job_directory: pathlib.Path,
267
- ) -> None:
268
- state_without_head = _baseline_state()
269
- del state_without_head["current_head"]
270
- _write_state(claude_job_directory, state_without_head)
271
- skill_payload: dict[str, Any] = {
272
- "tool_name": "Skill",
273
- "tool_input": {"skill": "bugteam"},
274
- }
275
- with mock.patch.object(hook_module, "_atomic_write_state") as patched_atomic_write:
276
- with mock.patch("sys.stdin", io.StringIO(json.dumps(skill_payload))):
277
- with mock.patch("sys.stdout", new_callable=io.StringIO):
278
- with mock.patch("sys.stderr", new_callable=io.StringIO):
279
- try:
280
- hook_module.main()
281
- except SystemExit:
282
- pass
283
- assert patched_atomic_write.call_count == 0
@@ -1,22 +0,0 @@
1
- # Proof-of-Work PR Comments
2
-
3
- **When this applies:** Every pull request. One comment on the PR carries the proof of the work — posted after `gh pr create` or after a material new commit, and always before the PR leaves draft.
4
-
5
- ## The standard
6
-
7
- A proof comment has five parts:
8
-
9
- 1. **The exact commands run on real data.** A fenced code block showing the command(s) that produced the artifact — not test output alone.
10
- 2. **Measured outcomes.** Numbers read from the produced artifact — counts, dimensions, hashes, byte sizes, rankings — as a table or a bullet list of facts.
11
- 3. **Plan linkage.** One sentence naming the parent issue or phase the PR advances, with the issue reference (for example: "Advances phase 2 of issue #12").
12
- 4. **Visual evidence for values a human cannot read at a glance.** When the change is visual (the diff touches images, HTML, CSS, or hex color values), embed an image: hex colors as inline swatch images (`![](https://placehold.co/20x20/RRGGBB/RRGGBB.png)`), coordinates as marked-up screenshots, size changes as before/after pairs. A wall of raw values fails the standard even when every value is correct.
13
- 5. **Honest gaps.** State plainly what the offline proof cannot show, and what covers that gap.
14
-
15
- ## Enforcement
16
-
17
- The `pr_description_enforcer` hook enforces the standard at two points:
18
-
19
- - **On `gh pr comment`:** a comment body whose heading names proof or verification is audited for the five parts. The audit looks for a fenced code block with content, a table row or bullet line carrying a number, a line pairing an issue reference (`#123`) with a linkage word (issue, phase, plan, parent, advances, milestone, part of), an image embed when the PR diff is visual, and a gap phrase (gap, limitation, cannot, does not show, not shown, unverified, not covered). The block message names each missing part.
20
- - **On `gh pr ready`:** the hook reads the PR's comments and blocks readying while no comment passes the audit. `gh pr ready --undo` returns a PR to draft and is never blocked.
21
-
22
- A `gh` failure (network, auth, missing executable) never blocks — the gate fails open on tooling problems, and the comment audit skips bodies it cannot read.
@@ -1,4 +0,0 @@
1
- # Optional Verifier Review
2
-
3
- Code-verifier review is an optional workflow selected by the task owner.
4
- Commit and push operations use the checks selected for the task.
@@ -1,106 +0,0 @@
1
- <#
2
- .SYNOPSIS
3
- Opens files on screen, sizing each image window to the image's own dimensions.
4
-
5
- .DESCRIPTION
6
- For every path given, an image opens in a window whose client area matches the
7
- image's pixel size, scaled down to fit the primary screen's working area when the
8
- image is larger than the screen. A small image gets a usable minimum window with
9
- the picture centered at native size. Non-image files open in their registered
10
- default application, and any file that cannot be loaded as an image falls back to
11
- that default application too. Escape or the close button dismisses a window; the
12
- process exits once every window is closed.
13
-
14
- .PARAMETER Paths
15
- One or more file paths to open.
16
- #>
17
- param(
18
- [Parameter(ValueFromRemainingArguments = $true)]
19
- [string[]]$Paths
20
- )
21
-
22
- Add-Type -AssemblyName System.Windows.Forms
23
- Add-Type -AssemblyName System.Drawing
24
-
25
- try {
26
- [System.Windows.Forms.Application]::SetHighDpiMode([System.Windows.Forms.HighDpiMode]::PerMonitorV2) | Out-Null
27
- }
28
- catch {
29
- $null = $_
30
- }
31
- [System.Windows.Forms.Application]::EnableVisualStyles()
32
-
33
- $imageExtensions = @('.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp', '.tif', '.tiff', '.ico')
34
- $screenMargin = 80
35
- $minimumClientWidth = 220
36
- $minimumClientHeight = 160
37
- $openWindowCount = 0
38
-
39
- foreach ($path in $Paths) {
40
- if (-not (Test-Path -LiteralPath $path)) { continue }
41
- $fullPath = (Resolve-Path -LiteralPath $path).Path
42
- $extension = [System.IO.Path]::GetExtension($fullPath).ToLowerInvariant()
43
-
44
- if ($imageExtensions -notcontains $extension) {
45
- Invoke-Item -LiteralPath $fullPath
46
- continue
47
- }
48
-
49
- try {
50
- $imageBytes = [System.IO.File]::ReadAllBytes($fullPath)
51
- $imageStream = New-Object System.IO.MemoryStream(, $imageBytes)
52
- $loadedImage = [System.Drawing.Image]::FromStream($imageStream)
53
- $image = New-Object System.Drawing.Bitmap($loadedImage)
54
- $loadedImage.Dispose()
55
- $imageStream.Dispose()
56
- }
57
- catch {
58
- Invoke-Item -LiteralPath $fullPath
59
- continue
60
- }
61
-
62
- $workingArea = [System.Windows.Forms.Screen]::PrimaryScreen.WorkingArea
63
- $maximumWidth = $workingArea.Width - $screenMargin
64
- $maximumHeight = $workingArea.Height - $screenMargin
65
- $scale = [Math]::Min(1.0, [Math]::Min($maximumWidth / $image.Width, $maximumHeight / $image.Height))
66
-
67
- $pictureBox = New-Object System.Windows.Forms.PictureBox
68
- $pictureBox.Dock = [System.Windows.Forms.DockStyle]::Fill
69
- $pictureBox.Image = $image
70
-
71
- if ($scale -lt 1.0) {
72
- $pictureBox.SizeMode = [System.Windows.Forms.PictureBoxSizeMode]::Zoom
73
- $clientWidth = [int][Math]::Round($image.Width * $scale)
74
- $clientHeight = [int][Math]::Round($image.Height * $scale)
75
- }
76
- else {
77
- $pictureBox.SizeMode = [System.Windows.Forms.PictureBoxSizeMode]::CenterImage
78
- $clientWidth = [Math]::Max($minimumClientWidth, $image.Width)
79
- $clientHeight = [Math]::Max($minimumClientHeight, $image.Height)
80
- }
81
-
82
- $form = New-Object System.Windows.Forms.Form
83
- $form.Text = [System.IO.Path]::GetFileName($fullPath)
84
- $form.AutoScaleMode = [System.Windows.Forms.AutoScaleMode]::None
85
- $form.StartPosition = [System.Windows.Forms.FormStartPosition]::CenterScreen
86
- $form.ClientSize = New-Object System.Drawing.Size($clientWidth, $clientHeight)
87
- $form.KeyPreview = $true
88
- $form.BackColor = [System.Drawing.Color]::FromArgb(24, 24, 24)
89
- $form.Controls.Add($pictureBox)
90
-
91
- $form.Add_KeyDown({
92
- param($sender, $eventArguments)
93
- if ($eventArguments.KeyCode -eq [System.Windows.Forms.Keys]::Escape) { $sender.Close() }
94
- })
95
- $form.Add_FormClosed({
96
- $script:openWindowCount--
97
- if ($script:openWindowCount -le 0) { [System.Windows.Forms.Application]::Exit() }
98
- })
99
-
100
- $openWindowCount++
101
- $form.Show()
102
- }
103
-
104
- if ($openWindowCount -gt 0) {
105
- [System.Windows.Forms.Application]::Run()
106
- }