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,28 +0,0 @@
1
- """Contract: code-verifier cannot message a warm session-advisor."""
2
-
3
- from pathlib import Path
4
-
5
- _PACKAGE_ROOT = Path(__file__).resolve().parents[2]
6
- _CODE_VERIFIER_AGENT_PATH = _PACKAGE_ROOT / "agents" / "code-verifier.md"
7
- _FRONTMATTER_OPEN = "---\n"
8
- _FRONTMATTER_CLOSE = "\n---\n"
9
- _TOOLS_PREFIX = "tools:"
10
- _SEND_MESSAGE_TOOL_NAME = "SendMessage"
11
-
12
-
13
- def test_code_verifier_tools_exclude_sendmessage() -> None:
14
- """tools frontmatter must not list SendMessage (warm-advisor path closed)."""
15
- agent_text = _CODE_VERIFIER_AGENT_PATH.read_text(encoding="utf-8")
16
- assert agent_text.startswith(_FRONTMATTER_OPEN), "missing frontmatter open"
17
- frontmatter_end = agent_text.find(_FRONTMATTER_CLOSE, len(_FRONTMATTER_OPEN))
18
- assert frontmatter_end > 0, "missing frontmatter close"
19
- frontmatter_body = agent_text[len(_FRONTMATTER_OPEN) : frontmatter_end]
20
- all_tools_lines = [
21
- each_line
22
- for each_line in frontmatter_body.splitlines()
23
- if each_line.startswith(_TOOLS_PREFIX)
24
- ]
25
- assert len(all_tools_lines) == 1, all_tools_lines
26
- tools_value = all_tools_lines[0].removeprefix(_TOOLS_PREFIX).strip()
27
- all_tool_names = {each_name.strip() for each_name in tools_value.split(",")}
28
- assert _SEND_MESSAGE_TOOL_NAME not in all_tool_names, all_tool_names
@@ -1,428 +0,0 @@
1
- """Unit tests for convergence-gate-blocker PreToolUse hook."""
2
-
3
- import importlib.util
4
- import io
5
- import json
6
- import pathlib
7
- import re
8
- import subprocess
9
- import sys
10
-
11
- import pytest
12
-
13
- _HOOK_DIR = pathlib.Path(__file__).parent
14
- if str(_HOOK_DIR) not in sys.path:
15
- sys.path.insert(0, str(_HOOK_DIR))
16
-
17
- _GH_PR_READY_PATTERN = re.compile(r"\bgh\s+pr\s+ready\b(?![^&|;\n]*--undo)")
18
-
19
- hook_spec = importlib.util.spec_from_file_location(
20
- "convergence_gate_blocker",
21
- _HOOK_DIR / "convergence_gate_blocker.py",
22
- )
23
- assert hook_spec is not None
24
- assert hook_spec.loader is not None
25
- hook_module = importlib.util.module_from_spec(hook_spec)
26
- hook_spec.loader.exec_module(hook_module)
27
- _resolve_pr_number = hook_module._resolve_pr_number
28
-
29
-
30
- def test_matches_gh_pr_ready_with_number() -> None:
31
- assert _GH_PR_READY_PATTERN.search("gh pr ready 418")
32
-
33
-
34
- def test_matches_gh_pr_ready_without_number() -> None:
35
- assert _GH_PR_READY_PATTERN.search("gh pr ready")
36
-
37
-
38
- def test_matches_gh_pr_ready_with_flags() -> None:
39
- assert not _GH_PR_READY_PATTERN.search("gh pr ready --undo")
40
-
41
-
42
- def test_does_not_match_gh_pr_create() -> None:
43
- assert not _GH_PR_READY_PATTERN.search("gh pr create --title T")
44
-
45
-
46
- def test_does_not_match_gh_pr_view() -> None:
47
- assert not _GH_PR_READY_PATTERN.search("gh pr view 418")
48
-
49
-
50
- def test_does_not_match_gh_issue_close() -> None:
51
- assert not _GH_PR_READY_PATTERN.search("gh issue close 42")
52
-
53
-
54
- def test_extracts_pr_number_from_command() -> None:
55
- assert _resolve_pr_number("gh pr ready 418", None, None, None) == 418
56
-
57
-
58
- def test_extracts_pr_number_when_repo_flag_precedes_number() -> None:
59
- assert (
60
- _resolve_pr_number(
61
- "gh pr ready --repo sample-owner/target-repo 161",
62
- None,
63
- "sample-owner",
64
- "target-repo",
65
- )
66
- == 161
67
- )
68
-
69
-
70
- def test_extracts_pr_number_with_flags() -> None:
71
- assert _resolve_pr_number("gh pr ready 99 --undo", None, None, None) == 99
72
-
73
-
74
- def test_returns_none_when_no_number_and_no_repo() -> None:
75
- assert _resolve_pr_number("gh pr ready", "/nonexistent/path", None, None) is None
76
-
77
-
78
- def test_number_resolution_binds_gh_view_to_named_repo(
79
- monkeypatch: pytest.MonkeyPatch,
80
- ) -> None:
81
- captured_commands: list[list[str]] = []
82
-
83
- def fake_run(all_arguments: list[str], **_kwargs: object) -> subprocess.CompletedProcess[str]:
84
- captured_commands.append(all_arguments)
85
- return subprocess.CompletedProcess(args=all_arguments, returncode=0, stdout="500\n", stderr="")
86
-
87
- monkeypatch.setattr(hook_module.subprocess, "run", fake_run)
88
- assert _resolve_pr_number("gh pr ready", None, "flag-owner", "flag-repo") == 500
89
- assert captured_commands
90
- for each_command in captured_commands:
91
- assert "--repo" in each_command
92
- assert "flag-owner/flag-repo" in each_command
93
-
94
-
95
- def test_matches_gh_pr_ready_in_compound_command() -> None:
96
- assert not _GH_PR_READY_PATTERN.search("gh pr ready --undo && gh pr create")
97
-
98
-
99
- def test_run_convergence_check_forwards_cwd_to_subprocess(
100
- monkeypatch: pytest.MonkeyPatch,
101
- ) -> None:
102
- captured_cwd: list[object] = []
103
-
104
- def fake_run(*_run_args: object, **run_keywords: object) -> subprocess.CompletedProcess[str]:
105
- captured_cwd.append(run_keywords.get("cwd"))
106
- return subprocess.CompletedProcess(args=[], returncode=0, stdout="", stderr="")
107
-
108
- monkeypatch.setattr(hook_module.subprocess, "run", fake_run)
109
- hook_module._run_convergence_check(
110
- "check_convergence.py", "owner", "repo", 783, "C:/worktrees/pr-783"
111
- )
112
- assert captured_cwd == ["C:/worktrees/pr-783"]
113
-
114
-
115
- def test_run_convergence_check_forwards_none_cwd_as_none(
116
- monkeypatch: pytest.MonkeyPatch,
117
- ) -> None:
118
- captured_cwd: list[object] = []
119
-
120
- def fake_run(*_run_args: object, **run_keywords: object) -> subprocess.CompletedProcess[str]:
121
- captured_cwd.append(run_keywords.get("cwd"))
122
- return subprocess.CompletedProcess(args=[], returncode=0, stdout="", stderr="")
123
-
124
- monkeypatch.setattr(hook_module.subprocess, "run", fake_run)
125
- hook_module._run_convergence_check("check_convergence.py", "owner", "repo", 783, None)
126
- assert captured_cwd == [None]
127
-
128
-
129
- def test_main_reads_cwd_from_top_level_payload(
130
- monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
131
- ) -> None:
132
- convergence_script = (
133
- tmp_path / ".claude" / "skills" / "pr-converge" / "scripts" / "check_convergence.py"
134
- )
135
- convergence_script.parent.mkdir(parents=True)
136
- convergence_script.write_text("")
137
- monkeypatch.setattr(hook_module.Path, "home", classmethod(lambda _cls: tmp_path))
138
-
139
- worktree_path = str(tmp_path / "worktrees" / "pr-783")
140
- monkeypatch.setattr(hook_module, "_resolve_owner_repo", lambda _cwd: ("jl-cmd", "repo"))
141
-
142
- captured_cwd: list[object] = []
143
-
144
- def fake_check(
145
- _script: str, _owner: str, _repo: str, _pr_number: int, cwd: str | None
146
- ) -> subprocess.CompletedProcess[str]:
147
- captured_cwd.append(cwd)
148
- return subprocess.CompletedProcess(args=[], returncode=0, stdout="", stderr="")
149
-
150
- monkeypatch.setattr(hook_module, "_run_convergence_check", fake_check)
151
-
152
- payload = {
153
- "tool_name": "Bash",
154
- "cwd": worktree_path,
155
- "tool_input": {"command": "gh pr ready 783", "cwd": "C:/wrong/session/dir"},
156
- }
157
- monkeypatch.setattr(sys, "stdin", io.StringIO(json.dumps(payload)))
158
-
159
- with pytest.raises(SystemExit):
160
- hook_module.main()
161
-
162
- assert captured_cwd == [worktree_path]
163
-
164
-
165
- def _capture_convergence_identity(
166
- monkeypatch: pytest.MonkeyPatch,
167
- tmp_path: pathlib.Path,
168
- command: str,
169
- cwd_owner_repo: tuple[str, str],
170
- ) -> tuple[str, str, int]:
171
- convergence_script = (
172
- tmp_path / ".claude" / "skills" / "pr-converge" / "scripts" / "check_convergence.py"
173
- )
174
- convergence_script.parent.mkdir(parents=True)
175
- convergence_script.write_text("")
176
- monkeypatch.setattr(hook_module.Path, "home", classmethod(lambda _cls: tmp_path))
177
- monkeypatch.setattr(hook_module, "_resolve_owner_repo", lambda _cwd: cwd_owner_repo)
178
-
179
- captured_identity: list[tuple[str, str, int]] = []
180
-
181
- def fake_check(
182
- _script: str, owner: str, repo: str, pr_number: int, _cwd: str | None
183
- ) -> subprocess.CompletedProcess[str]:
184
- captured_identity.append((owner, repo, pr_number))
185
- return subprocess.CompletedProcess(args=[], returncode=0, stdout="", stderr="")
186
-
187
- monkeypatch.setattr(hook_module, "_run_convergence_check", fake_check)
188
-
189
- payload = {
190
- "tool_name": "Bash",
191
- "cwd": str(tmp_path / "worktree"),
192
- "tool_input": {"command": command},
193
- }
194
- monkeypatch.setattr(sys, "stdin", io.StringIO(json.dumps(payload)))
195
- with pytest.raises(SystemExit):
196
- hook_module.main()
197
- return captured_identity[0]
198
-
199
-
200
- def test_repo_flag_overrides_cwd_repo(
201
- monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
202
- ) -> None:
203
- identity = _capture_convergence_identity(
204
- monkeypatch,
205
- tmp_path,
206
- "gh pr ready 161 --repo sample-owner/target-repo",
207
- ("cwd-owner", "cwd-repo"),
208
- )
209
- assert identity == ("sample-owner", "target-repo", 161)
210
-
211
-
212
- def test_pr_url_yields_repo_and_number(
213
- monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
214
- ) -> None:
215
- identity = _capture_convergence_identity(
216
- monkeypatch,
217
- tmp_path,
218
- "gh pr ready https://github.com/sample-owner/target-repo/pull/161",
219
- ("cwd-owner", "cwd-repo"),
220
- )
221
- assert identity == ("sample-owner", "target-repo", 161)
222
-
223
-
224
- def test_bare_pr_ready_resolves_repo_from_cwd(
225
- monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
226
- ) -> None:
227
- identity = _capture_convergence_identity(
228
- monkeypatch,
229
- tmp_path,
230
- "gh pr ready 418",
231
- ("cwd-owner", "cwd-repo"),
232
- )
233
- assert identity == ("cwd-owner", "cwd-repo", 418)
234
-
235
-
236
- def test_parse_repo_flag_space_form() -> None:
237
- assert hook_module._parse_repo_flag("gh pr ready 161 --repo sample-owner/target-repo") == (
238
- "sample-owner",
239
- "target-repo",
240
- )
241
-
242
-
243
- def test_parse_repo_flag_equals_form() -> None:
244
- assert hook_module._parse_repo_flag("gh pr ready 161 --repo=sample-owner/target-repo") == (
245
- "sample-owner",
246
- "target-repo",
247
- )
248
-
249
-
250
- def test_parse_repo_flag_short_alias() -> None:
251
- assert hook_module._parse_repo_flag("gh pr ready 161 -R sample-owner/target-repo") == (
252
- "sample-owner",
253
- "target-repo",
254
- )
255
-
256
-
257
- def test_parse_repo_flag_short_alias_attached_value() -> None:
258
- assert hook_module._parse_repo_flag("gh pr ready 161 -Rsample-owner/target-repo") == (
259
- "sample-owner",
260
- "target-repo",
261
- )
262
-
263
-
264
- def test_parse_repo_flag_absent_returns_none() -> None:
265
- assert hook_module._parse_repo_flag("gh pr ready 418") is None
266
-
267
-
268
- def test_parse_pr_url_yields_owner_repo_number() -> None:
269
- assert hook_module._parse_pr_url(
270
- "gh pr ready https://github.com/sample-owner/target-repo/pull/161"
271
- ) == ("sample-owner", "target-repo", 161)
272
-
273
-
274
- def test_parse_pr_url_absent_returns_none() -> None:
275
- assert hook_module._parse_pr_url("gh pr ready 418") is None
276
-
277
-
278
- def test_ready_segment_clips_at_command_separator() -> None:
279
- assert (
280
- hook_module._ready_command_segment(
281
- "gh pr ready 161 && gh pr comment 999 --repo other-owner/other-repo"
282
- )
283
- == "gh pr ready 161 "
284
- )
285
-
286
-
287
- def test_ready_segment_keeps_repo_flag_on_a_continued_line() -> None:
288
- segment = hook_module._ready_command_segment(
289
- "gh pr ready 161 \\\n --repo target-owner/target-repo"
290
- )
291
- assert "--repo" in segment
292
- assert "target-owner/target-repo" in segment
293
-
294
-
295
- def test_continued_repo_flag_binds_the_gate_to_the_named_repo(
296
- monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
297
- ) -> None:
298
- identity = _capture_convergence_identity(
299
- monkeypatch,
300
- tmp_path,
301
- "gh pr ready 161 \\\n --repo target-owner/target-repo",
302
- ("cwd-owner", "cwd-repo"),
303
- )
304
- assert identity == ("target-owner", "target-repo", 161)
305
-
306
-
307
- def test_chained_repo_flag_does_not_misbind_the_gate(
308
- monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
309
- ) -> None:
310
- identity = _capture_convergence_identity(
311
- monkeypatch,
312
- tmp_path,
313
- "gh pr ready 161 && gh pr comment 999 --repo other-owner/other-repo --body-file note.md",
314
- ("cwd-owner", "cwd-repo"),
315
- )
316
- assert identity == ("cwd-owner", "cwd-repo", 161)
317
-
318
-
319
- def test_chained_pr_url_does_not_misbind_the_gate(
320
- monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
321
- ) -> None:
322
- identity = _capture_convergence_identity(
323
- monkeypatch,
324
- tmp_path,
325
- "gh pr ready 161 && echo https://github.com/other-owner/other-repo/pull/5",
326
- ("cwd-owner", "cwd-repo"),
327
- )
328
- assert identity == ("cwd-owner", "cwd-repo", 161)
329
-
330
-
331
- def test_ready_segment_skips_a_leading_undo_invocation() -> None:
332
- assert (
333
- hook_module._ready_command_segment("gh pr ready --undo && gh pr ready 161")
334
- == "gh pr ready 161"
335
- )
336
-
337
-
338
- def test_parse_repo_flag_full_url_form() -> None:
339
- assert hook_module._parse_repo_flag(
340
- "gh pr ready 161 --repo https://github.com/other-owner/other-repo"
341
- ) == ("other-owner", "other-repo")
342
-
343
-
344
- def test_parse_repo_flag_short_alias_full_url_form() -> None:
345
- assert hook_module._parse_repo_flag(
346
- "gh pr ready 161 -R https://github.com/other-owner/other-repo"
347
- ) == ("other-owner", "other-repo")
348
-
349
-
350
- def test_parse_repo_flag_ssh_form() -> None:
351
- assert hook_module._parse_repo_flag(
352
- "gh pr ready 161 --repo git@github.com:other-owner/other-repo"
353
- ) == ("other-owner", "other-repo")
354
-
355
-
356
- def test_parse_repo_flag_ssh_form_strips_git_suffix() -> None:
357
- assert hook_module._parse_repo_flag(
358
- "gh pr ready 161 --repo git@github.com:other-owner/other-repo.git"
359
- ) == ("other-owner", "other-repo")
360
-
361
-
362
- def test_parse_repo_flag_url_form_strips_git_suffix() -> None:
363
- assert hook_module._parse_repo_flag(
364
- "gh pr ready 161 --repo https://github.com/other-owner/other-repo.git"
365
- ) == ("other-owner", "other-repo")
366
-
367
-
368
- def test_parse_pr_url_accepts_enterprise_host() -> None:
369
- assert hook_module._parse_pr_url(
370
- "gh pr ready https://github.example.com/other-owner/other-repo/pull/161"
371
- ) == ("other-owner", "other-repo", 161)
372
-
373
-
374
- def test_background_operator_clips_chained_repo_flag(
375
- monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
376
- ) -> None:
377
- identity = _capture_convergence_identity(
378
- monkeypatch,
379
- tmp_path,
380
- "gh pr ready 161 & gh pr comment 999 --repo other-owner/other-repo",
381
- ("cwd-owner", "cwd-repo"),
382
- )
383
- assert identity == ("cwd-owner", "cwd-repo", 161)
384
-
385
-
386
- def test_trailing_redirect_keeps_earlier_repo_flag(
387
- monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
388
- ) -> None:
389
- identity = _capture_convergence_identity(
390
- monkeypatch,
391
- tmp_path,
392
- "gh pr ready 161 --repo other-owner/other-repo 2>&1",
393
- ("cwd-owner", "cwd-repo"),
394
- )
395
- assert identity == ("other-owner", "other-repo", 161)
396
-
397
-
398
- def test_stderr_redirect_dup_keeps_repo_flag() -> None:
399
- segment = hook_module._ready_command_segment(
400
- "gh pr ready 161 >&2 --repo other-owner/other-repo"
401
- )
402
- assert hook_module._parse_repo_flag(segment) == ("other-owner", "other-repo")
403
-
404
-
405
- def test_stderr_to_stdout_redirect_keeps_repo_flag() -> None:
406
- segment = hook_module._ready_command_segment(
407
- "gh pr ready 2>&1 --repo other-owner/other-repo"
408
- )
409
- assert hook_module._parse_repo_flag(segment) == ("other-owner", "other-repo")
410
-
411
-
412
- def test_append_all_output_redirect_keeps_repo_flag() -> None:
413
- segment = hook_module._ready_command_segment(
414
- "gh pr ready 161 &> log --repo other-owner/other-repo"
415
- )
416
- assert hook_module._parse_repo_flag(segment) == ("other-owner", "other-repo")
417
-
418
-
419
- def test_stderr_redirect_binds_repo_end_to_end(
420
- monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
421
- ) -> None:
422
- identity = _capture_convergence_identity(
423
- monkeypatch,
424
- tmp_path,
425
- "gh pr ready 161 >&2 --repo other-owner/other-repo",
426
- ("cwd-owner", "cwd-repo"),
427
- )
428
- assert identity == ("other-owner", "other-repo", 161)