claude-dev-env 1.94.0 → 2.0.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 (280) hide show
  1. package/_shared/advisor/CLAUDE.md +2 -2
  2. package/_shared/advisor/advisor-protocol.md +41 -33
  3. package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +17 -13
  4. package/_shared/advisor/scripts/model_tier_run_validator.py +27 -18
  5. package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +95 -25
  6. package/_shared/advisor/scripts/tests/test_tier_model_ids.py +17 -17
  7. package/_shared/advisor/scripts/tier_model_ids.py +18 -18
  8. package/_shared/pr-loop/CLAUDE.md +1 -0
  9. package/_shared/pr-loop/scripts/CLAUDE.md +2 -1
  10. package/_shared/pr-loop/scripts/README.md +1 -0
  11. package/_shared/pr-loop/scripts/code_rules_gate.py +253 -1980
  12. package/_shared/pr-loop/scripts/code_rules_gate_parts/CLAUDE.md +32 -0
  13. package/_shared/pr-loop/scripts/code_rules_gate_parts/__init__.py +7 -0
  14. package/_shared/pr-loop/scripts/code_rules_gate_parts/added_line_maps.py +268 -0
  15. package/_shared/pr-loop/scripts/code_rules_gate_parts/enforcer_loading.py +172 -0
  16. package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_arguments.py +70 -0
  17. package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_running.py +326 -0
  18. package/_shared/pr-loop/scripts/code_rules_gate_parts/git_blob_readers.py +85 -0
  19. package/_shared/pr-loop/scripts/code_rules_gate_parts/git_file_sets.py +331 -0
  20. package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_running.py +369 -0
  21. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/conftest.py +14 -0
  22. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_added_line_maps.py +118 -0
  23. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_enforcer_loading.py +17 -0
  24. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_gate_arguments.py +29 -0
  25. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_gate_running.py +99 -0
  26. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_git_blob_readers.py +69 -0
  27. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_git_file_sets.py +137 -0
  28. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_staged_test_running.py +116 -0
  29. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_violation_scoping.py +75 -0
  30. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_wrapper_plumb_check.py +49 -0
  31. package/_shared/pr-loop/scripts/code_rules_gate_parts/violation_scoping.py +328 -0
  32. package/_shared/pr-loop/scripts/code_rules_gate_parts/wrapper_plumb_check.py +206 -0
  33. package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +24 -17
  34. package/_shared/pr-loop/scripts/pr_loop_shared_constants/reviews_disabled_constants.py +1 -0
  35. package/_shared/pr-loop/scripts/reviews_disabled.py +19 -2
  36. package/_shared/pr-loop/scripts/test_code_rules_gate.py +278 -0
  37. package/_shared/pr-loop/scripts/tests/test_code_rules_gate_constants.py +6 -39
  38. package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +43 -0
  39. package/_shared/pr-loop/worker-spawn.md +186 -0
  40. package/agents/code-verifier.md +1 -1
  41. package/bin/CLAUDE.md +10 -1
  42. package/bin/ever-shipped-skills.mjs +73 -0
  43. package/bin/expand_home_directory_tokens.mjs +1 -1
  44. package/bin/install.mjs +140 -7
  45. package/bin/install.prune.test.mjs +457 -0
  46. package/docs/CODE_RULES.md +1 -1
  47. package/hooks/advisory/refactor_guard.py +3 -4
  48. package/hooks/blocking/CLAUDE.md +7 -1
  49. package/hooks/blocking/block_main_commit.py +2 -2
  50. package/hooks/blocking/claude_md_orphan_file_blocker.py +75 -699
  51. package/hooks/blocking/claude_md_orphan_file_blocker_parts/CLAUDE.md +28 -0
  52. package/hooks/blocking/claude_md_orphan_file_blocker_parts/__init__.py +1 -0
  53. package/hooks/blocking/claude_md_orphan_file_blocker_parts/config/__init__.py +1 -0
  54. package/hooks/blocking/claude_md_orphan_file_blocker_parts/config/orphan_blocker_constants.py +18 -0
  55. package/hooks/blocking/claude_md_orphan_file_blocker_parts/decision.py +81 -0
  56. package/hooks/blocking/claude_md_orphan_file_blocker_parts/references.py +307 -0
  57. package/hooks/blocking/claude_md_orphan_file_blocker_parts/scan_plan.py +124 -0
  58. package/hooks/blocking/claude_md_orphan_file_blocker_parts/subtree_scan.py +179 -0
  59. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/conftest.py +10 -0
  60. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_decision.py +34 -0
  61. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_references.py +42 -0
  62. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_scan_plan.py +27 -0
  63. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_subtree_scan.py +30 -0
  64. package/hooks/blocking/code_rules_boolean_mustcheck.py +1 -1
  65. package/hooks/blocking/code_rules_enforcer.py +4 -0
  66. package/hooks/blocking/code_rules_mock_completeness.py +1 -1
  67. package/hooks/blocking/code_rules_optional_params.py +2 -2
  68. package/hooks/blocking/code_rules_shared.py +83 -1
  69. package/hooks/blocking/code_rules_test_assertions.py +1 -1
  70. package/hooks/blocking/code_rules_test_layout.py +9 -3
  71. package/hooks/blocking/code_rules_typeddict_stub.py +1 -1
  72. package/hooks/blocking/gh_pr_author_enforcer.py +1 -1
  73. package/hooks/blocking/inventory_intent_records/CLAUDE.md +26 -0
  74. package/hooks/blocking/inventory_intent_records/__init__.py +1 -0
  75. package/hooks/blocking/inventory_intent_records/config/__init__.py +1 -0
  76. package/hooks/blocking/inventory_intent_records/config/intent_records_constants.py +20 -0
  77. package/hooks/blocking/inventory_intent_records/records.py +271 -0
  78. package/hooks/blocking/inventory_intent_records/tests/conftest.py +10 -0
  79. package/hooks/blocking/inventory_intent_records/tests/test_records.py +80 -0
  80. package/hooks/blocking/package_inventory_stale_blocker.py +54 -384
  81. package/hooks/blocking/package_inventory_stale_blocker_parts/CLAUDE.md +26 -0
  82. package/hooks/blocking/package_inventory_stale_blocker_parts/__init__.py +1 -0
  83. package/hooks/blocking/package_inventory_stale_blocker_parts/config/__init__.py +1 -0
  84. package/hooks/blocking/package_inventory_stale_blocker_parts/config/inventory_blocker_constants.py +16 -0
  85. package/hooks/blocking/package_inventory_stale_blocker_parts/decision.py +84 -0
  86. package/hooks/blocking/package_inventory_stale_blocker_parts/inventory_detection.py +307 -0
  87. package/hooks/blocking/package_inventory_stale_blocker_parts/tests/conftest.py +10 -0
  88. package/hooks/blocking/package_inventory_stale_blocker_parts/tests/test_decision.py +38 -0
  89. package/hooks/blocking/package_inventory_stale_blocker_parts/tests/test_inventory_detection.py +61 -0
  90. package/hooks/blocking/pii_payload_scan.py +138 -42
  91. package/hooks/blocking/pii_prevention_blocker.py +185 -291
  92. package/hooks/blocking/pii_prevention_blocker_parts/CLAUDE.md +24 -0
  93. package/hooks/blocking/pii_prevention_blocker_parts/__init__.py +1 -0
  94. package/hooks/blocking/pii_prevention_blocker_parts/config/__init__.py +1 -0
  95. package/hooks/blocking/pii_prevention_blocker_parts/config/repository_resolution_constants.py +28 -0
  96. package/hooks/blocking/pii_prevention_blocker_parts/repository_exemption.py +214 -0
  97. package/hooks/blocking/pii_prevention_blocker_parts/repository_resolution.py +208 -0
  98. package/hooks/blocking/plain_language_blocker.py +138 -4
  99. package/hooks/blocking/pr_description_command_parser.py +8 -4
  100. package/hooks/blocking/precommit_code_rules_gate.py +3 -3
  101. package/hooks/blocking/sensitive_file_protector.py +114 -48
  102. package/hooks/blocking/tdd_enforcer.py +97 -601
  103. package/hooks/blocking/tdd_enforcer_parts/CLAUDE.md +30 -0
  104. package/hooks/blocking/tdd_enforcer_parts/__init__.py +1 -0
  105. package/hooks/blocking/tdd_enforcer_parts/candidate_paths.py +142 -0
  106. package/hooks/blocking/tdd_enforcer_parts/config/__init__.py +1 -0
  107. package/hooks/blocking/tdd_enforcer_parts/config/tdd_enforcer_constants.py +32 -0
  108. package/hooks/blocking/tdd_enforcer_parts/content_analysis.py +268 -0
  109. package/hooks/blocking/tdd_enforcer_parts/decisions.py +92 -0
  110. package/hooks/blocking/tdd_enforcer_parts/freshness.py +80 -0
  111. package/hooks/blocking/tdd_enforcer_parts/git_tracking.py +63 -0
  112. package/hooks/blocking/tdd_enforcer_parts/path_classification.py +119 -0
  113. package/hooks/blocking/tdd_enforcer_parts/tests/conftest.py +10 -0
  114. package/hooks/blocking/tdd_enforcer_parts/tests/test_candidate_paths.py +31 -0
  115. package/hooks/blocking/tdd_enforcer_parts/tests/test_content_analysis.py +30 -0
  116. package/hooks/blocking/tdd_enforcer_parts/tests/test_decisions.py +34 -0
  117. package/hooks/blocking/tdd_enforcer_parts/tests/test_freshness.py +28 -0
  118. package/hooks/blocking/tdd_enforcer_parts/tests/test_git_tracking.py +48 -0
  119. package/hooks/blocking/tdd_enforcer_parts/tests/test_path_classification.py +36 -0
  120. package/hooks/blocking/test_code_rules_enforcer_scratchpad.py +105 -0
  121. package/hooks/blocking/test_code_rules_shared.py +181 -0
  122. package/hooks/blocking/test_inventory_deadlock_resolution.py +154 -0
  123. package/hooks/blocking/test_pii_payload_scan.py +168 -0
  124. package/hooks/blocking/test_plain_language_blocker_allowlist.py +184 -0
  125. package/hooks/blocking/test_sensitive_file_protector.py +185 -0
  126. package/hooks/blocking/test_tdd_enforcer_restore.py +108 -0
  127. package/hooks/blocking/test_tdd_enforcer_scratchpad.py +105 -0
  128. package/hooks/blocking/tests/conftest.py +10 -0
  129. package/hooks/blocking/tests/test_pii_prevention_blocker.py +260 -0
  130. package/hooks/blocking/tests/test_repository_exemption.py +105 -0
  131. package/hooks/blocking/tests/test_repository_resolution.py +108 -0
  132. package/hooks/diagnostic/hook_log_extractor.py +12 -10
  133. package/hooks/git-hooks/post_commit.py +3 -4
  134. package/hooks/hooks_constants/CLAUDE.md +4 -2
  135. package/hooks/hooks_constants/banned_identifiers_constants.py +0 -1
  136. package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -1
  137. package/hooks/hooks_constants/harness_scratchpad_constants.py +17 -0
  138. package/hooks/hooks_constants/local_identity.py +59 -8
  139. package/hooks/hooks_constants/pii_prevention_constants.py +0 -6
  140. package/hooks/hooks_constants/plain_language_blocker_constants.py +5 -0
  141. package/hooks/hooks_constants/sensitive_file_protector_constants.py +42 -0
  142. package/hooks/hooks_constants/test_local_identity.py +105 -3
  143. package/hooks/pyproject.toml +52 -4
  144. package/hooks/session/plugin_data_dir_cleanup.py +0 -1
  145. package/hooks/validation/mypy_validator.py +2 -2
  146. package/hooks/validators/CLAUDE.md +1 -1
  147. package/hooks/validators/README.md +2 -0
  148. package/hooks/validators/health_check.py +1 -0
  149. package/hooks/validators/mypy_integration.py +2 -0
  150. package/hooks/validators/python_style_checks.py +114 -136
  151. package/hooks/validators/python_style_helpers.py +95 -0
  152. package/hooks/validators/ruff_integration.py +3 -0
  153. package/hooks/validators/test_python_style_checks.py +0 -164
  154. package/hooks/validators/test_python_style_checks_decorator_gap.py +119 -0
  155. package/hooks/validators/test_python_style_fixes.py +251 -0
  156. package/hooks/validators/test_python_style_helpers.py +125 -0
  157. package/hooks/workflow/auto_formatter.py +5 -4
  158. package/package.json +1 -1
  159. package/rules/CLAUDE.md +1 -0
  160. package/rules/anti-corollary-tests.md +69 -0
  161. package/rules/bdd.md +1 -3
  162. package/rules/code-reviews.md +1 -1
  163. package/rules/gh-paginate.md +1 -1
  164. package/rules/plain-language.md +2 -0
  165. package/scripts/CLAUDE.md +4 -0
  166. package/scripts/dev_env_scripts_constants/CLAUDE.md +6 -4
  167. package/scripts/dev_env_scripts_constants/code_review_constants.py +71 -0
  168. package/scripts/dev_env_scripts_constants/grok_worker_constants.py +435 -0
  169. package/scripts/dev_env_scripts_constants/timing.py +7 -1
  170. package/scripts/grok_headless_runner.py +294 -0
  171. package/scripts/grok_worker_preflight.py +410 -0
  172. package/scripts/invoke_code_review.py +463 -0
  173. package/scripts/resolve_worker_spawn.py +619 -0
  174. package/scripts/spawn_grok_batch.py +672 -0
  175. package/scripts/test_grok_headless_runner.py +626 -0
  176. package/scripts/test_grok_worker_preflight.py +1054 -0
  177. package/scripts/test_invoke_code_review.py +672 -0
  178. package/scripts/test_resolve_worker_spawn.py +1014 -0
  179. package/scripts/test_spawn_grok_batch.py +1017 -0
  180. package/skills/CLAUDE.md +6 -3
  181. package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +72 -13
  182. package/skills/_shared/pr-loop/scripts/build_fix_prompt.py +121 -14
  183. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +78 -0
  184. package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +121 -0
  185. package/skills/_shared/pr-loop/scripts/test_build_fix_prompt.py +196 -6
  186. package/skills/autoconverge/CLAUDE.md +3 -3
  187. package/skills/autoconverge/SKILL.md +9 -3
  188. package/skills/autoconverge/reference/CLAUDE.md +2 -2
  189. package/skills/autoconverge/reference/convergence.md +33 -11
  190. package/skills/autoconverge/reference/stop-conditions.md +16 -5
  191. package/skills/autoconverge/workflow/CLAUDE.md +2 -1
  192. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +7 -2
  193. package/skills/autoconverge/workflow/converge.codex-gate.test.mjs +300 -0
  194. package/skills/autoconverge/workflow/converge.contract.test.mjs +5 -5
  195. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +29 -29
  196. package/skills/autoconverge/workflow/converge.fix-progress.test.mjs +1 -1
  197. package/skills/autoconverge/workflow/converge.mjs +202 -18
  198. package/skills/bugteam/CLAUDE.md +2 -2
  199. package/skills/bugteam/CONSTRAINTS.md +3 -2
  200. package/skills/bugteam/PROMPTS.md +7 -6
  201. package/skills/bugteam/SKILL.md +18 -13
  202. package/skills/bugteam/reference/README.md +2 -3
  203. package/skills/bugteam/reference/audit-and-teammates.md +215 -35
  204. package/skills/bugteam/reference/design-rationale.md +1 -1
  205. package/skills/bugteam/reference/obstacles/CLAUDE.md +1 -1
  206. package/skills/bugteam/reference/team-setup.md +8 -2
  207. package/skills/closeout/SKILL.md +153 -0
  208. package/skills/closeout/reference/handoff-prompt-template.md +72 -0
  209. package/skills/closeout/reference/issue-body-templates.md +108 -0
  210. package/skills/closeout/reference/pii-redaction-checklist.md +36 -0
  211. package/skills/codex-review/CLAUDE.md +46 -0
  212. package/skills/codex-review/SKILL.md +181 -0
  213. package/skills/codex-review/reference/CLAUDE.md +15 -0
  214. package/skills/codex-review/reference/cli-contract.md +253 -0
  215. package/skills/codex-review/reference/loop-integration.md +118 -0
  216. package/skills/codex-review/scripts/codex_down_classifier.py +98 -0
  217. package/skills/codex-review/scripts/codex_review_scripts_constants/CLAUDE.md +18 -0
  218. package/skills/codex-review/scripts/codex_review_scripts_constants/__init__.py +1 -0
  219. package/skills/codex-review/scripts/codex_review_scripts_constants/classifier_constants.py +35 -0
  220. package/skills/codex-review/scripts/codex_review_scripts_constants/codex_usage_probe_constants.py +86 -0
  221. package/skills/codex-review/scripts/codex_review_scripts_constants/findings_constants.py +18 -0
  222. package/skills/codex-review/scripts/codex_review_scripts_constants/run_constants.py +45 -0
  223. package/skills/codex-review/scripts/codex_usage_probe.py +573 -0
  224. package/skills/codex-review/scripts/fixtures/auth_failure_synthetic.txt +1 -0
  225. package/skills/codex-review/scripts/fixtures/config_load_failure_v0.125.0.txt +1 -0
  226. package/skills/codex-review/scripts/fixtures/freeform_findings_v0.144.3.txt +6 -0
  227. package/skills/codex-review/scripts/fixtures/model_rejection_v0.125.0.jsonl +5 -0
  228. package/skills/codex-review/scripts/fixtures/structured_findings.txt +13 -0
  229. package/skills/codex-review/scripts/fixtures/success_stream_v0.144.3.jsonl +6 -0
  230. package/skills/codex-review/scripts/fixtures/unknown_failure_synthetic.txt +1 -0
  231. package/skills/codex-review/scripts/fixtures/usage_limit_synthetic.txt +1 -0
  232. package/skills/codex-review/scripts/parse_codex_findings.py +207 -0
  233. package/skills/codex-review/scripts/run_codex_review.py +415 -0
  234. package/skills/codex-review/scripts/test_codex_down_classifier.py +143 -0
  235. package/skills/codex-review/scripts/test_codex_usage_probe.py +678 -0
  236. package/skills/codex-review/scripts/test_parse_codex_findings.py +130 -0
  237. package/skills/codex-review/scripts/test_run_codex_review.py +812 -0
  238. package/skills/codex-review/test_skill_scaffold.py +192 -0
  239. package/skills/grok-spawn/CLAUDE.md +28 -0
  240. package/skills/grok-spawn/SKILL.md +226 -0
  241. package/skills/grok-spawn/reference/flag-profiles.md +132 -0
  242. package/skills/grok-spawn/reference/worker-briefs.md +152 -0
  243. package/skills/grokify/SKILL.md +9 -1
  244. package/skills/grokify/capability-claims.test.mjs +28 -0
  245. package/skills/grokify/evals/README.md +72 -0
  246. package/skills/grokify/evals/parse-payload.test.mjs +171 -0
  247. package/skills/grokify/evals/run-capability-evals.mjs +545 -0
  248. package/skills/orchestrator/SKILL.md +32 -22
  249. package/skills/orchestrator-refresh/SKILL.md +12 -8
  250. package/skills/pr-converge/CLAUDE.md +1 -1
  251. package/skills/pr-converge/SKILL.md +34 -13
  252. package/skills/pr-converge/reference/convergence-gates.md +42 -15
  253. package/skills/pr-converge/reference/fix-protocol.md +1 -1
  254. package/skills/pr-converge/reference/ground-rules.md +1 -1
  255. package/skills/pr-converge/reference/per-tick.md +130 -42
  256. package/skills/pr-converge/reference/state-schema.md +10 -0
  257. package/skills/pr-converge/scripts/CLAUDE.md +2 -0
  258. package/skills/pr-converge/scripts/_pr_converge_path_setup.py +5 -1
  259. package/skills/pr-converge/scripts/check_convergence.py +605 -29
  260. package/skills/pr-converge/scripts/check_convergence_availability.py +232 -0
  261. package/skills/pr-converge/scripts/check_convergence_gates.py +279 -235
  262. package/skills/pr-converge/scripts/check_convergence_thread_gates.py +1 -1
  263. package/skills/pr-converge/scripts/pr_converge_scripts_constants/convergence_gate_constants.py +36 -2
  264. package/skills/pr-converge/scripts/test__pr_converge_path_setup.py +4 -0
  265. package/skills/pr-converge/scripts/test_check_convergence.py +71 -3
  266. package/skills/pr-converge/scripts/test_check_convergence_availability.py +326 -0
  267. package/skills/pr-converge/scripts/test_check_convergence_codex.py +507 -0
  268. package/skills/pr-converge/scripts/test_check_convergence_contract.py +89 -17
  269. package/skills/pr-converge/scripts/test_check_convergence_fixture.py +179 -0
  270. package/skills/pr-converge/scripts/test_check_convergence_gates.py +84 -68
  271. package/skills/pr-converge/scripts/test_check_convergence_thread_gates.py +24 -0
  272. package/skills/pr-converge/test_step5_host_branch.py +106 -0
  273. package/skills/pr-fix-protocol/SKILL.md +65 -0
  274. package/skills/pr-loop-cloud-transport/SKILL.md +2 -0
  275. package/skills/reviewer-gates/SKILL.md +7 -5
  276. package/skills/skill-builder/references/skill-modularity.md +1 -1
  277. package/skills/team-advisor/SKILL.md +16 -12
  278. package/system-prompts/software-engineer.xml +7 -6
  279. package/hooks/validators/test_verify_paths.py +0 -32
  280. package/hooks/validators/verify_paths.py +0 -57
@@ -0,0 +1,463 @@
1
+ #!/usr/bin/env python3
2
+ """Host-aware helper that runs the built-in review slash command on opus.
3
+
4
+ Mode decision::
5
+
6
+ host=Claude, session_model=opus -> mode in_session (skill runs slash cmd)
7
+ host=Claude, session_model=sonnet -> mode chain (headless opus spawn)
8
+ host=ThirdParty, any model -> mode chain
9
+
10
+ Chain mode runs ``run_claude`` with argv from ``build_code_review_arguments``
11
+ (single-turn prompt, model opus, json output, bypassPermissions).
12
+
13
+ cwd is the PR working tree and stdin is redirected from the empty stream so
14
+ the spawn does not wait for interactive input. Result JSON on stdout only::
15
+
16
+ {"mode", "served_command", "returncode", "dirty_tree"}
17
+
18
+ A clean stamp requires a successful serve (``returncode == 0``, and for chain
19
+ mode a non-null ``served_command``) plus ``dirty_tree`` false. A failed chain
20
+ or config/host error leaves the tree clean but must not advance past
21
+ CODE_REVIEW — use ``is_code_review_clean_stamp_allowed``.
22
+
23
+ Import ``invoke_code_review`` for the outcome object, or run as a CLI::
24
+
25
+ python invoke_code_review.py --cwd <dir> --session-model <alias>
26
+ [--timeout-seconds N]
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import argparse
32
+ import json
33
+ import subprocess
34
+ import sys
35
+ import threading
36
+ from collections.abc import Callable, Sequence
37
+ from dataclasses import dataclass
38
+ from pathlib import Path
39
+
40
+ if str(Path(__file__).resolve().parent) not in sys.path:
41
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
42
+
43
+ _advisor_scripts_path = str(
44
+ Path(__file__).resolve().parent.parent / "_shared" / "advisor" / "scripts"
45
+ )
46
+ if _advisor_scripts_path not in sys.path:
47
+ sys.path.insert(0, _advisor_scripts_path)
48
+
49
+ from advisor_scripts_constants.model_tier_run_validator_constants import ( # noqa: E402
50
+ HOST_PROFILE_CLAUDE,
51
+ )
52
+ import claude_chain_runner as chain_runner # noqa: E402
53
+ from claude_chain_runner import ( # noqa: E402
54
+ ChainConfigurationError,
55
+ ChainInvocationOutcome,
56
+ run_claude,
57
+ )
58
+ from dev_env_scripts_constants.claude_chain_constants import ( # noqa: E402
59
+ CHAIN_CONFIG_ERROR_EXIT_CODE,
60
+ )
61
+ from dev_env_scripts_constants.code_review_constants import ( # noqa: E402
62
+ CLI_SESSION_MODEL_FLAG,
63
+ CODE_REVIEW_MODEL_ALIAS,
64
+ CODE_REVIEW_PROMPT,
65
+ GIT_BINARY,
66
+ GIT_PORCELAIN_FLAG,
67
+ GIT_STATUS_SUBCOMMAND,
68
+ HOST_PROFILE_ERROR_RETURNCODE,
69
+ IN_SESSION_RETURNCODE,
70
+ MODE_CHAIN,
71
+ MODE_IN_SESSION,
72
+ PERMISSION_MODE_BYPASS,
73
+ PERMISSION_MODE_FLAG,
74
+ RESULT_KEY_DIRTY_TREE,
75
+ RESULT_KEY_MODE,
76
+ RESULT_KEY_RETURNCODE,
77
+ RESULT_KEY_SERVED_COMMAND,
78
+ SUCCESSFUL_REVIEW_RETURNCODE,
79
+ )
80
+ from dev_env_scripts_constants.grok_worker_constants import ( # noqa: E402
81
+ CLI_TIMEOUT_FLAG,
82
+ CWD_FLAG,
83
+ MODEL_FLAG,
84
+ OUTPUT_FORMAT_FLAG,
85
+ OUTPUT_FORMAT_JSON,
86
+ SINGLE_TURN_FLAG,
87
+ )
88
+ from dev_env_scripts_constants.timing import ( # noqa: E402
89
+ DEFAULT_CODE_REVIEW_TIMEOUT_SECONDS,
90
+ )
91
+ from tier_model_ids import detect_host_profile # noqa: E402
92
+
93
+ _CHAIN_RUNNER_LOCK = threading.Lock()
94
+
95
+
96
+ def _chain_runner_lock() -> threading.Lock:
97
+ """Return the module lock that serializes chain-runner stdin/cwd swaps."""
98
+ return _CHAIN_RUNNER_LOCK
99
+
100
+
101
+ @dataclass(frozen=True)
102
+ class CodeReviewOutcome:
103
+ """Outcome of a host-aware code-review invocation.
104
+
105
+ ``mode`` is ``in_session`` when the skill should run the slash command
106
+ itself, or ``chain`` when a headless spawn already ran. ``served_command``
107
+ names the chain binary that served a chain run, or ``None`` otherwise.
108
+ ``is_dirty_tree`` is True when ``git status --porcelain`` is non-empty
109
+ after a chain run (fixes applied).
110
+ """
111
+
112
+ mode: str
113
+ served_command: str | None
114
+ returncode: int
115
+ is_dirty_tree: bool
116
+
117
+
118
+ review_claude_runner = run_claude
119
+ review_host_profile_detector = detect_host_profile
120
+ review_git_status_runner = subprocess.run
121
+
122
+ TextCapturingSubprocessRunner = Callable[
123
+ ...,
124
+ subprocess.CompletedProcess[str],
125
+ ]
126
+
127
+
128
+ def is_opus_session_model(session_model: str) -> bool:
129
+ """Return True when *session_model* is the opus short alias (any letter case).
130
+
131
+ ::
132
+
133
+ is_opus_session_model("opus") # ok: True
134
+ is_opus_session_model("Opus") # ok: True
135
+ is_opus_session_model("sonnet") # ok: False
136
+
137
+ Args:
138
+ session_model: Caller-stated session model short alias.
139
+
140
+ Returns:
141
+ True when the normalized alias matches the code-review model pin.
142
+ """
143
+ return session_model.strip().lower() == CODE_REVIEW_MODEL_ALIAS
144
+
145
+
146
+ def decide_review_mode(*, host_profile: str, session_model: str) -> str:
147
+ """Return ``in_session`` or ``chain`` from host profile and session model.
148
+
149
+ ::
150
+
151
+ decide_review_mode(host_profile="Claude", session_model="opus")
152
+ # ok: "in_session"
153
+ decide_review_mode(host_profile="Claude", session_model="sonnet")
154
+ # ok: "chain"
155
+ decide_review_mode(host_profile="ThirdParty", session_model="opus")
156
+ # ok: "chain"
157
+
158
+ Args:
159
+ host_profile: Detected host profile (``Claude`` or ``ThirdParty``).
160
+ session_model: Caller-stated session model short alias.
161
+
162
+ Returns:
163
+ ``MODE_IN_SESSION`` only for Claude host on opus; otherwise ``MODE_CHAIN``.
164
+ """
165
+ is_claude_host = host_profile == HOST_PROFILE_CLAUDE
166
+ if is_claude_host and is_opus_session_model(session_model):
167
+ return MODE_IN_SESSION
168
+ return MODE_CHAIN
169
+
170
+
171
+ def build_code_review_arguments() -> list[str]:
172
+ """Return the argv tokens passed to ``run_claude`` for a chain review.
173
+
174
+ ::
175
+
176
+ build_code_review_arguments()
177
+ # ok: ["-p", CODE_REVIEW_PROMPT, "--model", "opus", ...]
178
+
179
+ Returns:
180
+ Ordered claude CLI arguments for the headless opus review slash command.
181
+ """
182
+ return [
183
+ SINGLE_TURN_FLAG,
184
+ CODE_REVIEW_PROMPT,
185
+ MODEL_FLAG,
186
+ CODE_REVIEW_MODEL_ALIAS,
187
+ OUTPUT_FORMAT_FLAG,
188
+ OUTPUT_FORMAT_JSON,
189
+ PERMISSION_MODE_FLAG,
190
+ PERMISSION_MODE_BYPASS,
191
+ ]
192
+
193
+
194
+ def is_working_tree_dirty(working_directory: Path) -> bool:
195
+ """Return True when the tree is dirty or ``git status`` fails.
196
+
197
+ ::
198
+
199
+ is_working_tree_dirty(clean_repo) # ok: False
200
+ is_working_tree_dirty(dirty_repo) # ok: True
201
+ is_working_tree_dirty(broken_git) # ok: True (non-zero status)
202
+
203
+ A non-zero ``git status`` return code must not report clean: the caller
204
+ treats an unknown tree as dirty so a clean stamp cannot bypass the gate.
205
+
206
+ Args:
207
+ working_directory: Git working tree to inspect.
208
+
209
+ Returns:
210
+ True when porcelain output is non-empty, or when git status exits
211
+ non-zero.
212
+ """
213
+ completion = review_git_status_runner(
214
+ [GIT_BINARY, GIT_STATUS_SUBCOMMAND, GIT_PORCELAIN_FLAG],
215
+ cwd=str(working_directory),
216
+ capture_output=True,
217
+ text=True,
218
+ check=False,
219
+ )
220
+ if completion.returncode != 0:
221
+ return True
222
+ return bool(completion.stdout.strip())
223
+
224
+
225
+ def is_successful_code_review(review_outcome: CodeReviewOutcome) -> bool:
226
+ """Return True when the invocation completed a successful review serve.
227
+
228
+ ::
229
+
230
+ is_successful_code_review(in_session_ok) # ok: True
231
+ is_successful_code_review(chain_served) # ok: True
232
+ is_successful_code_review(chain_failed) # ok: False
233
+
234
+ Success requires ``returncode == 0``. Chain mode also requires a non-null
235
+ ``served_command``. In-session mode hands the slash command to the skill,
236
+ so ``served_command`` stays null by design.
237
+
238
+ Args:
239
+ review_outcome: Structured outcome from ``invoke_code_review``.
240
+
241
+ Returns:
242
+ True when the outcome is a successful serve that may stamp clean if
243
+ the working tree is also clean.
244
+ """
245
+ if review_outcome.returncode != SUCCESSFUL_REVIEW_RETURNCODE:
246
+ return False
247
+ if review_outcome.mode == MODE_CHAIN and review_outcome.served_command is None:
248
+ return False
249
+ return True
250
+
251
+
252
+ def is_code_review_clean_stamp_allowed(review_outcome: CodeReviewOutcome) -> bool:
253
+ """Return True when the outcome may set ``code_review_clean_at``.
254
+
255
+ ::
256
+
257
+ is_code_review_clean_stamp_allowed(chain_clean_ok) # ok: True
258
+ is_code_review_clean_stamp_allowed(chain_failed) # ok: False
259
+ is_code_review_clean_stamp_allowed(chain_dirty_ok) # ok: False
260
+
261
+ Clean stamp requires a successful serve and a clean working tree.
262
+ ``dirty_tree`` alone is not enough: a failed chain leaves the tree clean
263
+ and must stay in CODE_REVIEW.
264
+
265
+ Args:
266
+ review_outcome: Structured outcome from ``invoke_code_review``.
267
+
268
+ Returns:
269
+ True only when the review succeeded and ``is_dirty_tree`` is False.
270
+ """
271
+ if not is_successful_code_review(review_outcome):
272
+ return False
273
+ if review_outcome.is_dirty_tree:
274
+ return False
275
+ return True
276
+
277
+ def _run_claude_with_empty_stdin(
278
+ all_claude_arguments: list[str],
279
+ *,
280
+ timeout_seconds: int,
281
+ working_directory: Path,
282
+ ) -> ChainInvocationOutcome:
283
+ working_directory_path = str(working_directory)
284
+ with _CHAIN_RUNNER_LOCK:
285
+ previous_runner = chain_runner.chain_subprocess_runner
286
+
287
+ def _runner_with_empty_stdin(
288
+ all_invocation_tokens: Sequence[str],
289
+ *all_positionals: object,
290
+ **all_keywords: object,
291
+ ) -> subprocess.CompletedProcess[str]:
292
+ del all_positionals
293
+ maybe_timeout = all_keywords.get("timeout")
294
+ timeout_for_run: float | None
295
+ if isinstance(maybe_timeout, (int, float)):
296
+ timeout_for_run = float(maybe_timeout)
297
+ else:
298
+ timeout_for_run = None
299
+ completed_process: subprocess.CompletedProcess[str] = previous_runner(
300
+ all_invocation_tokens,
301
+ capture_output=True,
302
+ text=True,
303
+ timeout=timeout_for_run,
304
+ check=False,
305
+ stdin=subprocess.DEVNULL,
306
+ cwd=working_directory_path,
307
+ )
308
+ return completed_process
309
+
310
+ empty_stdin_runner: TextCapturingSubprocessRunner = _runner_with_empty_stdin
311
+ setattr(chain_runner, "chain_subprocess_runner", empty_stdin_runner)
312
+ try:
313
+ return review_claude_runner(
314
+ all_claude_arguments, timeout_seconds=timeout_seconds
315
+ )
316
+ finally:
317
+ setattr(chain_runner, "chain_subprocess_runner", previous_runner)
318
+
319
+
320
+ def _in_session_outcome() -> CodeReviewOutcome:
321
+ return CodeReviewOutcome(
322
+ mode=MODE_IN_SESSION,
323
+ served_command=None,
324
+ returncode=IN_SESSION_RETURNCODE,
325
+ is_dirty_tree=False,
326
+ )
327
+
328
+
329
+ def _chain_outcome(
330
+ chain_outcome: ChainInvocationOutcome,
331
+ *,
332
+ working_directory: Path,
333
+ ) -> CodeReviewOutcome:
334
+ return CodeReviewOutcome(
335
+ mode=MODE_CHAIN,
336
+ served_command=chain_outcome.served_command,
337
+ returncode=chain_outcome.returncode,
338
+ is_dirty_tree=is_working_tree_dirty(working_directory),
339
+ )
340
+
341
+
342
+ def _failure_code_review_outcome(returncode: int) -> CodeReviewOutcome:
343
+ return CodeReviewOutcome(
344
+ mode=MODE_CHAIN,
345
+ served_command=None,
346
+ returncode=returncode,
347
+ is_dirty_tree=False,
348
+ )
349
+
350
+ def invoke_code_review(
351
+ *,
352
+ working_directory: Path,
353
+ session_model: str,
354
+ timeout_seconds: int,
355
+ ) -> CodeReviewOutcome:
356
+ """Run or hand off ``/code-review`` based on host profile and session model.
357
+
358
+ Args:
359
+ working_directory: PR working tree used as cwd for the chain spawn.
360
+ session_model: Caller-stated session model short alias (for example
361
+ ``opus`` or ``sonnet``).
362
+ timeout_seconds: Timeout applied to each chain binary invocation.
363
+
364
+ Returns:
365
+ Structured outcome including mode, served binary, return code, and
366
+ whether the working tree is dirty after a chain run.
367
+ """
368
+ host_profile = review_host_profile_detector()
369
+ review_mode = decide_review_mode(
370
+ host_profile=host_profile,
371
+ session_model=session_model,
372
+ )
373
+ if review_mode == MODE_IN_SESSION:
374
+ return _in_session_outcome()
375
+ all_claude_arguments = build_code_review_arguments()
376
+ chain_outcome = _run_claude_with_empty_stdin(
377
+ all_claude_arguments,
378
+ timeout_seconds=timeout_seconds,
379
+ working_directory=working_directory,
380
+ )
381
+ return _chain_outcome(chain_outcome, working_directory=working_directory)
382
+
383
+
384
+ def encode_code_review_outcome(
385
+ review_outcome: CodeReviewOutcome,
386
+ ) -> dict[str, object]:
387
+ """Encode a code-review outcome as the JSON-serializable payload.
388
+
389
+ Args:
390
+ review_outcome: The invoker outcome to encode.
391
+
392
+ Returns:
393
+ A plain dict matching the CLI JSON shape.
394
+ """
395
+ return {
396
+ RESULT_KEY_MODE: review_outcome.mode,
397
+ RESULT_KEY_SERVED_COMMAND: review_outcome.served_command,
398
+ RESULT_KEY_RETURNCODE: review_outcome.returncode,
399
+ RESULT_KEY_DIRTY_TREE: review_outcome.is_dirty_tree,
400
+ }
401
+
402
+
403
+ def _build_argument_parser() -> argparse.ArgumentParser:
404
+ parser = argparse.ArgumentParser(
405
+ description=(
406
+ "Run built-in /code-review at high effort on opus, host-aware."
407
+ )
408
+ )
409
+ parser.add_argument(
410
+ CWD_FLAG,
411
+ dest="working_directory",
412
+ required=True,
413
+ type=Path,
414
+ help="PR working tree used as cwd for the chain spawn.",
415
+ )
416
+ parser.add_argument(
417
+ CLI_SESSION_MODEL_FLAG,
418
+ dest="session_model",
419
+ required=True,
420
+ help="Caller session model short alias (for example opus or sonnet).",
421
+ )
422
+ parser.add_argument(
423
+ CLI_TIMEOUT_FLAG,
424
+ dest="timeout_seconds",
425
+ type=int,
426
+ default=DEFAULT_CODE_REVIEW_TIMEOUT_SECONDS,
427
+ help="Timeout in seconds applied to each chain binary invocation.",
428
+ )
429
+ return parser
430
+
431
+
432
+ def main(all_command_arguments: list[str]) -> int:
433
+ """Run the invoker for CLI arguments and print the JSON outcome.
434
+
435
+ ``ChainConfigurationError`` and host ``ValueError`` still emit result JSON
436
+ on stdout (no traceback-only failure). The non-zero return code and null
437
+ ``served_command`` block a clean stamp.
438
+
439
+ Args:
440
+ all_command_arguments: The argument vector after the program name.
441
+
442
+ Returns:
443
+ The outcome return code (``0`` for in-session; chain return code otherwise).
444
+ """
445
+ parser = _build_argument_parser()
446
+ parsed_arguments = parser.parse_args(all_command_arguments)
447
+ try:
448
+ review_outcome = invoke_code_review(
449
+ working_directory=parsed_arguments.working_directory,
450
+ session_model=parsed_arguments.session_model,
451
+ timeout_seconds=parsed_arguments.timeout_seconds,
452
+ )
453
+ except ChainConfigurationError:
454
+ review_outcome = _failure_code_review_outcome(CHAIN_CONFIG_ERROR_EXIT_CODE)
455
+ except ValueError:
456
+ review_outcome = _failure_code_review_outcome(HOST_PROFILE_ERROR_RETURNCODE)
457
+ encoded_payload = encode_code_review_outcome(review_outcome)
458
+ sys.stdout.write(json.dumps(encoded_payload) + "\n")
459
+ return review_outcome.returncode
460
+
461
+
462
+ if __name__ == "__main__":
463
+ sys.exit(main(sys.argv[1:]))