claude-dev-env 2.3.0 → 2.5.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 (290) hide show
  1. package/CLAUDE.md +53 -48
  2. package/_shared/pr-loop/scripts/_claude_permissions_common.py +84 -0
  3. package/_shared/pr-loop/scripts/code_rules_gate.py +4 -2
  4. package/_shared/pr-loop/scripts/grant_project_claude_permissions.py +306 -306
  5. package/_shared/pr-loop/scripts/pr_loop_shared_constants/claude_permissions_constants.py +44 -0
  6. package/_shared/pr-loop/scripts/pr_loop_shared_constants/copilot_quota_constants.py +24 -24
  7. package/_shared/pr-loop/scripts/pr_loop_shared_constants/stale_worktree_rule_sweep_constants.py +107 -107
  8. package/_shared/pr-loop/scripts/revoke_project_claude_permissions.py +290 -48
  9. package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +42 -2
  10. package/_shared/pr-loop/scripts/tests/test_claude_permissions_constants.py +36 -0
  11. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +100 -1
  12. package/_shared/pr-loop/scripts/tests/test_fix_hookspath.py +497 -497
  13. package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +311 -2
  14. package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep.py +301 -301
  15. package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep_constants.py +85 -85
  16. package/_shared/pr-loop/worker-spawn.md +1 -1
  17. package/agents/CLAUDE.md +3 -1
  18. package/agents/caveman.md +0 -1
  19. package/agents/clasp-deployment-orchestrator.md +0 -1
  20. package/agents/clean-coder.md +0 -1
  21. package/agents/code-advisor.md +0 -1
  22. package/agents/code-quality-agent.md +1 -2
  23. package/agents/code-verifier.md +3 -4
  24. package/agents/deep-research.md +0 -1
  25. package/agents/docs-agent.md +0 -1
  26. package/agents/git-commit-crafter.md +0 -1
  27. package/agents/issue-tracker.md +42 -0
  28. package/agents/plan-packet-validator.md +0 -1
  29. package/agents/pr-description-writer.md +0 -1
  30. package/agents/skill-writer-agent.md +84 -0
  31. package/agents/test_agent_frontmatter.py +67 -18
  32. package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +105 -3
  33. package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +29 -13
  34. package/bin/CLAUDE.md +68 -5
  35. package/bin/ever-shipped-skills.mjs +1 -0
  36. package/bin/install-constants.mjs +88 -0
  37. package/bin/install.mjs +1138 -114
  38. package/bin/install.prune.test.mjs +869 -19
  39. package/bin/install.test.mjs +906 -2
  40. package/commands/implement.md +1 -1
  41. package/commands/right-size.md +1 -1
  42. package/docs/CLAUDE.md +2 -0
  43. package/docs/host-pool-health-monitor.md +102 -0
  44. package/docs/references/CLAUDE.md +5 -2
  45. package/docs/references/advisor-tool.md +13 -0
  46. package/docs/references/code-review-enforcement.md +107 -0
  47. package/docs/references/team-advisor-skill.md +14 -0
  48. package/docs/wsl-docker-cowork-starter-matrix.md +89 -0
  49. package/hooks/blocking/CLAUDE.md +9 -1
  50. package/hooks/blocking/code_review_enforcement_config_bootstrap.py +53 -0
  51. package/hooks/blocking/code_review_gate_deny.py +74 -0
  52. package/hooks/blocking/code_review_pr_create_gate.py +198 -0
  53. package/hooks/blocking/code_review_push_gate.py +145 -0
  54. package/hooks/blocking/code_review_stamp_directory_write_blocker.py +348 -0
  55. package/hooks/blocking/code_review_stamp_store.py +233 -0
  56. package/hooks/blocking/code_review_stamp_write_blocker_parts/__init__.py +7 -0
  57. package/hooks/blocking/code_review_stamp_write_blocker_parts/conftest.py +15 -0
  58. package/hooks/blocking/code_review_stamp_write_blocker_parts/obfuscated_stamp_path_reference.py +212 -0
  59. package/hooks/blocking/code_review_stamp_write_blocker_parts/split_directory_change_into_stamp.py +138 -0
  60. package/hooks/blocking/code_review_stamp_write_blocker_parts/test_obfuscated_stamp_path_reference.py +49 -0
  61. package/hooks/blocking/code_review_stamp_write_blocker_parts/test_split_directory_change_into_stamp.py +38 -0
  62. package/hooks/blocking/code_verifier_spawn_preflight_gate.py +39 -27
  63. package/hooks/blocking/config/__init__.py +5 -5
  64. package/hooks/blocking/config/code_review_enforcement_constants.py +113 -0
  65. package/hooks/blocking/config/test_code_review_enforcement_constants.py +113 -0
  66. package/hooks/blocking/config/verified_commit_constants.py +160 -155
  67. package/hooks/blocking/conftest.py +2 -0
  68. package/hooks/blocking/convergence_gate_blocker.py +112 -23
  69. package/hooks/blocking/destructive_command_blocker.py +19 -6
  70. package/hooks/blocking/orchestrator_refresh_reschedule_gate.py +256 -0
  71. package/hooks/blocking/pr_description_proof_of_work.py +52 -34
  72. package/hooks/blocking/pre_tool_use_dispatcher.py +24 -24
  73. package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +4 -1
  74. package/hooks/blocking/test_code_review_enforcement_config_bootstrap.py +62 -0
  75. package/hooks/blocking/test_code_review_gate_deny.py +54 -0
  76. package/hooks/blocking/test_code_review_pr_create_gate.py +199 -0
  77. package/hooks/blocking/test_code_review_push_gate.py +205 -0
  78. package/hooks/blocking/test_code_review_stamp_directory_write_blocker.py +199 -0
  79. package/hooks/blocking/test_code_review_stamp_store.py +205 -0
  80. package/hooks/blocking/test_code_verifier_spawn_preflight_gate.py +124 -2
  81. package/hooks/blocking/test_convergence_gate_blocker.py +153 -5
  82. package/hooks/blocking/test_destructive_command_blocker.py +1 -1
  83. package/hooks/blocking/test_destructive_command_blocker_deny_mode.py +45 -0
  84. package/hooks/blocking/test_orchestrator_refresh_reschedule_gate.py +231 -0
  85. package/hooks/blocking/test_pr_description_proof_of_work.py +151 -0
  86. package/hooks/blocking/test_pre_tool_use_dispatcher.py +17 -8
  87. package/hooks/blocking/test_verdict_directory_write_blocker.py +808 -808
  88. package/hooks/blocking/test_verification_verdict_store.py +974 -903
  89. package/hooks/blocking/test_verified_commit_gate.py +581 -581
  90. package/hooks/blocking/test_verified_commit_message_accuracy_blocker.py +131 -131
  91. package/hooks/blocking/test_volatile_path_in_post_blocker.py +114 -2
  92. package/hooks/blocking/verdict_directory_write_blocker.py +687 -687
  93. package/hooks/blocking/verification_verdict_store.py +1039 -1014
  94. package/hooks/blocking/verified_commit_gate_parts/gated_invocations.py +29 -17
  95. package/hooks/blocking/verified_commit_gate_parts/tests/test_gated_invocations.py +35 -0
  96. package/hooks/blocking/verified_commit_message_accuracy_blocker.py +167 -167
  97. package/hooks/blocking/verifier_verdict_minter.py +280 -280
  98. package/hooks/blocking/volatile_path_in_post_blocker.py +69 -8
  99. package/hooks/git-hooks/git_hooks_constants/__init__.py +6 -0
  100. package/hooks/git-hooks/pre_push.py +89 -2
  101. package/hooks/git-hooks/test_pre_push.py +128 -0
  102. package/hooks/hooks.json +26 -1
  103. package/hooks/hooks_constants/CLAUDE.md +3 -1
  104. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +8 -0
  105. package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -0
  106. package/hooks/hooks_constants/code_verifier_spawn_preflight_gate_constants.py +26 -11
  107. package/hooks/hooks_constants/convergence_gate_blocker_constants.py +20 -3
  108. package/hooks/hooks_constants/destructive_command_segment_constants.py +3 -1
  109. package/hooks/hooks_constants/enter_worktree_prefetch_constants.py +18 -18
  110. package/hooks/hooks_constants/orchestrator_refresh_reschedule_gate_constants.py +48 -0
  111. package/hooks/hooks_constants/pr_description_proof_of_work_constants.py +0 -4
  112. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +4 -0
  113. package/hooks/hooks_constants/pyproject_config_discovery_constants.py +16 -0
  114. package/hooks/hooks_constants/ruff_integration_constants.py +16 -0
  115. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +24 -0
  116. package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +6 -0
  117. package/hooks/hooks_constants/volatile_path_in_post_blocker_constants.py +8 -1
  118. package/hooks/lifecycle/enter_worktree_origin_prefetch.py +163 -146
  119. package/hooks/lifecycle/test_enter_worktree_origin_prefetch.py +185 -178
  120. package/hooks/pyproject.toml +1 -0
  121. package/hooks/validators/CLAUDE.md +2 -0
  122. package/hooks/validators/config/__init__.py +0 -0
  123. package/hooks/validators/config/directory_exemption_constants.py +183 -0
  124. package/hooks/validators/config/test_directory_exemption_constants.py +21 -0
  125. package/hooks/validators/conftest.py +4 -0
  126. package/hooks/validators/mypy_integration.py +63 -50
  127. package/hooks/validators/pyproject_config_discovery.py +101 -0
  128. package/hooks/validators/ruff_integration.py +257 -25
  129. package/hooks/validators/run_all_validators.py +223 -19
  130. package/hooks/validators/test_directory_exemption_constants.py +185 -0
  131. package/hooks/validators/test_mypy_integration.py +32 -0
  132. package/hooks/validators/test_pyproject_config_discovery.py +94 -0
  133. package/hooks/validators/test_python_antipattern_checks.py +110 -5
  134. package/hooks/validators/test_ruff_integration.py +160 -2
  135. package/hooks/validators/test_run_all_validators.py +140 -68
  136. package/hooks/validators/test_run_all_validators_config_discovery.py +123 -0
  137. package/hooks/validators/test_run_all_validators_pretooluse.py +159 -1
  138. package/package.json +10 -2
  139. package/rules/CLAUDE.md +1 -0
  140. package/rules/docstring-prose-matches-implementation.md +45 -67
  141. package/rules/durable-post-artifacts.md +7 -0
  142. package/rules/state-what-is.md +25 -0
  143. package/rules/verified-commit-gate-skip.md +1 -1
  144. package/scripts/CLAUDE.md +1 -0
  145. package/scripts/Capture-PoolHealth.ps1 +410 -0
  146. package/scripts/_code_review_test_support.py +404 -0
  147. package/scripts/claude_chain_runner.py +141 -1
  148. package/scripts/codec_forwarding_test_support.py +83 -0
  149. package/scripts/conftest.py +23 -0
  150. package/scripts/dev_env_scripts_constants/CLAUDE.md +2 -2
  151. package/scripts/dev_env_scripts_constants/claude_chain_constants.py +53 -1
  152. package/scripts/dev_env_scripts_constants/code_review_constants.py +129 -12
  153. package/scripts/dev_env_scripts_constants/test_code_review_constants.py +55 -0
  154. package/scripts/invoke_code_review.py +550 -38
  155. package/scripts/resolve_worker_spawn.py +626 -619
  156. package/scripts/spawn_grok_batch.py +672 -672
  157. package/scripts/test_claude_chain_runner.py +131 -0
  158. package/scripts/test_invoke_code_review_chain.py +70 -0
  159. package/scripts/test_invoke_code_review_cli.py +192 -0
  160. package/scripts/test_invoke_code_review_codec.py +77 -0
  161. package/scripts/test_invoke_code_review_contract.py +256 -0
  162. package/scripts/test_invoke_code_review_git.py +123 -0
  163. package/scripts/test_invoke_code_review_mode.py +99 -0
  164. package/scripts/test_resolve_worker_spawn.py +1014 -1014
  165. package/scripts/test_resolve_worker_spawn_codec.py +101 -0
  166. package/skills/CLAUDE.md +2 -0
  167. package/skills/auditing-claude-config/SKILL.md +114 -114
  168. package/skills/autoconverge/SKILL.md +427 -421
  169. package/skills/autoconverge/reference/convergence.md +26 -4
  170. package/skills/autoconverge/reference/multi-pr.md +6 -1
  171. package/skills/autoconverge/reference/stop-conditions.md +16 -10
  172. package/skills/autoconverge/workflow/CLAUDE.md +1 -0
  173. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +4 -4
  174. package/skills/autoconverge/workflow/converge.codex-gate.test.mjs +175 -3
  175. package/skills/autoconverge/workflow/converge.contract.test.mjs +1263 -1244
  176. package/skills/autoconverge/workflow/converge.mjs +191 -8
  177. package/skills/autoconverge/workflow/converge.p2-advance.test.mjs +202 -0
  178. package/skills/autoconverge/workflow/converge_multi.mjs +7 -3
  179. package/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +5 -0
  180. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a11d903476b803493.jsonl +2 -2
  181. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a26213978adeef6fb.jsonl +2 -2
  182. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a3def0d15ed9d9110.jsonl +2 -2
  183. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a41f41b1b708ee3b7.jsonl +2 -2
  184. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a758b880abecc3ff7.jsonl +2 -2
  185. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a8897b89656b1bd16.jsonl +2 -2
  186. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-abd463d744a1437bc.jsonl +2 -2
  187. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ad19d027ae8ee1816.jsonl +2 -2
  188. package/skills/autoconverge/workflow/fixtures/wf_run/workflows/wf_881252e6-700.json +265 -265
  189. package/skills/closeout/SKILL.md +33 -50
  190. package/skills/codex-review/scripts/codex_review_scripts_constants/run_constants.py +8 -0
  191. package/skills/codex-review/scripts/run_codex_review.py +233 -1
  192. package/skills/codex-review/scripts/test_run_codex_review.py +189 -0
  193. package/skills/condensing-instructions/SKILL.md +81 -0
  194. package/skills/copilot-review/SKILL.md +119 -119
  195. package/skills/e-code-review/SKILL.md +52 -0
  196. package/skills/e-code-review/reference/fix.md +54 -0
  197. package/skills/e-code-review/reference/loop.md +43 -0
  198. package/skills/e-code-review/reference/low.md +57 -0
  199. package/skills/e-code-review/reference/medium.md +153 -0
  200. package/skills/e-code-review/reference/xhigh.md +182 -0
  201. package/skills/e-simplify/SKILL.md +97 -0
  202. package/skills/fresh-branch/CLAUDE.md +2 -0
  203. package/skills/fresh-branch/SKILL.md +2 -0
  204. package/skills/fresh-branch/scripts/create_fresh_branch.py +78 -180
  205. package/skills/fresh-branch/scripts/fresh_branch_git_commands.py +285 -0
  206. package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +1 -0
  207. package/skills/fresh-branch/scripts/pytest.ini +4 -0
  208. package/skills/fresh-branch/scripts/test_create_fresh_branch.py +98 -0
  209. package/skills/fresh-branch/scripts/test_fresh_branch_git_commands.py +310 -0
  210. package/skills/issue-tracker/SKILL.md +92 -0
  211. package/skills/issue-tracker/reference/epic-and-sub-issue-model.md +55 -0
  212. package/skills/issue-tracker/reference/handoff-schema.md +64 -0
  213. package/skills/issue-tracker/reference/operation-matrix.md +41 -0
  214. package/skills/orchestrator/SKILL.md +162 -21
  215. package/skills/orchestrator/scripts/status_gate.py +625 -0
  216. package/skills/orchestrator/scripts/status_gate_constants/__init__.py +1 -0
  217. package/skills/orchestrator/scripts/status_gate_constants/config/__init__.py +1 -0
  218. package/skills/orchestrator/scripts/status_gate_constants/config/constants.py +47 -0
  219. package/skills/orchestrator/scripts/test_status_gate.py +439 -0
  220. package/skills/orchestrator-refresh/SKILL.md +110 -35
  221. package/skills/plan-to-pr/SKILL.md +155 -0
  222. package/skills/plan-to-pr/reference/final-validation-tasks.md +15 -0
  223. package/skills/plan-to-pr/reference/model-routing.md +36 -0
  224. package/skills/plan-to-pr/reference/packet-contract.md +43 -0
  225. package/skills/plan-to-pr/reference/packet-schema.json +57 -0
  226. package/skills/plan-to-pr/reference/process-inventory.md +22 -0
  227. package/skills/plan-to-pr/reference/review-loop.md +33 -0
  228. package/skills/plan-to-pr/reference/run-record.schema.json +27 -0
  229. package/skills/plan-to-pr/reference/self-audit-tasks.md +15 -0
  230. package/skills/plan-to-pr/reference/task-seeds.md +14 -0
  231. package/skills/plan-to-pr/reference/task-ticket.md +38 -0
  232. package/skills/plan-to-pr/scripts/config/__init__.py +1 -0
  233. package/skills/plan-to-pr/scripts/config/constants.py +193 -0
  234. package/skills/plan-to-pr/scripts/create_packet.py +173 -0
  235. package/skills/plan-to-pr/scripts/test_create_packet.py +102 -0
  236. package/skills/plan-to-pr/scripts/test_validate_packet.py +256 -0
  237. package/skills/plan-to-pr/scripts/test_validate_protocol.py +135 -0
  238. package/skills/plan-to-pr/scripts/test_validate_run.py +158 -0
  239. package/skills/plan-to-pr/scripts/validate_packet.py +655 -0
  240. package/skills/plan-to-pr/scripts/validate_protocol.py +622 -0
  241. package/skills/plan-to-pr/scripts/validate_run.py +173 -0
  242. package/skills/plan-to-pr/test_skill_contract.py +207 -0
  243. package/skills/plan-to-pr/test_task_ticket_contract.py +151 -0
  244. package/skills/pr-converge/SKILL.md +472 -469
  245. package/skills/pr-converge/reference/examples.md +3 -3
  246. package/skills/pr-converge/reference/fix-protocol.md +1 -1
  247. package/skills/pr-converge/reference/ground-rules.md +7 -4
  248. package/skills/pr-converge/reference/multi-pr-orchestration.md +4 -1
  249. package/skills/pr-converge/reference/per-tick.md +5 -5
  250. package/skills/pr-converge/reference/progress-checklist.md +1 -1
  251. package/skills/pr-converge/scripts/check_convergence_gates.py +279 -279
  252. package/skills/pr-converge/scripts/test_check_convergence_codex.py +507 -507
  253. package/skills/pr-converge/scripts/test_check_convergence_gates.py +84 -84
  254. package/skills/pr-converge/test_step5_host_branch.py +1 -1
  255. package/skills/pr-fix-protocol/SKILL.md +1 -1
  256. package/skills/privacy-hygiene/SKILL.md +68 -68
  257. package/skills/prototype/SKILL.md +86 -0
  258. package/skills/prototype/reference/honest-limitations.md +23 -0
  259. package/skills/prototype/reference/promotion-tasks.md +23 -0
  260. package/skills/prototype/scripts/build_sandbox_settings.py +249 -0
  261. package/skills/prototype/scripts/conftest.py +15 -0
  262. package/skills/prototype/scripts/launch_sandbox.py +205 -0
  263. package/skills/prototype/scripts/probe_sandbox_safety.py +311 -0
  264. package/skills/prototype/scripts/prototype_scripts_constants/__init__.py +1 -0
  265. package/skills/prototype/scripts/prototype_scripts_constants/config/__init__.py +0 -0
  266. package/skills/prototype/scripts/prototype_scripts_constants/config/build_sandbox_settings_constants.py +41 -0
  267. package/skills/prototype/scripts/prototype_scripts_constants/config/launch_sandbox_constants.py +23 -0
  268. package/skills/prototype/scripts/prototype_scripts_constants/config/probe_sandbox_safety_constants.py +45 -0
  269. package/skills/prototype/scripts/prototype_scripts_constants/config/prototype_common_constants.py +10 -0
  270. package/skills/prototype/scripts/test_build_sandbox_settings.py +275 -0
  271. package/skills/prototype/scripts/test_launch_sandbox.py +303 -0
  272. package/skills/prototype/scripts/test_probe_sandbox_safety.py +284 -0
  273. package/skills/prototype/workflows/promotion.md +27 -0
  274. package/skills/prototype/workflows/sandbox.md +35 -0
  275. package/skills/release-notes-html/SKILL.md +164 -0
  276. package/skills/skill-builder/CLAUDE.md +3 -3
  277. package/skills/skill-builder/SKILL.md +5 -5
  278. package/skills/skill-builder/references/CLAUDE.md +1 -1
  279. package/skills/skill-builder/references/delegation-map.md +3 -3
  280. package/skills/skill-builder/references/description-field.md +1 -1
  281. package/skills/skill-builder/references/skill-modularity.md +2 -3
  282. package/skills/skill-builder/workflows/CLAUDE.md +1 -1
  283. package/skills/skill-builder/workflows/improve-skill.md +1 -1
  284. package/skills/skill-builder/workflows/new-skill.md +2 -2
  285. package/skills/task-build/CLAUDE.md +8 -7
  286. package/skills/task-build/SKILL.md +16 -8
  287. package/skills/task-build/reference/tool-routing.md +19 -0
  288. package/skills/team-advisor/SKILL.md +2 -2
  289. package/scripts/test_invoke_code_review.py +0 -672
  290. package/skills/closeout/reference/issue-body-templates.md +0 -108
@@ -0,0 +1,256 @@
1
+ """Argument contract, encoding, clean-stamp rules, effort tokens, and stamps."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ import pytest
8
+
9
+ import invoke_code_review as invoker
10
+ from _code_review_test_support import (
11
+ DriftingReview,
12
+ EFFORT_LOW,
13
+ FIXTURE_CHAIN_RETURNCODE,
14
+ FIXTURE_FAILED_RETURNCODE,
15
+ FIXTURE_SERVED_COMMAND,
16
+ FIXTURE_SESSION_OPUS,
17
+ HOST_PROFILE_THIRD_PARTY,
18
+ MISSING_STORE_FILE_NAME,
19
+ REJECTED_ULTRA_EFFORT,
20
+ SINGLE_PASS_CAP,
21
+ claude_failed,
22
+ init_git_repository,
23
+ install_seams,
24
+ prepared_surface_repo,
25
+ run_review,
26
+ stable_clean_review,
27
+ surface_changing_review,
28
+ )
29
+ from dev_env_scripts_constants.code_review_constants import (
30
+ CODE_REVIEW_MODEL_ALIAS,
31
+ DEFAULT_CODE_REVIEW_EFFORT,
32
+ IN_SESSION_RETURNCODE,
33
+ MAXIMUM_STAMP_MINT_PASSES,
34
+ MODE_CHAIN,
35
+ MODE_IN_SESSION,
36
+ PERMISSION_MODE_BYPASS,
37
+ PERMISSION_MODE_FLAG,
38
+ RESULT_KEY_BOUND_HASH,
39
+ RESULT_KEY_DIRTY_TREE,
40
+ RESULT_KEY_MODE,
41
+ RESULT_KEY_PASS_COUNT,
42
+ RESULT_KEY_RETURNCODE,
43
+ RESULT_KEY_SERVED_COMMAND,
44
+ RESULT_KEY_STAMP_MINTED,
45
+ )
46
+ from dev_env_scripts_constants.grok_worker_constants import (
47
+ MODEL_FLAG,
48
+ OUTPUT_FORMAT_FLAG,
49
+ OUTPUT_FORMAT_JSON,
50
+ SINGLE_TURN_FLAG,
51
+ )
52
+ from dev_env_scripts_constants.timing import DEFAULT_CODE_REVIEW_TIMEOUT_SECONDS
53
+
54
+ CLEAN_SUCCESS_OUTCOME = invoker.CodeReviewOutcome(
55
+ mode=MODE_CHAIN,
56
+ served_command=FIXTURE_SERVED_COMMAND,
57
+ returncode=FIXTURE_CHAIN_RETURNCODE,
58
+ is_dirty_tree=False,
59
+ )
60
+ DIRTY_SUCCESS_OUTCOME = invoker.CodeReviewOutcome(
61
+ mode=MODE_CHAIN,
62
+ served_command=FIXTURE_SERVED_COMMAND,
63
+ returncode=FIXTURE_CHAIN_RETURNCODE,
64
+ is_dirty_tree=True,
65
+ )
66
+ FAILED_SERVE_OUTCOME = invoker.CodeReviewOutcome(
67
+ mode=MODE_CHAIN,
68
+ served_command=None,
69
+ returncode=FIXTURE_FAILED_RETURNCODE,
70
+ is_dirty_tree=False,
71
+ )
72
+ IN_SESSION_READY_OUTCOME = invoker.CodeReviewOutcome(
73
+ mode=MODE_IN_SESSION,
74
+ served_command=None,
75
+ returncode=IN_SESSION_RETURNCODE,
76
+ is_dirty_tree=False,
77
+ )
78
+
79
+
80
+ def test_build_code_review_arguments_matches_contract() -> None:
81
+ all_arguments = invoker.build_code_review_arguments()
82
+ assert all_arguments == [
83
+ SINGLE_TURN_FLAG,
84
+ invoker.build_code_review_prompt(DEFAULT_CODE_REVIEW_EFFORT),
85
+ MODEL_FLAG,
86
+ CODE_REVIEW_MODEL_ALIAS,
87
+ OUTPUT_FORMAT_FLAG,
88
+ OUTPUT_FORMAT_JSON,
89
+ PERMISSION_MODE_FLAG,
90
+ PERMISSION_MODE_BYPASS,
91
+ ]
92
+
93
+
94
+ def test_chain_failure_preserves_returncode(
95
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
96
+ ) -> None:
97
+ working_directory = init_git_repository(tmp_path / "repo")
98
+ install_seams(
99
+ monkeypatch,
100
+ host_profile=HOST_PROFILE_THIRD_PARTY,
101
+ claude_outcome=claude_failed(),
102
+ working_directory=working_directory,
103
+ )
104
+
105
+ review_outcome = run_review(working_directory, session_model=FIXTURE_SESSION_OPUS)
106
+
107
+ assert review_outcome.mode == MODE_CHAIN
108
+ assert review_outcome.served_command is None
109
+ assert review_outcome.returncode == FIXTURE_FAILED_RETURNCODE
110
+ assert review_outcome.is_dirty_tree is False
111
+ assert invoker.is_successful_code_review(review_outcome) is False
112
+ assert invoker.is_code_review_clean_stamp_allowed(review_outcome) is False
113
+
114
+
115
+ def test_clean_stamp_allowed_only_on_successful_clean_serve() -> None:
116
+ assert invoker.is_code_review_clean_stamp_allowed(CLEAN_SUCCESS_OUTCOME) is True
117
+ assert invoker.is_code_review_clean_stamp_allowed(DIRTY_SUCCESS_OUTCOME) is False
118
+ assert invoker.is_code_review_clean_stamp_allowed(FAILED_SERVE_OUTCOME) is False
119
+ assert invoker.is_code_review_clean_stamp_allowed(IN_SESSION_READY_OUTCOME) is True
120
+ assert invoker.is_successful_code_review(FAILED_SERVE_OUTCOME) is False
121
+ assert invoker.is_successful_code_review(CLEAN_SUCCESS_OUTCOME) is True
122
+
123
+
124
+ def test_encode_code_review_outcome_shape() -> None:
125
+ review_outcome = invoker.CodeReviewOutcome(
126
+ mode=MODE_CHAIN,
127
+ served_command=FIXTURE_SERVED_COMMAND,
128
+ returncode=FIXTURE_CHAIN_RETURNCODE,
129
+ is_dirty_tree=True,
130
+ )
131
+ encoded_payload = invoker.encode_code_review_outcome(review_outcome)
132
+ assert encoded_payload == {
133
+ RESULT_KEY_MODE: MODE_CHAIN,
134
+ RESULT_KEY_SERVED_COMMAND: FIXTURE_SERVED_COMMAND,
135
+ RESULT_KEY_RETURNCODE: FIXTURE_CHAIN_RETURNCODE,
136
+ RESULT_KEY_DIRTY_TREE: True,
137
+ }
138
+
139
+
140
+ @pytest.mark.parametrize("valid_effort", ["low", "medium", "high", "xhigh", "max"])
141
+ def test_validate_effort_token_accepts_known_tokens(valid_effort: str) -> None:
142
+ assert invoker.validate_effort_token(valid_effort) is None
143
+
144
+
145
+ def test_validate_effort_token_rejects_ultra_loudly() -> None:
146
+ error_message = invoker.validate_effort_token(REJECTED_ULTRA_EFFORT)
147
+ assert error_message is not None
148
+ assert REJECTED_ULTRA_EFFORT in error_message
149
+
150
+
151
+ def test_validate_effort_token_rejects_unknown_token() -> None:
152
+ error_message = invoker.validate_effort_token("bogus")
153
+ assert error_message is not None
154
+ assert "bogus" in error_message
155
+
156
+
157
+ def test_build_code_review_prompt_reads_as_slash_command() -> None:
158
+ assert invoker.build_code_review_prompt(EFFORT_LOW) == "/code-review low --fix"
159
+ assert invoker.build_code_review_prompt("xhigh") == "/code-review xhigh --fix"
160
+
161
+
162
+ def test_encode_stamp_mint_outcome_includes_mint_metadata() -> None:
163
+ review_outcome = invoker.CodeReviewOutcome(
164
+ mode=MODE_CHAIN,
165
+ served_command=FIXTURE_SERVED_COMMAND,
166
+ returncode=FIXTURE_CHAIN_RETURNCODE,
167
+ is_dirty_tree=False,
168
+ )
169
+ mint_outcome = invoker.StampMintOutcome(
170
+ review_outcome=review_outcome,
171
+ is_stamp_minted=True,
172
+ pass_count=SINGLE_PASS_CAP,
173
+ bound_hash="abc123",
174
+ )
175
+ encoded_payload = invoker.encode_stamp_mint_outcome(mint_outcome)
176
+ assert encoded_payload[RESULT_KEY_STAMP_MINTED] is True
177
+ assert encoded_payload[RESULT_KEY_PASS_COUNT] == SINGLE_PASS_CAP
178
+ assert encoded_payload[RESULT_KEY_BOUND_HASH] == "abc123"
179
+
180
+
181
+ def test_record_stamp_mints_on_surface_stable_clean_pass(
182
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
183
+ ) -> None:
184
+ working_directory = prepared_surface_repo(monkeypatch, tmp_path)
185
+ monkeypatch.setattr(invoker, "invoke_code_review", stable_clean_review)
186
+ mint_outcome = invoker.invoke_code_review_and_record_stamp(
187
+ working_directory=working_directory,
188
+ session_model=CODE_REVIEW_MODEL_ALIAS,
189
+ timeout_seconds=DEFAULT_CODE_REVIEW_TIMEOUT_SECONDS,
190
+ effort=EFFORT_LOW,
191
+ )
192
+ assert mint_outcome.is_stamp_minted is True
193
+ assert mint_outcome.bound_hash is not None
194
+ store_module = invoker.load_code_review_stamp_store()
195
+ assert store_module.stamp_covers_surface(
196
+ str(working_directory), mint_outcome.bound_hash, EFFORT_LOW
197
+ )
198
+
199
+
200
+ def test_record_stamp_does_not_mint_when_review_changes_surface(
201
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
202
+ ) -> None:
203
+ working_directory = prepared_surface_repo(monkeypatch, tmp_path)
204
+ monkeypatch.setattr(invoker, "invoke_code_review", surface_changing_review)
205
+ mint_outcome = invoker.invoke_code_review_and_record_stamp(
206
+ working_directory=working_directory,
207
+ session_model=CODE_REVIEW_MODEL_ALIAS,
208
+ timeout_seconds=DEFAULT_CODE_REVIEW_TIMEOUT_SECONDS,
209
+ effort=EFFORT_LOW,
210
+ maximum_passes=SINGLE_PASS_CAP,
211
+ )
212
+ assert mint_outcome.is_stamp_minted is False
213
+ assert mint_outcome.pass_count == SINGLE_PASS_CAP
214
+ assert mint_outcome.bound_hash is None
215
+
216
+
217
+ def test_record_stamp_hits_cap_without_minting(
218
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
219
+ ) -> None:
220
+ working_directory = prepared_surface_repo(monkeypatch, tmp_path)
221
+ monkeypatch.setattr(invoker, "invoke_code_review", DriftingReview())
222
+ mint_outcome = invoker.invoke_code_review_and_record_stamp(
223
+ working_directory=working_directory,
224
+ session_model=CODE_REVIEW_MODEL_ALIAS,
225
+ timeout_seconds=DEFAULT_CODE_REVIEW_TIMEOUT_SECONDS,
226
+ effort=EFFORT_LOW,
227
+ maximum_passes=MAXIMUM_STAMP_MINT_PASSES,
228
+ )
229
+ assert mint_outcome.is_stamp_minted is False
230
+ assert mint_outcome.pass_count == MAXIMUM_STAMP_MINT_PASSES
231
+
232
+
233
+ def test_load_code_review_stamp_store_records_and_covers_surface(
234
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
235
+ ) -> None:
236
+ working_directory = prepared_surface_repo(monkeypatch, tmp_path)
237
+ store_module = invoker.load_code_review_stamp_store()
238
+ surface_hash = store_module.live_surface_hash(str(working_directory))
239
+ assert surface_hash is not None
240
+ stamp_path = store_module.record_clean_stamp(
241
+ str(working_directory), surface_hash, EFFORT_LOW
242
+ )
243
+ assert stamp_path.exists()
244
+ assert store_module.stamp_covers_surface(
245
+ str(working_directory), surface_hash, EFFORT_LOW
246
+ )
247
+
248
+
249
+ def test_load_code_review_stamp_store_raises_when_file_absent(
250
+ monkeypatch: pytest.MonkeyPatch,
251
+ ) -> None:
252
+ monkeypatch.setattr(
253
+ invoker, "STAMP_STORE_MODULE_FILE_NAME", MISSING_STORE_FILE_NAME
254
+ )
255
+ with pytest.raises(ModuleNotFoundError):
256
+ invoker.load_code_review_stamp_store()
@@ -0,0 +1,123 @@
1
+ """Working-tree dirty detection and the git status porcelain command."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import subprocess
6
+ from collections.abc import Sequence
7
+ from pathlib import Path
8
+
9
+ import pytest
10
+
11
+ import invoke_code_review as invoker
12
+ from _code_review_test_support import (
13
+ DIRTY_FILE_CONTENTS,
14
+ DIRTY_FILE_NAME,
15
+ FIXTURE_CHAIN_RETURNCODE,
16
+ FIXTURE_GIT_STATUS_FAILURE_RETURNCODE,
17
+ FIXTURE_SERVED_COMMAND,
18
+ FIXTURE_SESSION_OPUS,
19
+ HOST_PROFILE_THIRD_PARTY,
20
+ claude_served,
21
+ init_git_repository,
22
+ install_seams,
23
+ run_review,
24
+ )
25
+ from dev_env_scripts_constants.code_review_constants import (
26
+ GIT_BINARY,
27
+ GIT_PORCELAIN_FLAG,
28
+ GIT_STATUS_SUBCOMMAND,
29
+ MODE_CHAIN,
30
+ )
31
+
32
+
33
+ def test_dirty_tree_true_after_chain_writes_file(
34
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
35
+ ) -> None:
36
+ working_directory = init_git_repository(tmp_path / "repo")
37
+ install_seams(
38
+ monkeypatch,
39
+ host_profile=HOST_PROFILE_THIRD_PARTY,
40
+ claude_outcome=claude_served(),
41
+ should_dirty_tree_on_chain=True,
42
+ working_directory=working_directory,
43
+ )
44
+
45
+ review_outcome = run_review(working_directory, session_model=FIXTURE_SESSION_OPUS)
46
+
47
+ assert review_outcome.mode == MODE_CHAIN
48
+ assert review_outcome.is_dirty_tree is True
49
+ assert review_outcome.served_command == FIXTURE_SERVED_COMMAND
50
+ assert review_outcome.returncode == FIXTURE_CHAIN_RETURNCODE
51
+
52
+
53
+ def test_dirty_tree_false_when_chain_leaves_tree_clean(
54
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
55
+ ) -> None:
56
+ working_directory = init_git_repository(tmp_path / "repo")
57
+ install_seams(
58
+ monkeypatch,
59
+ host_profile=HOST_PROFILE_THIRD_PARTY,
60
+ claude_outcome=claude_served(),
61
+ should_dirty_tree_on_chain=False,
62
+ working_directory=working_directory,
63
+ )
64
+
65
+ review_outcome = run_review(working_directory, session_model=FIXTURE_SESSION_OPUS)
66
+
67
+ assert review_outcome.is_dirty_tree is False
68
+
69
+
70
+ def test_is_working_tree_dirty_against_real_git_repo(tmp_path: Path) -> None:
71
+ working_directory = init_git_repository(tmp_path / "repo")
72
+ assert invoker.is_working_tree_dirty(working_directory) is False
73
+ (working_directory / DIRTY_FILE_NAME).write_text(
74
+ DIRTY_FILE_CONTENTS, encoding="utf-8"
75
+ )
76
+ assert invoker.is_working_tree_dirty(working_directory) is True
77
+
78
+
79
+ def test_git_status_command_uses_porcelain(
80
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
81
+ ) -> None:
82
+ working_directory = init_git_repository(tmp_path / "repo")
83
+ all_commands: list[list[str]] = []
84
+
85
+ def fake_git_status(
86
+ all_command_tokens: Sequence[str],
87
+ *all_positionals: object,
88
+ **all_keywords: object,
89
+ ) -> subprocess.CompletedProcess[str]:
90
+ del all_positionals, all_keywords
91
+ all_commands.append(list(all_command_tokens))
92
+ return subprocess.CompletedProcess(
93
+ args=list(all_command_tokens),
94
+ returncode=0,
95
+ stdout="",
96
+ stderr="",
97
+ )
98
+
99
+ monkeypatch.setattr(invoker, "review_git_status_runner", fake_git_status)
100
+ invoker.is_working_tree_dirty(working_directory)
101
+ assert all_commands == [[GIT_BINARY, GIT_STATUS_SUBCOMMAND, GIT_PORCELAIN_FLAG]]
102
+
103
+
104
+ def test_is_working_tree_dirty_nonzero_returncode_is_not_clean(
105
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
106
+ ) -> None:
107
+ working_directory = init_git_repository(tmp_path / "repo")
108
+
109
+ def fake_git_status(
110
+ all_command_tokens: Sequence[str],
111
+ *all_positionals: object,
112
+ **all_keywords: object,
113
+ ) -> subprocess.CompletedProcess[str]:
114
+ del all_positionals, all_keywords
115
+ return subprocess.CompletedProcess(
116
+ args=list(all_command_tokens),
117
+ returncode=FIXTURE_GIT_STATUS_FAILURE_RETURNCODE,
118
+ stdout="",
119
+ stderr="fatal: not a git repository",
120
+ )
121
+
122
+ monkeypatch.setattr(invoker, "review_git_status_runner", fake_git_status)
123
+ assert invoker.is_working_tree_dirty(working_directory) is True
@@ -0,0 +1,99 @@
1
+ """Mode-decision behavior of the host-aware ``/code-review`` invoker."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ import pytest
8
+
9
+ import invoke_code_review as invoker
10
+ from _code_review_test_support import (
11
+ FIXTURE_SESSION_HAIKU,
12
+ FIXTURE_SESSION_OPUS,
13
+ FIXTURE_SESSION_OPUS_UPPER,
14
+ FIXTURE_SESSION_SONNET,
15
+ HOST_PROFILE_CLAUDE,
16
+ HOST_PROFILE_THIRD_PARTY,
17
+ claude_served,
18
+ init_git_repository,
19
+ install_seams,
20
+ run_review,
21
+ )
22
+ from dev_env_scripts_constants.code_review_constants import (
23
+ IN_SESSION_RETURNCODE,
24
+ MODE_CHAIN,
25
+ MODE_IN_SESSION,
26
+ )
27
+
28
+
29
+ @pytest.mark.parametrize(
30
+ ("host_profile", "session_model", "expected_mode"),
31
+ [
32
+ (HOST_PROFILE_CLAUDE, FIXTURE_SESSION_OPUS, MODE_IN_SESSION),
33
+ (HOST_PROFILE_CLAUDE, FIXTURE_SESSION_OPUS_UPPER, MODE_IN_SESSION),
34
+ (HOST_PROFILE_CLAUDE, FIXTURE_SESSION_SONNET, MODE_CHAIN),
35
+ (HOST_PROFILE_CLAUDE, FIXTURE_SESSION_HAIKU, MODE_CHAIN),
36
+ (HOST_PROFILE_THIRD_PARTY, FIXTURE_SESSION_OPUS, MODE_CHAIN),
37
+ (HOST_PROFILE_THIRD_PARTY, FIXTURE_SESSION_SONNET, MODE_CHAIN),
38
+ ],
39
+ )
40
+ def test_mode_decision_host_and_model_matrix(
41
+ monkeypatch: pytest.MonkeyPatch,
42
+ tmp_path: Path,
43
+ host_profile: str,
44
+ session_model: str,
45
+ expected_mode: str,
46
+ ) -> None:
47
+ working_directory = init_git_repository(tmp_path / "repo")
48
+ claude_outcome = claude_served() if expected_mode == MODE_CHAIN else None
49
+ call_log = install_seams(
50
+ monkeypatch,
51
+ host_profile=host_profile,
52
+ claude_outcome=claude_outcome,
53
+ working_directory=working_directory,
54
+ )
55
+
56
+ review_outcome = run_review(working_directory, session_model=session_model)
57
+
58
+ assert review_outcome.mode == expected_mode
59
+ assert call_log.host_profile_calls == 1
60
+ if expected_mode == MODE_IN_SESSION:
61
+ assert call_log.claude_calls == 0
62
+ assert review_outcome.served_command is None
63
+ assert review_outcome.returncode == IN_SESSION_RETURNCODE
64
+ assert review_outcome.is_dirty_tree is False
65
+ else:
66
+ assert call_log.claude_calls == 1
67
+
68
+
69
+ @pytest.mark.parametrize(
70
+ ("session_model", "expected_is_opus"),
71
+ [
72
+ (FIXTURE_SESSION_OPUS, True),
73
+ (FIXTURE_SESSION_OPUS_UPPER, True),
74
+ (FIXTURE_SESSION_SONNET, False),
75
+ (" opus ", True),
76
+ ],
77
+ )
78
+ def test_is_opus_session_model(session_model: str, expected_is_opus: bool) -> None:
79
+ assert invoker.is_opus_session_model(session_model) is expected_is_opus
80
+
81
+
82
+ @pytest.mark.parametrize(
83
+ ("host_profile", "session_model", "expected_mode"),
84
+ [
85
+ (HOST_PROFILE_CLAUDE, FIXTURE_SESSION_OPUS, MODE_IN_SESSION),
86
+ (HOST_PROFILE_CLAUDE, FIXTURE_SESSION_SONNET, MODE_CHAIN),
87
+ (HOST_PROFILE_THIRD_PARTY, FIXTURE_SESSION_OPUS, MODE_CHAIN),
88
+ ],
89
+ )
90
+ def test_decide_review_mode(
91
+ host_profile: str, session_model: str, expected_mode: str
92
+ ) -> None:
93
+ assert (
94
+ invoker.decide_review_mode(
95
+ host_profile=host_profile,
96
+ session_model=session_model,
97
+ )
98
+ == expected_mode
99
+ )