claude-dev-env 2.4.0 → 2.7.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 (309) hide show
  1. package/CLAUDE.md +26 -59
  2. package/_shared/pr-loop/scripts/_claude_permissions_common.py +84 -0
  3. package/_shared/pr-loop/scripts/code_rules_gate.py +6 -3
  4. package/_shared/pr-loop/scripts/code_rules_gate_parts/CLAUDE.md +12 -2
  5. package/_shared/pr-loop/scripts/code_rules_gate_parts/baseline_import_isolation.py +309 -0
  6. package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_regression.py +540 -0
  7. package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_running.py +206 -70
  8. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/__init__.py +1 -0
  9. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/_repo_test_helpers.py +76 -0
  10. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_baseline_import_isolation.py +248 -0
  11. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_staged_test_regression.py +309 -0
  12. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_staged_test_running.py +91 -58
  13. package/_shared/pr-loop/scripts/grant_project_claude_permissions.py +306 -306
  14. package/_shared/pr-loop/scripts/pr_loop_shared_constants/claude_permissions_constants.py +44 -0
  15. package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +202 -0
  16. package/_shared/pr-loop/scripts/pr_loop_shared_constants/copilot_quota_constants.py +24 -24
  17. package/_shared/pr-loop/scripts/pr_loop_shared_constants/stale_worktree_rule_sweep_constants.py +107 -107
  18. package/_shared/pr-loop/scripts/revoke_project_claude_permissions.py +290 -48
  19. package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +42 -2
  20. package/_shared/pr-loop/scripts/tests/test_claude_permissions_constants.py +36 -0
  21. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +100 -1
  22. package/_shared/pr-loop/scripts/tests/test_fix_hookspath.py +497 -497
  23. package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +311 -2
  24. package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep.py +301 -301
  25. package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep_constants.py +85 -85
  26. package/_shared/pr-loop/worker-spawn.md +1 -1
  27. package/agents/CLAUDE.md +3 -2
  28. package/agents/caveman.md +0 -1
  29. package/agents/clasp-deployment-orchestrator.md +0 -1
  30. package/agents/clean-coder.md +0 -1
  31. package/agents/code-advisor.md +0 -1
  32. package/agents/code-quality-agent.md +1 -2
  33. package/agents/code-verifier.md +36 -8
  34. package/agents/deep-research.md +0 -1
  35. package/agents/docs-agent.md +0 -1
  36. package/agents/git-commit-crafter.md +0 -1
  37. package/agents/issue-tracker.md +42 -0
  38. package/agents/plan-packet-validator.md +0 -1
  39. package/agents/pr-description-writer.md +0 -1
  40. package/agents/test_agent_frontmatter.py +67 -18
  41. package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +143 -141
  42. package/bin/CLAUDE.md +68 -5
  43. package/bin/codex-compat.mjs +104 -0
  44. package/bin/codex-compat.test.mjs +51 -0
  45. package/bin/ever-shipped-skills.mjs +1 -0
  46. package/bin/install-constants.mjs +88 -0
  47. package/bin/install.mjs +1138 -114
  48. package/bin/install.prune.test.mjs +869 -19
  49. package/bin/install.test.mjs +906 -2
  50. package/codex-capability-map.json +13 -0
  51. package/commands/implement.md +1 -1
  52. package/commands/right-size.md +1 -1
  53. package/docs/CLAUDE.md +1 -0
  54. package/docs/CODE_RULES.md +2 -0
  55. package/docs/codex-compatibility.md +25 -0
  56. package/docs/host-pool-health-monitor.md +102 -0
  57. package/docs/nas-ssh-invocation.md +96 -12
  58. package/docs/references/CLAUDE.md +4 -2
  59. package/docs/references/advisor-tool.md +13 -0
  60. package/docs/references/code-review-enforcement.md +35 -0
  61. package/docs/references/team-advisor-skill.md +14 -0
  62. package/hooks/blocking/CLAUDE.md +4 -0
  63. package/hooks/blocking/code_review_pr_create_gate.py +7 -3
  64. package/hooks/blocking/code_review_push_gate.py +9 -4
  65. package/hooks/blocking/code_review_stamp_directory_write_blocker.py +8 -0
  66. package/hooks/blocking/config/__init__.py +5 -5
  67. package/hooks/blocking/config/code_review_enforcement_constants.py +40 -7
  68. package/hooks/blocking/config/test_code_review_enforcement_constants.py +58 -0
  69. package/hooks/blocking/config/verified_commit_constants.py +160 -159
  70. package/hooks/blocking/eli11_reply_enforcer.py +479 -0
  71. package/hooks/blocking/gh_body_arg_blocker.py +1 -1
  72. package/hooks/blocking/nas_ssh_binary_enforcer.py +8 -46
  73. package/hooks/blocking/orchestrator_refresh_reschedule_gate.py +256 -0
  74. package/hooks/blocking/pre_tool_use_dispatcher.py +24 -24
  75. package/hooks/blocking/shell_substitution_blocker.py +129 -0
  76. package/hooks/blocking/state_description_blocker.py +1 -1
  77. package/hooks/blocking/stop_dispatcher.py +1 -1
  78. package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +2 -3
  79. package/hooks/blocking/test_code_review_pr_create_gate.py +14 -0
  80. package/hooks/blocking/test_code_review_push_gate.py +16 -0
  81. package/hooks/blocking/test_code_review_stamp_directory_write_blocker.py +19 -0
  82. package/hooks/blocking/test_eli11_reply_enforcer.py +457 -0
  83. package/hooks/blocking/test_orchestrator_refresh_reschedule_gate.py +231 -0
  84. package/hooks/blocking/test_pre_tool_use_dispatcher.py +10 -1
  85. package/hooks/blocking/test_shell_substitution_blocker.py +124 -0
  86. package/hooks/blocking/test_stop_dispatcher.py +23 -0
  87. package/hooks/blocking/test_unscoped_search_blocker.py +102 -0
  88. package/hooks/blocking/test_verdict_directory_write_blocker.py +804 -808
  89. package/hooks/blocking/test_verification_verdict_store.py +54 -0
  90. package/hooks/blocking/test_verified_commit_gate.py +581 -581
  91. package/hooks/blocking/test_verified_commit_message_accuracy_blocker.py +131 -131
  92. package/hooks/blocking/unscoped_search_blocker.py +391 -0
  93. package/hooks/blocking/verdict_directory_write_blocker.py +687 -687
  94. package/hooks/blocking/verification_verdict_store.py +1039 -1036
  95. package/hooks/blocking/verified_commit_message_accuracy_blocker.py +167 -167
  96. package/hooks/blocking/verifier_verdict_minter.py +280 -280
  97. package/hooks/git-hooks/CLAUDE.md +3 -0
  98. package/hooks/git-hooks/conftest.py +30 -0
  99. package/hooks/git-hooks/gate_utils.py +2 -2
  100. package/hooks/git-hooks/git_hooks_constants/__init__.py +41 -2
  101. package/hooks/git-hooks/pre_push.py +75 -4
  102. package/hooks/git-hooks/pre_push_base_reference.py +166 -0
  103. package/hooks/git-hooks/test_config.py +0 -15
  104. package/hooks/git-hooks/test_gate_utils.py +3 -15
  105. package/hooks/git-hooks/test_pre_commit.py +1 -15
  106. package/hooks/git-hooks/test_pre_push.py +257 -23
  107. package/hooks/git-hooks/test_pre_push_base_reference.py +339 -0
  108. package/hooks/hooks.json +10 -12
  109. package/hooks/hooks_constants/CLAUDE.md +7 -2
  110. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +4 -4
  111. package/hooks/hooks_constants/eli11_reply_enforcer_constants.py +101 -0
  112. package/hooks/hooks_constants/enter_worktree_prefetch_constants.py +18 -18
  113. package/hooks/hooks_constants/nas_ssh_binary_enforcer_constants.py +2 -8
  114. package/hooks/hooks_constants/orchestrator_refresh_reschedule_gate_constants.py +48 -0
  115. package/hooks/hooks_constants/ruff_integration_constants.py +16 -0
  116. package/hooks/hooks_constants/shell_command_segments.py +82 -0
  117. package/hooks/hooks_constants/shell_substitution_blocker_constants.py +67 -0
  118. package/hooks/hooks_constants/stop_dispatcher_constants.py +1 -0
  119. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +5 -6
  120. package/hooks/hooks_constants/test_stop_dispatcher_constants.py +1 -0
  121. package/hooks/hooks_constants/unscoped_search_blocker_constants.py +153 -0
  122. package/hooks/lifecycle/enter_worktree_origin_prefetch.py +163 -146
  123. package/hooks/lifecycle/test_enter_worktree_origin_prefetch.py +185 -178
  124. package/hooks/pyproject.toml +1 -0
  125. package/hooks/validators/CLAUDE.md +1 -0
  126. package/hooks/validators/config/__init__.py +0 -0
  127. package/hooks/validators/config/directory_exemption_constants.py +183 -0
  128. package/hooks/validators/config/test_directory_exemption_constants.py +21 -0
  129. package/hooks/validators/conftest.py +4 -0
  130. package/hooks/validators/ruff_integration.py +49 -5
  131. package/hooks/validators/run_all_validators.py +206 -9
  132. package/hooks/validators/test_directory_exemption_constants.py +185 -0
  133. package/hooks/validators/test_python_antipattern_checks.py +110 -5
  134. package/hooks/validators/test_ruff_integration.py +92 -1
  135. package/hooks/validators/test_run_all_validators.py +115 -68
  136. package/hooks/validators/test_run_all_validators_pretooluse.py +159 -1
  137. package/package.json +13 -3
  138. package/rules/CLAUDE.md +17 -22
  139. package/rules/agent-spawn-protocol.md +6 -6
  140. package/rules/anti-corollary-tests.md +1 -1
  141. package/rules/bdd.md +1 -1
  142. package/rules/cleanup-temp-files.md +10 -4
  143. package/rules/code-standards.md +7 -0
  144. package/rules/conservative-action.md +1 -5
  145. package/rules/context7.md +0 -4
  146. package/rules/destructive-commands.md +47 -0
  147. package/rules/doc-inventory-integrity.md +48 -0
  148. package/rules/doc-prose-cuts.md +58 -0
  149. package/rules/docstring-prose-matches-implementation.md +53 -44
  150. package/rules/durable-post-artifacts.md +0 -4
  151. package/rules/eli11-replies.md +31 -0
  152. package/rules/explore-thoroughly.md +4 -4
  153. package/rules/falsify-before-green.md +68 -0
  154. package/rules/file-global-constants.md +1 -1
  155. package/rules/filesystem-search.md +51 -0
  156. package/rules/gh-cli-conventions.md +27 -0
  157. package/rules/git-workflow.md +26 -0
  158. package/rules/hedging-claims.md +9 -0
  159. package/rules/long-horizon-autonomy.md +0 -4
  160. package/rules/measurement-denominators.md +48 -0
  161. package/rules/nas-ssh-invocation.md +23 -5
  162. package/rules/parallel-tools.md +2 -2
  163. package/rules/plain-illustrative-docstrings.md +3 -7
  164. package/rules/plain-language.md +2 -0
  165. package/rules/proof-of-work-pr-comments.md +0 -4
  166. package/rules/re-stage-before-commit.md +2 -0
  167. package/rules/research-mode.md +10 -0
  168. package/rules/shell-invocation.md +21 -0
  169. package/rules/testing.md +4 -0
  170. package/rules/verified-commit-gate-skip.md +3 -27
  171. package/rules/verify-before-asking.md +5 -0
  172. package/rules/windows-filesystem-safe.md +1 -1
  173. package/rules/workers-done-before-complete.md +4 -0
  174. package/scripts/CLAUDE.md +1 -0
  175. package/scripts/Capture-PoolHealth.ps1 +410 -0
  176. package/scripts/Migrate-ShellPolicy.ps1 +1 -1
  177. package/scripts/_code_review_test_support.py +404 -0
  178. package/scripts/claude_chain_runner.py +141 -1
  179. package/scripts/codex_capability_bridge.py +171 -0
  180. package/scripts/codex_compat_materializer.py +1087 -0
  181. package/scripts/codex_compat_watcher.py +502 -0
  182. package/scripts/conftest.py +16 -1
  183. package/scripts/dev_env_scripts_constants/CLAUDE.md +1 -1
  184. package/scripts/dev_env_scripts_constants/claude_chain_constants.py +9 -0
  185. package/scripts/dev_env_scripts_constants/code_review_constants.py +37 -0
  186. package/scripts/invoke_code_review.py +11 -4
  187. package/scripts/resolve_worker_spawn.py +626 -626
  188. package/scripts/spawn_grok_batch.py +672 -672
  189. package/scripts/sync_to_cursor/rules.py +0 -10
  190. package/scripts/test_claude_chain_runner.py +131 -0
  191. package/scripts/test_invoke_code_review.py +85 -908
  192. package/scripts/test_invoke_code_review_chain.py +70 -0
  193. package/scripts/test_invoke_code_review_cli.py +192 -0
  194. package/scripts/test_invoke_code_review_contract.py +256 -0
  195. package/scripts/test_invoke_code_review_git.py +123 -0
  196. package/scripts/test_invoke_code_review_mode.py +99 -0
  197. package/scripts/test_resolve_worker_spawn.py +1014 -1014
  198. package/scripts/tests/test_code_review_constants.py +80 -0
  199. package/scripts/tests/test_codex_capability_bridge.py +91 -0
  200. package/scripts/tests/test_codex_compat_materializer.py +632 -0
  201. package/scripts/tests/test_codex_compat_watcher.py +599 -0
  202. package/scripts/tests/test_sync_to_cursor.py +0 -1
  203. package/skills/CLAUDE.md +2 -0
  204. package/skills/auditing-claude-config/SKILL.md +114 -114
  205. package/skills/autoconverge/SKILL.md +427 -427
  206. package/skills/autoconverge/reference/convergence.md +24 -3
  207. package/skills/autoconverge/workflow/CLAUDE.md +1 -0
  208. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +3 -3
  209. package/skills/autoconverge/workflow/converge.contract.test.mjs +1263 -1263
  210. package/skills/autoconverge/workflow/converge.mjs +168 -1
  211. package/skills/autoconverge/workflow/converge.p2-advance.test.mjs +202 -0
  212. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a11d903476b803493.jsonl +2 -2
  213. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a26213978adeef6fb.jsonl +2 -2
  214. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a3def0d15ed9d9110.jsonl +2 -2
  215. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a41f41b1b708ee3b7.jsonl +2 -2
  216. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a758b880abecc3ff7.jsonl +2 -2
  217. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a8897b89656b1bd16.jsonl +2 -2
  218. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-abd463d744a1437bc.jsonl +2 -2
  219. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ad19d027ae8ee1816.jsonl +2 -2
  220. package/skills/autoconverge/workflow/fixtures/wf_run/workflows/wf_881252e6-700.json +265 -265
  221. package/skills/bugteam/reference/copilot-gap-analysis.md +1 -1
  222. package/skills/closeout/SKILL.md +33 -50
  223. package/skills/codex-review/scripts/codex_review_scripts_constants/run_constants.py +8 -0
  224. package/skills/codex-review/scripts/run_codex_review.py +233 -1
  225. package/skills/codex-review/scripts/test_run_codex_review.py +189 -0
  226. package/skills/condensing-instructions/SKILL.md +72 -0
  227. package/skills/copilot-review/SKILL.md +119 -119
  228. package/skills/e-code-review/SKILL.md +52 -0
  229. package/skills/e-code-review/reference/fix.md +54 -0
  230. package/skills/e-code-review/reference/loop.md +43 -0
  231. package/skills/e-code-review/reference/low.md +57 -0
  232. package/skills/e-code-review/reference/medium.md +153 -0
  233. package/skills/e-code-review/reference/xhigh.md +182 -0
  234. package/skills/e-simplify/SKILL.md +97 -0
  235. package/skills/fresh-branch/CLAUDE.md +1 -1
  236. package/skills/fresh-branch/SKILL.md +5 -6
  237. package/skills/fresh-branch/scripts/create_fresh_branch.py +42 -24
  238. package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +1 -3
  239. package/skills/fresh-branch/scripts/test_create_fresh_branch.py +30 -126
  240. package/skills/issue-tracker/SKILL.md +92 -0
  241. package/skills/issue-tracker/reference/epic-and-sub-issue-model.md +55 -0
  242. package/skills/issue-tracker/reference/handoff-schema.md +64 -0
  243. package/skills/issue-tracker/reference/operation-matrix.md +41 -0
  244. package/skills/orchestrator/SKILL.md +177 -22
  245. package/skills/orchestrator/scripts/status_gate.py +625 -0
  246. package/skills/orchestrator/scripts/status_gate_constants/__init__.py +1 -0
  247. package/skills/orchestrator/scripts/status_gate_constants/config/__init__.py +1 -0
  248. package/skills/orchestrator/scripts/status_gate_constants/config/constants.py +47 -0
  249. package/skills/orchestrator/scripts/test_status_gate.py +439 -0
  250. package/skills/orchestrator-refresh/SKILL.md +129 -35
  251. package/skills/plan-to-pr/SKILL.md +155 -0
  252. package/skills/plan-to-pr/reference/final-validation-tasks.md +15 -0
  253. package/skills/plan-to-pr/reference/model-routing.md +36 -0
  254. package/skills/plan-to-pr/reference/packet-contract.md +43 -0
  255. package/skills/plan-to-pr/reference/packet-schema.json +57 -0
  256. package/skills/plan-to-pr/reference/process-inventory.md +22 -0
  257. package/skills/plan-to-pr/reference/review-loop.md +33 -0
  258. package/skills/plan-to-pr/reference/run-record.schema.json +27 -0
  259. package/skills/plan-to-pr/reference/self-audit-tasks.md +15 -0
  260. package/skills/plan-to-pr/reference/task-seeds.md +14 -0
  261. package/skills/plan-to-pr/reference/task-ticket.md +38 -0
  262. package/skills/plan-to-pr/scripts/config/__init__.py +1 -0
  263. package/skills/plan-to-pr/scripts/config/constants.py +193 -0
  264. package/skills/plan-to-pr/scripts/create_packet.py +173 -0
  265. package/skills/plan-to-pr/scripts/test_create_packet.py +102 -0
  266. package/skills/plan-to-pr/scripts/test_validate_packet.py +256 -0
  267. package/skills/plan-to-pr/scripts/test_validate_protocol.py +135 -0
  268. package/skills/plan-to-pr/scripts/test_validate_run.py +158 -0
  269. package/skills/plan-to-pr/scripts/validate_packet.py +655 -0
  270. package/skills/plan-to-pr/scripts/validate_protocol.py +622 -0
  271. package/skills/plan-to-pr/scripts/validate_run.py +173 -0
  272. package/skills/plan-to-pr/test_skill_contract.py +207 -0
  273. package/skills/plan-to-pr/test_task_ticket_contract.py +151 -0
  274. package/skills/pr-converge/SKILL.md +472 -469
  275. package/skills/pr-converge/reference/examples.md +3 -3
  276. package/skills/pr-converge/reference/fix-protocol.md +1 -1
  277. package/skills/pr-converge/reference/ground-rules.md +7 -4
  278. package/skills/pr-converge/reference/multi-pr-orchestration.md +4 -1
  279. package/skills/pr-converge/reference/per-tick.md +5 -5
  280. package/skills/pr-converge/reference/progress-checklist.md +1 -1
  281. package/skills/pr-converge/scripts/check_convergence_gates.py +279 -279
  282. package/skills/pr-converge/scripts/test_check_convergence_codex.py +507 -507
  283. package/skills/pr-converge/scripts/test_check_convergence_gates.py +84 -84
  284. package/skills/pr-converge/test_step5_host_branch.py +1 -1
  285. package/skills/pr-fix-protocol/SKILL.md +1 -1
  286. package/skills/privacy-hygiene/SKILL.md +68 -68
  287. package/skills/privacy-hygiene/reference/sweep-procedure.md +1 -1
  288. package/skills/prototype/workflows/promotion.md +1 -1
  289. package/skills/release-notes-html/SKILL.md +164 -0
  290. package/skills/session-log/SKILL.md +1 -1
  291. package/skills/task-build/CLAUDE.md +8 -7
  292. package/skills/task-build/SKILL.md +16 -8
  293. package/skills/task-build/reference/tool-routing.md +19 -0
  294. package/rules/claude-md-orphan-file.md +0 -28
  295. package/rules/cleanup-command-forms.md +0 -23
  296. package/rules/code-reviews.md +0 -11
  297. package/rules/env-var-table-code-drift.md +0 -10
  298. package/rules/gh-body-file.md +0 -5
  299. package/rules/gh-paginate.md +0 -3
  300. package/rules/hook-prose-matches-detector.md +0 -15
  301. package/rules/no-historical-clutter.md +0 -26
  302. package/rules/no-inline-destructive-literals.md +0 -9
  303. package/rules/no-justification-noise.md +0 -61
  304. package/rules/package-inventory-stale-entry.md +0 -25
  305. package/rules/right-sized-engineering.md +0 -28
  306. package/rules/self-contained-docs.md +0 -17
  307. package/rules/shell-invocation-policy.md +0 -5
  308. package/rules/tdd.md +0 -7
  309. package/skills/closeout/reference/issue-body-templates.md +0 -108
@@ -0,0 +1,404 @@
1
+ """Shared fixtures and seams for the ``/code-review`` invoker test suite.
2
+
3
+ Every ``test_invoke_code_review_*`` module imports its fixture values, its
4
+ chain-outcome builders, its runner shortcuts, and its seam installer from here
5
+ so the behavioral groups stay small and none of the shared support is copied
6
+ between them.
7
+
8
+ This module inserts the scripts directory on ``sys.path`` before its flat
9
+ import list, so the invoker and its constants package resolve by bare name
10
+ whether or not pytest's conftest has already registered the directory.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import subprocess
16
+ import sys
17
+ from collections.abc import Callable, Sequence
18
+ from dataclasses import dataclass, field
19
+ from pathlib import Path
20
+
21
+ import pytest
22
+
23
+ _SCRIPTS_DIRECTORY = str(Path(__file__).resolve().parent)
24
+ if _SCRIPTS_DIRECTORY not in sys.path:
25
+ sys.path.insert(0, _SCRIPTS_DIRECTORY)
26
+
27
+ import claude_chain_runner as chain_runner # noqa: E402
28
+ import invoke_code_review as invoker # noqa: E402
29
+ from claude_chain_runner import ChainAttempt, ChainInvocationOutcome # noqa: E402
30
+ from dev_env_scripts_constants.code_review_constants import ( # noqa: E402
31
+ CLI_SESSION_MODEL_FLAG,
32
+ CODE_REVIEW_MODEL_ALIAS,
33
+ GIT_BINARY,
34
+ RECORD_STAMP_FLAG,
35
+ )
36
+ from dev_env_scripts_constants.grok_worker_constants import ( # noqa: E402
37
+ CLI_TIMEOUT_FLAG,
38
+ CWD_FLAG,
39
+ )
40
+ from dev_env_scripts_constants.timing import ( # noqa: E402
41
+ DEFAULT_CODE_REVIEW_TIMEOUT_SECONDS,
42
+ )
43
+
44
+ HOST_PROFILE_CLAUDE = "Claude"
45
+ HOST_PROFILE_THIRD_PARTY = "ThirdParty"
46
+
47
+ FIXTURE_SERVED_COMMAND = "claude"
48
+ FIXTURE_CHAIN_RETURNCODE = 0
49
+ FIXTURE_FAILED_RETURNCODE = 1
50
+ FIXTURE_GIT_STATUS_FAILURE_RETURNCODE = 128
51
+ FIXTURE_CHAIN_STDOUT = '{"result":"review done"}'
52
+ FIXTURE_SESSION_OPUS = "opus"
53
+ FIXTURE_SESSION_OPUS_UPPER = "Opus"
54
+ FIXTURE_SESSION_SONNET = "sonnet"
55
+ FIXTURE_SESSION_HAIKU = "haiku"
56
+ FIXTURE_CHAIN_CONFIG_ERROR_MESSAGE = "chain config missing"
57
+ FIXTURE_HOST_PROFILE_ERROR_MESSAGE = "unknown host profile"
58
+ DIRTY_FILE_NAME = "review_fix.txt"
59
+ DIRTY_FILE_CONTENTS = "applied fix\n"
60
+ GIT_INIT_TIMEOUT_SECONDS = 30
61
+ REPOSITORY_USER_EMAIL = "reviewer@example.com"
62
+ REPOSITORY_USER_NAME = "Reviewer"
63
+ PINNED_INITIAL_BRANCH = "main"
64
+
65
+
66
+ def _run_git(*git_arguments: str, working_directory: Path | None = None) -> None:
67
+ all_git_tokens = [GIT_BINARY]
68
+ if working_directory is not None:
69
+ all_git_tokens += ["-C", str(working_directory)]
70
+ all_git_tokens += list(git_arguments)
71
+ subprocess.run(
72
+ all_git_tokens,
73
+ check=True,
74
+ capture_output=True,
75
+ text=True,
76
+ timeout=GIT_INIT_TIMEOUT_SECONDS,
77
+ )
78
+
79
+
80
+ def _initialize_repository(repository_directory: Path) -> None:
81
+ repository_directory.mkdir(parents=True, exist_ok=True)
82
+ _run_git(
83
+ "init",
84
+ "--initial-branch",
85
+ PINNED_INITIAL_BRANCH,
86
+ working_directory=repository_directory,
87
+ )
88
+ _run_git(
89
+ "config",
90
+ "user.email",
91
+ REPOSITORY_USER_EMAIL,
92
+ working_directory=repository_directory,
93
+ )
94
+ _run_git(
95
+ "config",
96
+ "user.name",
97
+ REPOSITORY_USER_NAME,
98
+ working_directory=repository_directory,
99
+ )
100
+
101
+
102
+ def write_dirty_tree(working_directory: Path) -> None:
103
+ dirty_file = working_directory / DIRTY_FILE_NAME
104
+ dirty_file.write_text(DIRTY_FILE_CONTENTS, encoding="utf-8")
105
+
106
+
107
+ def run_record_stamp_cli(working_directory: Path, *, effort: str) -> int:
108
+ return invoker.main(
109
+ [
110
+ CWD_FLAG,
111
+ str(working_directory),
112
+ CLI_SESSION_MODEL_FLAG,
113
+ CODE_REVIEW_MODEL_ALIAS,
114
+ RECORD_STAMP_FLAG,
115
+ effort,
116
+ ]
117
+ )
118
+
119
+
120
+ def claude_served(
121
+ *,
122
+ returncode: int = FIXTURE_CHAIN_RETURNCODE,
123
+ stdout: str = FIXTURE_CHAIN_STDOUT,
124
+ ) -> ChainInvocationOutcome:
125
+ return ChainInvocationOutcome(
126
+ served_command=FIXTURE_SERVED_COMMAND,
127
+ returncode=returncode,
128
+ stdout=stdout,
129
+ stderr="",
130
+ attempts=(ChainAttempt(command=FIXTURE_SERVED_COMMAND, status="served"),),
131
+ )
132
+
133
+
134
+ def claude_failed() -> ChainInvocationOutcome:
135
+ return ChainInvocationOutcome(
136
+ served_command=None,
137
+ returncode=FIXTURE_FAILED_RETURNCODE,
138
+ stdout="",
139
+ stderr="chain exhausted",
140
+ attempts=(
141
+ ChainAttempt(command=FIXTURE_SERVED_COMMAND, status="usage_limited"),
142
+ ),
143
+ )
144
+
145
+
146
+ def init_git_repository(repository_directory: Path) -> Path:
147
+ _initialize_repository(repository_directory)
148
+ (repository_directory / "README.md").write_text("baseline\n", encoding="utf-8")
149
+ _run_git("add", "README.md", working_directory=repository_directory)
150
+ _run_git("commit", "-m", "baseline", working_directory=repository_directory)
151
+ return repository_directory
152
+
153
+
154
+ def run_review(
155
+ working_directory: Path, *, session_model: str
156
+ ) -> invoker.CodeReviewOutcome:
157
+ return invoker.invoke_code_review(
158
+ working_directory=working_directory,
159
+ session_model=session_model,
160
+ timeout_seconds=DEFAULT_CODE_REVIEW_TIMEOUT_SECONDS,
161
+ )
162
+
163
+
164
+ def run_review_cli(working_directory: Path, *, session_model: str) -> int:
165
+ return invoker.main(
166
+ [
167
+ CWD_FLAG,
168
+ str(working_directory),
169
+ CLI_SESSION_MODEL_FLAG,
170
+ session_model,
171
+ CLI_TIMEOUT_FLAG,
172
+ str(DEFAULT_CODE_REVIEW_TIMEOUT_SECONDS),
173
+ ]
174
+ )
175
+
176
+
177
+ @dataclass
178
+ class SeamCallLog:
179
+ claude_calls: int = 0
180
+ claude_arguments: list[str] | None = None
181
+ host_profile_calls: int = 0
182
+ is_stdin_empty: bool = False
183
+ claude_working_directory: Path | None = None
184
+ all_observed_working_directories: list[Path] = field(default_factory=list)
185
+
186
+
187
+ @dataclass
188
+ class _SeamConfiguration:
189
+ host_profile: str
190
+ claude_outcome: ChainInvocationOutcome | BaseException | None
191
+ should_dirty_tree_on_chain: bool
192
+ working_directory: Path | None
193
+
194
+
195
+ def _record_dirty_tree(configuration: _SeamConfiguration) -> None:
196
+ if not configuration.should_dirty_tree_on_chain:
197
+ return
198
+ if configuration.working_directory is None:
199
+ return
200
+ write_dirty_tree(configuration.working_directory)
201
+
202
+
203
+ def _detect_empty_stdin(maybe_stdin: object) -> bool | None:
204
+ if maybe_stdin is subprocess.DEVNULL:
205
+ return True
206
+ if maybe_stdin is None:
207
+ return None
208
+ maybe_read = getattr(maybe_stdin, "read", None)
209
+ if not callable(maybe_read):
210
+ return None
211
+ stdin_contents = maybe_read()
212
+ maybe_seek = getattr(maybe_stdin, "seek", None)
213
+ if callable(maybe_seek):
214
+ maybe_seek(0)
215
+ return stdin_contents == ""
216
+
217
+
218
+ def _build_host_profile_seam(
219
+ call_log: SeamCallLog, configuration: _SeamConfiguration
220
+ ) -> Callable[..., str]:
221
+ def fake_host_profile(setting_by_name: object | None = None) -> str:
222
+ del setting_by_name
223
+ call_log.host_profile_calls += 1
224
+ return configuration.host_profile
225
+
226
+ return fake_host_profile
227
+
228
+
229
+ def _build_claude_seam(
230
+ call_log: SeamCallLog, configuration: _SeamConfiguration
231
+ ) -> Callable[..., ChainInvocationOutcome]:
232
+ def fake_claude(
233
+ all_claude_arguments: list[str], *, timeout_seconds: int
234
+ ) -> ChainInvocationOutcome:
235
+ call_log.claude_calls += 1
236
+ call_log.claude_arguments = list(all_claude_arguments)
237
+ chain_runner.chain_subprocess_runner(
238
+ ["claude", *all_claude_arguments],
239
+ capture_output=True,
240
+ text=True,
241
+ timeout=timeout_seconds,
242
+ check=False,
243
+ )
244
+ _record_dirty_tree(configuration)
245
+ if isinstance(configuration.claude_outcome, BaseException):
246
+ raise configuration.claude_outcome
247
+ assert isinstance(configuration.claude_outcome, ChainInvocationOutcome)
248
+ return configuration.claude_outcome
249
+
250
+ return fake_claude
251
+
252
+
253
+ def _build_subprocess_seam(
254
+ call_log: SeamCallLog,
255
+ ) -> Callable[..., subprocess.CompletedProcess[str]]:
256
+ def tracking_subprocess_runner(
257
+ all_invocation_tokens: Sequence[str],
258
+ *all_positionals: object,
259
+ **all_keywords: object,
260
+ ) -> subprocess.CompletedProcess[str]:
261
+ del all_positionals
262
+ maybe_empty_stdin = _detect_empty_stdin(all_keywords.get("stdin"))
263
+ if maybe_empty_stdin is not None:
264
+ call_log.is_stdin_empty = maybe_empty_stdin
265
+ maybe_cwd = all_keywords.get("cwd")
266
+ if maybe_cwd is not None:
267
+ resolved_directory = Path(str(maybe_cwd))
268
+ call_log.claude_working_directory = resolved_directory
269
+ call_log.all_observed_working_directories.append(resolved_directory)
270
+ return subprocess.CompletedProcess(
271
+ args=list(all_invocation_tokens),
272
+ returncode=0,
273
+ stdout="{}",
274
+ stderr="",
275
+ )
276
+
277
+ return tracking_subprocess_runner
278
+
279
+
280
+ def _apply_seams(
281
+ monkeypatch: pytest.MonkeyPatch,
282
+ call_log: SeamCallLog,
283
+ configuration: _SeamConfiguration,
284
+ ) -> None:
285
+ monkeypatch.setattr(
286
+ invoker,
287
+ "review_host_profile_detector",
288
+ _build_host_profile_seam(call_log, configuration),
289
+ )
290
+ monkeypatch.setattr(
291
+ invoker,
292
+ "review_claude_runner",
293
+ _build_claude_seam(call_log, configuration),
294
+ )
295
+ monkeypatch.setattr(
296
+ chain_runner,
297
+ "chain_subprocess_runner",
298
+ _build_subprocess_seam(call_log),
299
+ )
300
+
301
+
302
+ def install_seams(
303
+ monkeypatch: pytest.MonkeyPatch,
304
+ *,
305
+ host_profile: str = HOST_PROFILE_CLAUDE,
306
+ claude_outcome: ChainInvocationOutcome | BaseException | None = None,
307
+ should_dirty_tree_on_chain: bool = False,
308
+ working_directory: Path | None = None,
309
+ ) -> SeamCallLog:
310
+ call_log = SeamCallLog()
311
+ configuration = _SeamConfiguration(
312
+ host_profile=host_profile,
313
+ claude_outcome=claude_outcome,
314
+ should_dirty_tree_on_chain=should_dirty_tree_on_chain,
315
+ working_directory=working_directory,
316
+ )
317
+ _apply_seams(monkeypatch, call_log, configuration)
318
+ return call_log
319
+
320
+
321
+ EFFORT_LOW = "low"
322
+ REJECTED_ULTRA_EFFORT = "ultra"
323
+ SINGLE_PASS_CAP = 1
324
+ MISSING_STORE_FILE_NAME = "code_review_stamp_store_absent.py"
325
+ SURFACE_SOURCE = "def add(left: int, right: int) -> int:\n return left + right\n"
326
+ SURFACE_CHANGE_SOURCE = (
327
+ "def add(left: int, right: int) -> int:\n return left - right\n"
328
+ )
329
+
330
+
331
+ def _make_repo_with_change_surface(tmp_path: Path) -> Path:
332
+ origin_directory = tmp_path / "origin.git"
333
+ work_directory = tmp_path / "work"
334
+ _run_git(
335
+ "init",
336
+ "--bare",
337
+ "--initial-branch",
338
+ PINNED_INITIAL_BRANCH,
339
+ str(origin_directory),
340
+ )
341
+ _initialize_repository(work_directory)
342
+ (work_directory / "app.py").write_text(SURFACE_SOURCE, encoding="utf-8")
343
+ _run_git("add", "-A", working_directory=work_directory)
344
+ _run_git("commit", "-m", "base", working_directory=work_directory)
345
+ _run_git(
346
+ "remote",
347
+ "add",
348
+ "origin",
349
+ str(origin_directory),
350
+ working_directory=work_directory,
351
+ )
352
+ _run_git(
353
+ "push", "-u", "origin", PINNED_INITIAL_BRANCH, working_directory=work_directory
354
+ )
355
+ (work_directory / "app.py").write_text(SURFACE_CHANGE_SOURCE, encoding="utf-8")
356
+ return work_directory
357
+
358
+
359
+ def _isolate_home(monkeypatch: pytest.MonkeyPatch, fake_home: Path) -> None:
360
+ home_text = str(fake_home)
361
+ monkeypatch.setenv("HOME", home_text)
362
+ monkeypatch.setenv("USERPROFILE", home_text)
363
+ monkeypatch.delenv("HOMEDRIVE", raising=False)
364
+ monkeypatch.delenv("HOMEPATH", raising=False)
365
+
366
+
367
+ def prepared_surface_repo(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> Path:
368
+ fake_home = tmp_path / "home"
369
+ fake_home.mkdir()
370
+ _isolate_home(monkeypatch, fake_home)
371
+ return _make_repo_with_change_surface(tmp_path)
372
+
373
+
374
+ def _chain_clean_outcome() -> invoker.CodeReviewOutcome:
375
+ return invoker.CodeReviewOutcome(
376
+ mode=invoker.MODE_CHAIN,
377
+ served_command=FIXTURE_SERVED_COMMAND,
378
+ returncode=FIXTURE_CHAIN_RETURNCODE,
379
+ is_dirty_tree=True,
380
+ )
381
+
382
+
383
+ def stable_clean_review(**_review_keywords: object) -> invoker.CodeReviewOutcome:
384
+ return _chain_clean_outcome()
385
+
386
+
387
+ def surface_changing_review(
388
+ *, working_directory: Path, **_review_keywords: object
389
+ ) -> invoker.CodeReviewOutcome:
390
+ write_dirty_tree(working_directory)
391
+ return _chain_clean_outcome()
392
+
393
+
394
+ class DriftingReview:
395
+ def __init__(self) -> None:
396
+ self.pass_count = 0
397
+
398
+ def __call__(
399
+ self, *, working_directory: Path, **_review_keywords: object
400
+ ) -> invoker.CodeReviewOutcome:
401
+ self.pass_count += 1
402
+ drift_path = working_directory / f"fix_{self.pass_count}.txt"
403
+ drift_path.write_text(str(self.pass_count), encoding="utf-8")
404
+ return _chain_clean_outcome()
@@ -40,6 +40,7 @@ import io
40
40
  import json
41
41
  import subprocess
42
42
  import sys
43
+ import tempfile
43
44
  from collections.abc import Callable, Sequence
44
45
  from dataclasses import dataclass
45
46
  from pathlib import Path
@@ -58,6 +59,7 @@ from dev_env_scripts_constants.claude_chain_constants import (
58
59
  ATTEMPT_STATUS_USAGE_LIMITED,
59
60
  ATTEMPT_SUMMARY_ENTRY_TEMPLATE,
60
61
  ATTEMPT_SUMMARY_JOIN_SEPARATOR,
62
+ CARRIAGE_RETURN,
61
63
  CHAIN_CONFIG_ERROR_EXIT_CODE,
62
64
  CHAIN_EXHAUSTED_EXIT_CODE,
63
65
  CHAIN_EXHAUSTED_MESSAGE_TEMPLATE,
@@ -82,13 +84,151 @@ from dev_env_scripts_constants.claude_chain_constants import (
82
84
  CONFIG_MISSING_MESSAGE_TEMPLATE,
83
85
  CONFIG_NOT_OBJECT_REASON,
84
86
  CONFIG_UNREADABLE_MESSAGE_TEMPLATE,
87
+ CRLF_NEWLINE,
85
88
  DEFAULT_TIMEOUT_SECONDS,
86
89
  EXAMPLE_CONFIG_FILENAME,
90
+ LINE_FEED,
87
91
  NO_COMPLETED_PROCESS_RETURN_CODE,
88
92
  UTF8_ENCODING,
89
93
  )
90
94
 
91
95
 
96
+ def _decode_captured_stream(raw_bytes: bytes, encoding: str, errors: str) -> str:
97
+ """Decode captured *raw_bytes* with ``text=True`` universal-newline semantics.
98
+
99
+ Spool capture writes binary temp files, so a bare ``.decode`` leaves CRLF and
100
+ bare CR intact. ``subprocess.run(..., text=True)`` normalized those to LF;
101
+ this helper restores that contract for Windows children that emit ``\\r\\n``.
102
+ """
103
+ decoded_text = raw_bytes.decode(encoding, errors)
104
+ return decoded_text.replace(CRLF_NEWLINE, LINE_FEED).replace(
105
+ CARRIAGE_RETURN, LINE_FEED
106
+ )
107
+
108
+
109
+ class _SpooledByteStream(Protocol):
110
+ """Binary spool with seek/read — TemporaryFile wrappers and BufferedIO."""
111
+
112
+ def seek(self, target: int, whence: int = 0, /) -> int: ...
113
+
114
+ def read(self, size: int | None = -1, /) -> bytes: ...
115
+
116
+
117
+ def _decoded_spooled_streams(
118
+ stdout_file: _SpooledByteStream,
119
+ stderr_file: _SpooledByteStream,
120
+ encoding: str,
121
+ errors: str,
122
+ ) -> tuple[str, str]:
123
+ """Seek both spool files to the start and decode their full contents."""
124
+ stdout_file.seek(0)
125
+ stderr_file.seek(0)
126
+ return (
127
+ _decode_captured_stream(stdout_file.read(), encoding, errors),
128
+ _decode_captured_stream(stderr_file.read(), encoding, errors),
129
+ )
130
+
131
+
132
+ def _attach_partial_timeout_streams(
133
+ timeout_error: subprocess.TimeoutExpired,
134
+ stdout_file: _SpooledByteStream,
135
+ stderr_file: _SpooledByteStream,
136
+ encoding: str,
137
+ errors: str,
138
+ ) -> None:
139
+ """Decode partial spool contents onto *timeout_error* before re-raise."""
140
+ captured_stdout, captured_stderr = _decoded_spooled_streams(
141
+ stdout_file, stderr_file, encoding, errors
142
+ )
143
+ timeout_error.stdout = captured_stdout
144
+ timeout_error.stderr = captured_stderr
145
+
146
+
147
+ # Capturing a large-output child through OS pipes (``capture_output=True``)
148
+ # deadlocks on Windows: the child buffers its whole response and flushes it at
149
+ # once, the pipe buffer fills before the parent drains it, and both sides block.
150
+ # Redirecting each stream to a temporary file removes the pipe, so the child
151
+ # writes freely; the files are then read back and decoded the way a pipe capture
152
+ # would. ``capture_output``, ``text``, and ``env`` are ignored in favor of file
153
+ # redirection and the parent environment; ``timeout``, ``check``, ``cwd``,
154
+ # ``stdin``, ``input``, ``encoding``, and ``errors`` are honored. On timeout,
155
+ # partial stdout/stderr are decoded from the temp files and attached to the
156
+ # raised ``TimeoutExpired``. When ``check=True`` and the child exits non-zero,
157
+ # ``CalledProcessError.stdout`` / ``.stderr`` stay unset (temp files are not
158
+ # attached to that raised error).
159
+ def _run_captured_subprocess(
160
+ all_invocation_tokens: list[str],
161
+ **all_subprocess_options: object,
162
+ ) -> subprocess.CompletedProcess[str]:
163
+ """Run *all_invocation_tokens*, spooling stdout and stderr to temp files."""
164
+ encoding = str(all_subprocess_options.get("encoding") or UTF8_ENCODING)
165
+ errors = str(all_subprocess_options.get("errors") or CODEC_ERROR_STRATEGY)
166
+ input_bytes = _captured_stdin_bytes(all_subprocess_options, encoding, errors)
167
+ working_directory = all_subprocess_options.get("cwd")
168
+ timeout_seconds = all_subprocess_options.get("timeout")
169
+ with (
170
+ tempfile.TemporaryFile() as stdout_file,
171
+ tempfile.TemporaryFile() as stderr_file,
172
+ ):
173
+ try:
174
+ completion = subprocess.run(
175
+ all_invocation_tokens,
176
+ stdout=stdout_file,
177
+ stderr=stderr_file,
178
+ cwd=working_directory if isinstance(working_directory, str) else None,
179
+ check=bool(all_subprocess_options.get("check", False)),
180
+ timeout=(
181
+ float(timeout_seconds)
182
+ if isinstance(timeout_seconds, (int, float))
183
+ else None
184
+ ),
185
+ input=input_bytes,
186
+ )
187
+ except subprocess.TimeoutExpired as timeout_error:
188
+ _attach_partial_timeout_streams(
189
+ timeout_error, stdout_file, stderr_file, encoding, errors
190
+ )
191
+ raise
192
+ captured_stdout, captured_stderr = _decoded_spooled_streams(
193
+ stdout_file, stderr_file, encoding, errors
194
+ )
195
+ return subprocess.CompletedProcess(
196
+ all_invocation_tokens,
197
+ completion.returncode,
198
+ captured_stdout,
199
+ captured_stderr,
200
+ )
201
+
202
+
203
+ def _captured_stdin_bytes(
204
+ all_subprocess_options: dict[str, object], encoding: str, errors: str
205
+ ) -> bytes | None:
206
+ """Return the bytes to feed the child's stdin for a spooled run.
207
+
208
+ ::
209
+
210
+ stdin=<open prompt file> -> the file's bytes
211
+ stdin=subprocess.DEVNULL -> b"" (an immediate EOF)
212
+ input="charter text" -> the encoded text
213
+
214
+ A wrapper hands the runner a ``stdin`` stream or a ``DEVNULL`` sentinel; the
215
+ spooled run reads from an ``input`` pipe rather than the caller's handle, so
216
+ the stream is read into bytes here to deliver the same stdin a direct pipe
217
+ would. When no ``stdin`` is given, the ``input`` text is encoded instead.
218
+ """
219
+ stdin_source = all_subprocess_options.get("stdin")
220
+ if isinstance(stdin_source, io.TextIOBase):
221
+ return stdin_source.read().encode(encoding, errors)
222
+ if isinstance(stdin_source, (io.RawIOBase, io.BufferedIOBase)):
223
+ return stdin_source.read() or b""
224
+ if isinstance(stdin_source, int):
225
+ return b""
226
+ input_text = all_subprocess_options.get("input")
227
+ if input_text is None:
228
+ return None
229
+ return str(input_text).encode(encoding, errors)
230
+
231
+
92
232
  class ChainConfigurationError(Exception):
93
233
  """Raised when the chain configuration is missing, unreadable, or malformed."""
94
234
 
@@ -138,7 +278,7 @@ class WeeklyUsageAccountReport(Protocol):
138
278
  command: str
139
279
 
140
280
 
141
- chain_subprocess_runner = subprocess.run
281
+ chain_subprocess_runner = _run_captured_subprocess
142
282
 
143
283
 
144
284
  def _load_chain_usage_module() -> ModuleType: