claude-dev-env 1.95.0 → 2.0.1

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 (241) hide show
  1. package/_shared/advisor/CLAUDE.md +2 -2
  2. package/_shared/advisor/advisor-protocol.md +20 -20
  3. package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +15 -12
  4. package/_shared/advisor/scripts/model_tier_run_validator.py +11 -10
  5. package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +25 -19
  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/ever-shipped-skills.mjs +3 -0
  42. package/bin/expand_home_directory_tokens.mjs +1 -1
  43. package/bin/install.mjs +5 -2
  44. package/hooks/advisory/refactor_guard.py +3 -4
  45. package/hooks/blocking/CLAUDE.md +7 -1
  46. package/hooks/blocking/block_main_commit.py +2 -2
  47. package/hooks/blocking/claude_md_orphan_file_blocker.py +75 -699
  48. package/hooks/blocking/claude_md_orphan_file_blocker_parts/CLAUDE.md +28 -0
  49. package/hooks/blocking/claude_md_orphan_file_blocker_parts/__init__.py +1 -0
  50. package/hooks/blocking/claude_md_orphan_file_blocker_parts/config/__init__.py +1 -0
  51. package/hooks/blocking/claude_md_orphan_file_blocker_parts/config/orphan_blocker_constants.py +18 -0
  52. package/hooks/blocking/claude_md_orphan_file_blocker_parts/decision.py +81 -0
  53. package/hooks/blocking/claude_md_orphan_file_blocker_parts/references.py +307 -0
  54. package/hooks/blocking/claude_md_orphan_file_blocker_parts/scan_plan.py +124 -0
  55. package/hooks/blocking/claude_md_orphan_file_blocker_parts/subtree_scan.py +179 -0
  56. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/conftest.py +10 -0
  57. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_decision.py +34 -0
  58. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_references.py +42 -0
  59. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_scan_plan.py +27 -0
  60. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_subtree_scan.py +30 -0
  61. package/hooks/blocking/code_rules_boolean_mustcheck.py +1 -1
  62. package/hooks/blocking/code_rules_mock_completeness.py +1 -1
  63. package/hooks/blocking/code_rules_optional_params.py +2 -2
  64. package/hooks/blocking/code_rules_shared.py +1 -1
  65. package/hooks/blocking/code_rules_test_assertions.py +1 -1
  66. package/hooks/blocking/code_rules_typeddict_stub.py +1 -1
  67. package/hooks/blocking/gh_pr_author_enforcer.py +1 -1
  68. package/hooks/blocking/inventory_intent_records/CLAUDE.md +26 -0
  69. package/hooks/blocking/inventory_intent_records/__init__.py +1 -0
  70. package/hooks/blocking/inventory_intent_records/config/__init__.py +1 -0
  71. package/hooks/blocking/inventory_intent_records/config/intent_records_constants.py +20 -0
  72. package/hooks/blocking/inventory_intent_records/records.py +271 -0
  73. package/hooks/blocking/inventory_intent_records/tests/conftest.py +10 -0
  74. package/hooks/blocking/inventory_intent_records/tests/test_records.py +80 -0
  75. package/hooks/blocking/package_inventory_stale_blocker.py +54 -384
  76. package/hooks/blocking/package_inventory_stale_blocker_parts/CLAUDE.md +26 -0
  77. package/hooks/blocking/package_inventory_stale_blocker_parts/__init__.py +1 -0
  78. package/hooks/blocking/package_inventory_stale_blocker_parts/config/__init__.py +1 -0
  79. package/hooks/blocking/package_inventory_stale_blocker_parts/config/inventory_blocker_constants.py +16 -0
  80. package/hooks/blocking/package_inventory_stale_blocker_parts/decision.py +84 -0
  81. package/hooks/blocking/package_inventory_stale_blocker_parts/inventory_detection.py +307 -0
  82. package/hooks/blocking/package_inventory_stale_blocker_parts/tests/conftest.py +10 -0
  83. package/hooks/blocking/package_inventory_stale_blocker_parts/tests/test_decision.py +38 -0
  84. package/hooks/blocking/package_inventory_stale_blocker_parts/tests/test_inventory_detection.py +61 -0
  85. package/hooks/blocking/pii_payload_scan.py +138 -42
  86. package/hooks/blocking/pii_prevention_blocker.py +185 -291
  87. package/hooks/blocking/pii_prevention_blocker_parts/CLAUDE.md +24 -0
  88. package/hooks/blocking/pii_prevention_blocker_parts/__init__.py +1 -0
  89. package/hooks/blocking/pii_prevention_blocker_parts/config/__init__.py +1 -0
  90. package/hooks/blocking/pii_prevention_blocker_parts/config/repository_resolution_constants.py +28 -0
  91. package/hooks/blocking/pii_prevention_blocker_parts/repository_exemption.py +214 -0
  92. package/hooks/blocking/pii_prevention_blocker_parts/repository_resolution.py +208 -0
  93. package/hooks/blocking/pr_description_command_parser.py +8 -4
  94. package/hooks/blocking/precommit_code_rules_gate.py +3 -3
  95. package/hooks/blocking/tdd_enforcer.py +97 -608
  96. package/hooks/blocking/tdd_enforcer_parts/CLAUDE.md +30 -0
  97. package/hooks/blocking/tdd_enforcer_parts/__init__.py +1 -0
  98. package/hooks/blocking/tdd_enforcer_parts/candidate_paths.py +142 -0
  99. package/hooks/blocking/tdd_enforcer_parts/config/__init__.py +1 -0
  100. package/hooks/blocking/tdd_enforcer_parts/config/tdd_enforcer_constants.py +32 -0
  101. package/hooks/blocking/tdd_enforcer_parts/content_analysis.py +268 -0
  102. package/hooks/blocking/tdd_enforcer_parts/decisions.py +92 -0
  103. package/hooks/blocking/tdd_enforcer_parts/freshness.py +80 -0
  104. package/hooks/blocking/tdd_enforcer_parts/git_tracking.py +63 -0
  105. package/hooks/blocking/tdd_enforcer_parts/path_classification.py +119 -0
  106. package/hooks/blocking/tdd_enforcer_parts/tests/conftest.py +10 -0
  107. package/hooks/blocking/tdd_enforcer_parts/tests/test_candidate_paths.py +31 -0
  108. package/hooks/blocking/tdd_enforcer_parts/tests/test_content_analysis.py +30 -0
  109. package/hooks/blocking/tdd_enforcer_parts/tests/test_decisions.py +34 -0
  110. package/hooks/blocking/tdd_enforcer_parts/tests/test_freshness.py +28 -0
  111. package/hooks/blocking/tdd_enforcer_parts/tests/test_git_tracking.py +48 -0
  112. package/hooks/blocking/tdd_enforcer_parts/tests/test_path_classification.py +36 -0
  113. package/hooks/blocking/test_inventory_deadlock_resolution.py +154 -0
  114. package/hooks/blocking/test_pii_payload_scan.py +168 -0
  115. package/hooks/blocking/test_tdd_enforcer_restore.py +108 -0
  116. package/hooks/blocking/test_verifier_verdict_minter.py +55 -158
  117. package/hooks/blocking/tests/conftest.py +10 -0
  118. package/hooks/blocking/tests/test_pii_prevention_blocker.py +260 -0
  119. package/hooks/blocking/tests/test_repository_exemption.py +105 -0
  120. package/hooks/blocking/tests/test_repository_resolution.py +108 -0
  121. package/hooks/diagnostic/hook_log_extractor.py +12 -10
  122. package/hooks/git-hooks/post_commit.py +3 -4
  123. package/hooks/hooks_constants/CLAUDE.md +2 -2
  124. package/hooks/hooks_constants/banned_identifiers_constants.py +0 -1
  125. package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -1
  126. package/hooks/hooks_constants/local_identity.py +59 -8
  127. package/hooks/hooks_constants/pii_prevention_constants.py +0 -6
  128. package/hooks/hooks_constants/test_local_identity.py +105 -3
  129. package/hooks/pyproject.toml +13 -36
  130. package/hooks/session/plugin_data_dir_cleanup.py +0 -1
  131. package/hooks/validation/mypy_validator.py +2 -2
  132. package/hooks/validators/health_check.py +1 -0
  133. package/hooks/validators/mypy_integration.py +2 -0
  134. package/hooks/validators/ruff_integration.py +3 -0
  135. package/hooks/workflow/auto_formatter.py +5 -4
  136. package/package.json +1 -1
  137. package/scripts/CLAUDE.md +4 -0
  138. package/scripts/dev_env_scripts_constants/CLAUDE.md +6 -4
  139. package/scripts/dev_env_scripts_constants/code_review_constants.py +71 -0
  140. package/scripts/dev_env_scripts_constants/grok_worker_constants.py +435 -0
  141. package/scripts/dev_env_scripts_constants/timing.py +7 -1
  142. package/scripts/grok_headless_runner.py +294 -0
  143. package/scripts/grok_worker_preflight.py +410 -0
  144. package/scripts/invoke_code_review.py +463 -0
  145. package/scripts/resolve_worker_spawn.py +619 -0
  146. package/scripts/spawn_grok_batch.py +672 -0
  147. package/scripts/test_grok_headless_runner.py +626 -0
  148. package/scripts/test_grok_worker_preflight.py +1054 -0
  149. package/scripts/test_invoke_code_review.py +672 -0
  150. package/scripts/test_resolve_worker_spawn.py +1014 -0
  151. package/scripts/test_spawn_grok_batch.py +1017 -0
  152. package/skills/CLAUDE.md +5 -3
  153. package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +72 -13
  154. package/skills/_shared/pr-loop/scripts/build_fix_prompt.py +121 -14
  155. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +78 -0
  156. package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +121 -0
  157. package/skills/_shared/pr-loop/scripts/test_build_fix_prompt.py +196 -6
  158. package/skills/autoconverge/CLAUDE.md +3 -3
  159. package/skills/autoconverge/SKILL.md +9 -3
  160. package/skills/autoconverge/reference/CLAUDE.md +2 -2
  161. package/skills/autoconverge/reference/convergence.md +33 -11
  162. package/skills/autoconverge/reference/stop-conditions.md +16 -5
  163. package/skills/autoconverge/workflow/CLAUDE.md +2 -1
  164. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +7 -2
  165. package/skills/autoconverge/workflow/converge.codex-gate.test.mjs +300 -0
  166. package/skills/autoconverge/workflow/converge.contract.test.mjs +5 -5
  167. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +29 -29
  168. package/skills/autoconverge/workflow/converge.fix-progress.test.mjs +1 -1
  169. package/skills/autoconverge/workflow/converge.mjs +200 -16
  170. package/skills/bugteam/CLAUDE.md +2 -2
  171. package/skills/bugteam/CONSTRAINTS.md +3 -2
  172. package/skills/bugteam/PROMPTS.md +7 -6
  173. package/skills/bugteam/SKILL.md +18 -13
  174. package/skills/bugteam/reference/audit-and-teammates.md +215 -35
  175. package/skills/bugteam/reference/design-rationale.md +1 -1
  176. package/skills/bugteam/reference/obstacles/CLAUDE.md +1 -1
  177. package/skills/bugteam/reference/team-setup.md +8 -2
  178. package/skills/codex-review/CLAUDE.md +46 -0
  179. package/skills/codex-review/SKILL.md +181 -0
  180. package/skills/codex-review/reference/CLAUDE.md +15 -0
  181. package/skills/codex-review/reference/cli-contract.md +253 -0
  182. package/skills/codex-review/reference/loop-integration.md +118 -0
  183. package/skills/codex-review/scripts/codex_down_classifier.py +98 -0
  184. package/skills/codex-review/scripts/codex_review_scripts_constants/CLAUDE.md +18 -0
  185. package/skills/codex-review/scripts/codex_review_scripts_constants/__init__.py +1 -0
  186. package/skills/codex-review/scripts/codex_review_scripts_constants/classifier_constants.py +35 -0
  187. package/skills/codex-review/scripts/codex_review_scripts_constants/codex_usage_probe_constants.py +86 -0
  188. package/skills/codex-review/scripts/codex_review_scripts_constants/findings_constants.py +18 -0
  189. package/skills/codex-review/scripts/codex_review_scripts_constants/run_constants.py +45 -0
  190. package/skills/codex-review/scripts/codex_usage_probe.py +573 -0
  191. package/skills/codex-review/scripts/fixtures/auth_failure_synthetic.txt +1 -0
  192. package/skills/codex-review/scripts/fixtures/config_load_failure_v0.125.0.txt +1 -0
  193. package/skills/codex-review/scripts/fixtures/freeform_findings_v0.144.3.txt +6 -0
  194. package/skills/codex-review/scripts/fixtures/model_rejection_v0.125.0.jsonl +5 -0
  195. package/skills/codex-review/scripts/fixtures/structured_findings.txt +13 -0
  196. package/skills/codex-review/scripts/fixtures/success_stream_v0.144.3.jsonl +6 -0
  197. package/skills/codex-review/scripts/fixtures/unknown_failure_synthetic.txt +1 -0
  198. package/skills/codex-review/scripts/fixtures/usage_limit_synthetic.txt +1 -0
  199. package/skills/codex-review/scripts/parse_codex_findings.py +207 -0
  200. package/skills/codex-review/scripts/run_codex_review.py +415 -0
  201. package/skills/codex-review/scripts/test_codex_down_classifier.py +143 -0
  202. package/skills/codex-review/scripts/test_codex_usage_probe.py +678 -0
  203. package/skills/codex-review/scripts/test_parse_codex_findings.py +130 -0
  204. package/skills/codex-review/scripts/test_run_codex_review.py +812 -0
  205. package/skills/codex-review/test_skill_scaffold.py +192 -0
  206. package/skills/grok-spawn/CLAUDE.md +28 -0
  207. package/skills/grok-spawn/SKILL.md +226 -0
  208. package/skills/grok-spawn/reference/flag-profiles.md +132 -0
  209. package/skills/grok-spawn/reference/worker-briefs.md +152 -0
  210. package/skills/grokify/SKILL.md +9 -1
  211. package/skills/grokify/capability-claims.test.mjs +28 -0
  212. package/skills/grokify/evals/README.md +72 -0
  213. package/skills/grokify/evals/parse-payload.test.mjs +171 -0
  214. package/skills/grokify/evals/run-capability-evals.mjs +545 -0
  215. package/skills/orchestrator/SKILL.md +42 -29
  216. package/skills/orchestrator-refresh/SKILL.md +17 -9
  217. package/skills/pr-converge/SKILL.md +34 -13
  218. package/skills/pr-converge/reference/convergence-gates.md +42 -15
  219. package/skills/pr-converge/reference/fix-protocol.md +1 -1
  220. package/skills/pr-converge/reference/ground-rules.md +1 -1
  221. package/skills/pr-converge/reference/per-tick.md +130 -42
  222. package/skills/pr-converge/reference/state-schema.md +10 -0
  223. package/skills/pr-converge/scripts/CLAUDE.md +2 -0
  224. package/skills/pr-converge/scripts/_pr_converge_path_setup.py +5 -1
  225. package/skills/pr-converge/scripts/check_convergence.py +605 -29
  226. package/skills/pr-converge/scripts/check_convergence_availability.py +232 -0
  227. package/skills/pr-converge/scripts/check_convergence_gates.py +279 -235
  228. package/skills/pr-converge/scripts/check_convergence_thread_gates.py +1 -1
  229. package/skills/pr-converge/scripts/pr_converge_scripts_constants/convergence_gate_constants.py +36 -2
  230. package/skills/pr-converge/scripts/test__pr_converge_path_setup.py +4 -0
  231. package/skills/pr-converge/scripts/test_check_convergence.py +71 -3
  232. package/skills/pr-converge/scripts/test_check_convergence_availability.py +326 -0
  233. package/skills/pr-converge/scripts/test_check_convergence_codex.py +507 -0
  234. package/skills/pr-converge/scripts/test_check_convergence_contract.py +89 -17
  235. package/skills/pr-converge/scripts/test_check_convergence_fixture.py +179 -0
  236. package/skills/pr-converge/scripts/test_check_convergence_gates.py +84 -68
  237. package/skills/pr-converge/scripts/test_check_convergence_thread_gates.py +24 -0
  238. package/skills/pr-converge/test_step5_host_branch.py +106 -0
  239. package/skills/pr-loop-cloud-transport/SKILL.md +2 -0
  240. package/skills/reviewer-gates/SKILL.md +7 -5
  241. package/skills/team-advisor/SKILL.md +7 -7
@@ -7,10 +7,11 @@
7
7
  enabled lists bugbot, disabled bugbot -> bugbot flag: off (opt-out wins)
8
8
  disabled empty -> bugteam ok: runs
9
9
  disabled lists copilot -> copilot flag: off
10
+ disabled lists codex -> codex flag: off
10
11
 
11
12
  Bugbot is off by default and runs only when the enabled list names it.
12
- Bugteam and copilot run by default and stop only when the disabled list
13
- names them; both lists parse case-insensitively and tolerate whitespace.
13
+ Bugteam, copilot, and codex run by default and stop only when the disabled
14
+ list names them; both lists parse case-insensitively and tolerate whitespace.
14
15
  """
15
16
 
16
17
  from __future__ import annotations
@@ -22,6 +23,7 @@ import sys
22
23
  from pr_loop_shared_constants.reviews_disabled_constants import (
23
24
  CLAUDE_REVIEWS_DISABLED_BUGBOT_TOKEN,
24
25
  CLAUDE_REVIEWS_DISABLED_BUGTEAM_TOKEN,
26
+ CLAUDE_REVIEWS_DISABLED_CODEX_TOKEN,
25
27
  CLAUDE_REVIEWS_DISABLED_COPILOT_TOKEN,
26
28
  CLAUDE_REVIEWS_DISABLED_ENV_VAR_NAME,
27
29
  CLAUDE_REVIEWS_DISABLED_TOKEN_SEPARATOR,
@@ -33,6 +35,7 @@ from pr_loop_shared_constants.reviews_disabled_constants import (
33
35
  __all__ = [
34
36
  "CLAUDE_REVIEWS_DISABLED_BUGBOT_TOKEN",
35
37
  "CLAUDE_REVIEWS_DISABLED_BUGTEAM_TOKEN",
38
+ "CLAUDE_REVIEWS_DISABLED_CODEX_TOKEN",
36
39
  "CLAUDE_REVIEWS_DISABLED_COPILOT_TOKEN",
37
40
  "CLAUDE_REVIEWS_DISABLED_ENV_VAR_NAME",
38
41
  "CLAUDE_REVIEWS_DISABLED_TOKEN_SEPARATOR",
@@ -41,6 +44,7 @@ __all__ = [
41
44
  "is_bugbot_disabled_via_env",
42
45
  "is_bugbot_opted_out_via_env",
43
46
  "is_bugteam_disabled_via_env",
47
+ "is_codex_disabled_via_env",
44
48
  "is_copilot_disabled_via_env",
45
49
  "main",
46
50
  ]
@@ -129,6 +133,17 @@ def is_copilot_disabled_via_env() -> bool:
129
133
  )
130
134
 
131
135
 
136
+ def is_codex_disabled_via_env() -> bool:
137
+ """Check whether CLAUDE_REVIEWS_DISABLED opts the Codex reviewer out.
138
+
139
+ Returns:
140
+ True when the env var lists the ``codex`` token.
141
+ """
142
+ return _is_reviewer_listed_in_env(
143
+ CLAUDE_REVIEWS_DISABLED_ENV_VAR_NAME, CLAUDE_REVIEWS_DISABLED_CODEX_TOKEN
144
+ )
145
+
146
+
132
147
  def parse_arguments(all_argv: list[str]) -> argparse.Namespace:
133
148
  """Parse command-line arguments for the reviewer opt-out and opt-in gate check.
134
149
 
@@ -148,6 +163,7 @@ def parse_arguments(all_argv: list[str]) -> argparse.Namespace:
148
163
  CLAUDE_REVIEWS_DISABLED_BUGBOT_TOKEN,
149
164
  CLAUDE_REVIEWS_DISABLED_BUGTEAM_TOKEN,
150
165
  CLAUDE_REVIEWS_DISABLED_COPILOT_TOKEN,
166
+ CLAUDE_REVIEWS_DISABLED_CODEX_TOKEN,
151
167
  ],
152
168
  help="Reviewer token to test against the CLAUDE_REVIEWS_DISABLED / CLAUDE_REVIEWS_ENABLED gates",
153
169
  )
@@ -168,6 +184,7 @@ def main(all_arguments: list[str]) -> int:
168
184
  CLAUDE_REVIEWS_DISABLED_BUGBOT_TOKEN: is_bugbot_disabled_via_env,
169
185
  CLAUDE_REVIEWS_DISABLED_BUGTEAM_TOKEN: is_bugteam_disabled_via_env,
170
186
  CLAUDE_REVIEWS_DISABLED_COPILOT_TOKEN: is_copilot_disabled_via_env,
187
+ CLAUDE_REVIEWS_DISABLED_CODEX_TOKEN: is_codex_disabled_via_env,
171
188
  }
172
189
  return 0 if disabled_checker_by_reviewer[arguments.reviewer]() else 1
173
190
 
@@ -0,0 +1,278 @@
1
+ """Behavioral tests for the gate's empty-file-set loudness and inspected-count report.
2
+
3
+ Covers the three acceptance criteria of the loud-empty change:
4
+ - A run whose resolved file set is empty exits non-zero and says so; a set
5
+ emptied only by the ``--only-under`` scope exits clean with the loud count.
6
+ - A run over new, untracked modules inspects them.
7
+ - Every run reports how many files it inspected.
8
+ """
9
+
10
+ import importlib.util
11
+ import os
12
+ import subprocess
13
+ from pathlib import Path
14
+ from types import ModuleType
15
+
16
+ import pytest
17
+
18
+
19
+ def _load_gate_module() -> ModuleType:
20
+ module_path = Path(__file__).parent / "code_rules_gate.py"
21
+ spec = importlib.util.spec_from_file_location(
22
+ "code_rules_gate_empty_set_under_test", module_path
23
+ )
24
+ assert spec is not None
25
+ assert spec.loader is not None
26
+ module = importlib.util.module_from_spec(spec)
27
+ spec.loader.exec_module(module)
28
+ return module
29
+
30
+
31
+ gate_module = _load_gate_module()
32
+
33
+
34
+ def _control_violation_module_content() -> str:
35
+ """Return module content the enforcer must block.
36
+
37
+ The control violation is a formatted-string logging call. The source
38
+ fragment is assembled from pieces so this test file itself does not
39
+ carry the flagged pattern.
40
+ """
41
+ formatted_call = "logger.info(" + 'f"announcing {name}")'
42
+ return (
43
+ '"""Fresh module with a logging violation."""\n'
44
+ "import logging\n\n"
45
+ 'logger = logging.getLogger("fresh_violation")\n\n\n'
46
+ "def announce(name: str) -> None:\n"
47
+ f" {formatted_call}\n"
48
+ )
49
+
50
+
51
+ def run_git_in_repository(repository_root: Path, *arguments: str) -> str:
52
+ completion = subprocess.run(
53
+ ["git", *arguments],
54
+ cwd=str(repository_root),
55
+ capture_output=True,
56
+ text=True,
57
+ encoding="utf-8",
58
+ errors="replace",
59
+ check=True,
60
+ env=gate_module.repository_environment(),
61
+ )
62
+ return completion.stdout
63
+
64
+
65
+ def initialize_git_repository(repository_root: Path) -> None:
66
+ run_git_in_repository(repository_root, "init", "--initial-branch=main")
67
+ run_git_in_repository(repository_root, "config", "user.email", "test@example.com")
68
+ run_git_in_repository(repository_root, "config", "user.name", "Test")
69
+ run_git_in_repository(repository_root, "config", "commit.gpgsign", "false")
70
+ disabled_hooks_directory = repository_root / "disabled-git-hooks"
71
+ disabled_hooks_directory.mkdir()
72
+ run_git_in_repository(
73
+ repository_root, "config", "core.hooksPath", str(disabled_hooks_directory)
74
+ )
75
+
76
+
77
+ def commit_all_files(repository_root: Path, commit_message: str) -> None:
78
+ run_git_in_repository(repository_root, "add", "-A")
79
+ run_git_in_repository(repository_root, "commit", "-m", commit_message)
80
+
81
+
82
+ @pytest.fixture(autouse=True)
83
+ def isolated_git_environment(monkeypatch: pytest.MonkeyPatch) -> None:
84
+ for each_variable_name in list(os.environ):
85
+ if each_variable_name.startswith("GIT_"):
86
+ monkeypatch.delenv(each_variable_name, raising=False)
87
+
88
+
89
+ @pytest.fixture()
90
+ def temporary_git_repository(tmp_path: Path) -> Path:
91
+ repository_root = tmp_path / "repository_under_test"
92
+ repository_root.mkdir()
93
+ initialize_git_repository(repository_root)
94
+ (repository_root / "seed.txt").write_text("seed\n", encoding="utf-8")
95
+ commit_all_files(repository_root, "seed commit")
96
+ return repository_root
97
+
98
+
99
+ def test_diff_mode_with_empty_file_set_exits_non_zero_and_says_so(
100
+ temporary_git_repository: Path,
101
+ monkeypatch: pytest.MonkeyPatch,
102
+ capsys: pytest.CaptureFixture[str],
103
+ ) -> None:
104
+ """HEAD == base and nothing untracked: the gate refuses to vouch for nothing.
105
+
106
+ A diff that resolves zero candidate files means the gate inspected
107
+ nothing — a bad merge base or a wrong directory looks exactly like this.
108
+ Issue #62's contract: that run exits non-zero and says so, because a
109
+ silent pass over zero files is trusted like a real pass.
110
+ """
111
+ monkeypatch.chdir(temporary_git_repository)
112
+
113
+ exit_code = gate_module.main(["--base", "HEAD"])
114
+
115
+ captured = capsys.readouterr()
116
+ assert exit_code != 0
117
+ assert "inspected 0 file(s)" in captured.err
118
+ assert "empty" in captured.err.lower()
119
+
120
+
121
+ def test_diff_mode_changes_outside_only_under_prefixes_exit_clean_and_report(
122
+ temporary_git_repository: Path,
123
+ monkeypatch: pytest.MonkeyPatch,
124
+ capsys: pytest.CaptureFixture[str],
125
+ ) -> None:
126
+ """Changes exist but none under the given prefixes: a scoped no-op exits 0.
127
+
128
+ The candidate set is non-empty, so the gate's wiring is proven live; the
129
+ ``--only-under`` scope legitimately excludes every candidate. That run
130
+ reports the zero count loudly and exits clean, so scoped gate loops keep
131
+ converging on branches whose changes sit outside the scoped tree.
132
+ """
133
+ (temporary_git_repository / "notes.txt").write_text("note\n", encoding="utf-8")
134
+ monkeypatch.chdir(temporary_git_repository)
135
+
136
+ exit_code = gate_module.main(["--base", "HEAD", "--only-under", "src"])
137
+
138
+ captured = capsys.readouterr()
139
+ assert exit_code == 0
140
+ assert "inspected 0 file(s)" in captured.err
141
+
142
+
143
+ def test_diff_mode_inspects_untracked_module_and_reports_count(
144
+ temporary_git_repository: Path,
145
+ monkeypatch: pytest.MonkeyPatch,
146
+ capsys: pytest.CaptureFixture[str],
147
+ ) -> None:
148
+ """A brand-new untracked module joins the file set and is validated."""
149
+ untracked_module = temporary_git_repository / "fresh_module.py"
150
+ untracked_module.write_text(
151
+ '"""Fresh module."""\n\n\ndef greet() -> str:\n return "hello"\n',
152
+ encoding="utf-8",
153
+ )
154
+ monkeypatch.chdir(temporary_git_repository)
155
+
156
+ exit_code = gate_module.main(["--base", "HEAD"])
157
+
158
+ captured = capsys.readouterr()
159
+ assert "inspected 1 file(s)" in captured.err
160
+ assert exit_code == 0
161
+
162
+
163
+ def test_diff_mode_blocks_violation_in_untracked_module(
164
+ temporary_git_repository: Path,
165
+ monkeypatch: pytest.MonkeyPatch,
166
+ capsys: pytest.CaptureFixture[str],
167
+ ) -> None:
168
+ """An untracked module whose content the enforcer rejects fails the gate."""
169
+ untracked_module = temporary_git_repository / "fresh_violation.py"
170
+ untracked_module.write_text(
171
+ _control_violation_module_content(),
172
+ encoding="utf-8",
173
+ )
174
+ monkeypatch.chdir(temporary_git_repository)
175
+
176
+ exit_code = gate_module.main(["--base", "HEAD"])
177
+
178
+ captured = capsys.readouterr()
179
+ assert exit_code != 0
180
+ assert "inspected 1 file(s)" in captured.err
181
+ assert "fresh_violation.py" in captured.err
182
+
183
+
184
+ def test_explicit_paths_run_reports_inspected_count(
185
+ temporary_git_repository: Path,
186
+ monkeypatch: pytest.MonkeyPatch,
187
+ capsys: pytest.CaptureFixture[str],
188
+ ) -> None:
189
+ """The count line appears on explicit-path runs too, so a caller always sees it."""
190
+ module_path = temporary_git_repository / "explicit_module.py"
191
+ module_path.write_text(
192
+ '"""Explicit module."""\n\n\ndef ping() -> str:\n return "pong"\n',
193
+ encoding="utf-8",
194
+ )
195
+ commit_all_files(temporary_git_repository, "add explicit module")
196
+ monkeypatch.chdir(temporary_git_repository)
197
+
198
+ exit_code = gate_module.main(["explicit_module.py"])
199
+
200
+ captured = capsys.readouterr()
201
+ assert exit_code == 0
202
+ assert "inspected 1 file(s)" in captured.err
203
+
204
+
205
+ def test_staged_mode_with_nothing_staged_reports_zero_inspected(
206
+ temporary_git_repository: Path,
207
+ monkeypatch: pytest.MonkeyPatch,
208
+ capsys: pytest.CaptureFixture[str],
209
+ ) -> None:
210
+ """Staged mode keeps its exit contract for an empty commit but names the count."""
211
+ monkeypatch.chdir(temporary_git_repository)
212
+
213
+ exit_code = gate_module.main(["--staged"])
214
+
215
+ captured = capsys.readouterr()
216
+ assert exit_code == 0
217
+ assert "inspected 0 file(s)" in captured.err
218
+
219
+
220
+ def test_untracked_paths_helper_lists_only_untracked_files(
221
+ temporary_git_repository: Path,
222
+ ) -> None:
223
+ tracked_path = temporary_git_repository / "tracked.py"
224
+ tracked_path.write_text('"""Tracked."""\n', encoding="utf-8")
225
+ commit_all_files(temporary_git_repository, "add tracked module")
226
+ untracked_path = temporary_git_repository / "untracked.py"
227
+ untracked_path.write_text('"""Untracked."""\n', encoding="utf-8")
228
+
229
+ resolved_paths = gate_module.paths_from_git_untracked(temporary_git_repository)
230
+
231
+ resolved_names = {each_path.name for each_path in resolved_paths}
232
+ assert "untracked.py" in resolved_names
233
+ assert "tracked.py" not in resolved_names
234
+
235
+
236
+ def _gate_import_names_in_source(module_source: str) -> set[str]:
237
+ """Collect the names a source file imports from ``code_rules_gate``.
238
+
239
+ Args:
240
+ module_source: Python source text to scan for the gate import block.
241
+
242
+ Returns:
243
+ The imported-name set from the ``from code_rules_gate import (...)``
244
+ statement in the source, empty when no such block exists.
245
+ """
246
+ _, _, after_open = module_source.partition("from code_rules_gate import (")
247
+ import_block, _, _ = after_open.partition(")")
248
+ return {
249
+ each_line.strip().rstrip(",")
250
+ for each_line in import_block.splitlines()
251
+ if each_line.strip() and not each_line.strip().startswith("#")
252
+ }
253
+
254
+
255
+ def test_entry_exposes_every_symbol_the_preflight_hook_imports() -> None:
256
+ """The gate entry re-exports the whole surface the spawn-preflight hook uses.
257
+
258
+ The code-verifier spawn-preflight hook imports gate helpers by name at
259
+ runtime; a name the entry drops breaks that hook's import and silently
260
+ turns its deny paths into allows.
261
+ """
262
+ hook_path = (
263
+ Path(__file__).resolve().parents[3]
264
+ / "hooks"
265
+ / "blocking"
266
+ / "code_verifier_spawn_preflight_gate.py"
267
+ )
268
+ hook_imported_names = _gate_import_names_in_source(
269
+ hook_path.read_text(encoding="utf-8")
270
+ )
271
+
272
+ assert hook_imported_names, "hook no longer imports from code_rules_gate"
273
+ missing_names = sorted(
274
+ each_name
275
+ for each_name in hook_imported_names
276
+ if not hasattr(gate_module, each_name)
277
+ )
278
+ assert not missing_names, f"gate entry drops hook imports: {missing_names}"
@@ -7,9 +7,7 @@ from types import ModuleType
7
7
 
8
8
  def _load_constants_module() -> ModuleType:
9
9
  module_path = (
10
- Path(__file__).parent.parent
11
- / "pr_loop_shared_constants"
12
- / "code_rules_gate_constants.py"
10
+ Path(__file__).parent.parent / "pr_loop_shared_constants" / "code_rules_gate_constants.py"
13
11
  )
14
12
  specification = importlib.util.spec_from_file_location(
15
13
  "pr_loop_shared_constants.code_rules_gate_constants", module_path
@@ -29,29 +27,14 @@ def test_max_violations_per_check_is_typed_integer() -> None:
29
27
  assert constants_module.MAX_VIOLATIONS_PER_CHECK == 3
30
28
 
31
29
 
32
- def test_expected_tuple_pair_length_is_typed_integer() -> None:
33
- assert isinstance(constants_module.EXPECTED_TUPLE_PAIR_LENGTH, int)
34
- assert constants_module.EXPECTED_TUPLE_PAIR_LENGTH == 2
35
-
36
-
37
30
  def test_all_code_file_extensions_is_frozenset() -> None:
38
31
  assert isinstance(constants_module.ALL_CODE_FILE_EXTENSIONS, frozenset)
39
- assert constants_module.ALL_CODE_FILE_EXTENSIONS == frozenset(
40
- {".py", ".js", ".ts", ".tsx", ".jsx"}
41
- )
42
-
43
-
44
- def test_all_literal_keyword_exemptions_is_frozenset() -> None:
45
- assert isinstance(constants_module.ALL_LITERAL_KEYWORD_EXEMPTIONS, frozenset)
46
- assert constants_module.ALL_LITERAL_KEYWORD_EXEMPTIONS == frozenset(
47
- {"true", "false", "none", "null"}
32
+ assert (
33
+ frozenset({".py", ".js", ".ts", ".tsx", ".jsx"})
34
+ == constants_module.ALL_CODE_FILE_EXTENSIONS
48
35
  )
49
36
 
50
37
 
51
- def test_config_path_segment() -> None:
52
- assert constants_module.CONFIG_PATH_SEGMENT == "/config/"
53
-
54
-
55
38
  def test_tests_path_segment() -> None:
56
39
  assert constants_module.TESTS_PATH_SEGMENT == "/tests/"
57
40
 
@@ -73,10 +56,6 @@ def test_test_filename_prefix() -> None:
73
56
  assert constants_module.TEST_FILENAME_PREFIX == "test_"
74
57
 
75
58
 
76
- def test_minimum_column_name_length_after_first_char() -> None:
77
- assert constants_module.MINIMUM_COLUMN_NAME_LENGTH_AFTER_FIRST_CHAR == 2
78
-
79
-
80
59
  def test_git_name_status_added_prefix() -> None:
81
60
  assert constants_module.GIT_NAME_STATUS_ADDED_PREFIX == "A"
82
61
 
@@ -89,17 +68,8 @@ def test_expected_rename_column_count() -> None:
89
68
  assert constants_module.EXPECTED_RENAME_COLUMN_COUNT == 3
90
69
 
91
70
 
92
- def test_column_key_pattern_template_renders_with_minimum_length() -> None:
93
- rendered_pattern = constants_module.COLUMN_KEY_PATTERN_TEMPLATE.format(
94
- minimum_length=constants_module.MINIMUM_COLUMN_NAME_LENGTH_AFTER_FIRST_CHAR
95
- )
96
- assert rendered_pattern == r"^[a-z][a-z0-9_]{2,}$"
97
-
98
-
99
71
  def test_git_diff_name_only_null_terminated_command_prefix_includes_dash_z() -> None:
100
- command_prefix = (
101
- constants_module.ALL_GIT_DIFF_NAME_ONLY_NULL_TERMINATED_COMMAND_PREFIX
102
- )
72
+ command_prefix = constants_module.ALL_GIT_DIFF_NAME_ONLY_NULL_TERMINATED_COMMAND_PREFIX
103
73
  assert command_prefix == ("git", "diff", "--name-only", "-z")
104
74
 
105
75
 
@@ -110,10 +80,7 @@ def test_banned_noun_span_pattern_extracts_definition_line_and_span() -> None:
110
80
  )
111
81
  match = constants_module.BANNED_NOUN_VIOLATION_PATTERN.search(message)
112
82
  assert match is not None
113
- definition_line = int(
114
- match.group(constants_module.BANNED_NOUN_DEFINITION_LINE_GROUP_INDEX)
115
- )
83
+ definition_line = int(match.group(constants_module.BANNED_NOUN_DEFINITION_LINE_GROUP_INDEX))
116
84
  line_span = int(match.group(constants_module.BANNED_NOUN_SPAN_GROUP_INDEX))
117
85
  assert definition_line == 1
118
86
  assert line_span == 3
119
-
@@ -143,6 +143,35 @@ def test_is_copilot_disabled_via_env_true_when_listed_among_other_tokens(
143
143
  assert reviews_disabled.is_bugbot_disabled_via_env() is True
144
144
 
145
145
 
146
+ def test_is_codex_disabled_via_env_returns_true_when_env_lists_codex(
147
+ monkeypatch: pytest.MonkeyPatch,
148
+ ) -> None:
149
+ monkeypatch.setenv("CLAUDE_REVIEWS_DISABLED", "codex")
150
+ assert reviews_disabled.is_codex_disabled_via_env() is True
151
+
152
+
153
+ def test_is_codex_disabled_via_env_returns_false_when_env_is_empty(
154
+ monkeypatch: pytest.MonkeyPatch,
155
+ ) -> None:
156
+ monkeypatch.delenv("CLAUDE_REVIEWS_DISABLED", raising=False)
157
+ assert reviews_disabled.is_codex_disabled_via_env() is False
158
+
159
+
160
+ def test_is_codex_disabled_via_env_returns_false_when_only_bugbot_listed(
161
+ monkeypatch: pytest.MonkeyPatch,
162
+ ) -> None:
163
+ monkeypatch.setenv("CLAUDE_REVIEWS_DISABLED", "bugbot")
164
+ assert reviews_disabled.is_codex_disabled_via_env() is False
165
+
166
+
167
+ def test_is_codex_disabled_via_env_true_when_listed_among_other_tokens(
168
+ monkeypatch: pytest.MonkeyPatch,
169
+ ) -> None:
170
+ monkeypatch.setenv("CLAUDE_REVIEWS_DISABLED", " BugBot , CoDex ")
171
+ assert reviews_disabled.is_codex_disabled_via_env() is True
172
+ assert reviews_disabled.is_bugbot_disabled_via_env() is True
173
+
174
+
146
175
  def test_cli_main_returns_zero_when_named_reviewer_disabled(
147
176
  monkeypatch: pytest.MonkeyPatch,
148
177
  ) -> None:
@@ -185,3 +214,17 @@ def test_cli_main_returns_one_for_copilot_when_only_bugbot_disabled(
185
214
  ) -> None:
186
215
  monkeypatch.setenv("CLAUDE_REVIEWS_DISABLED", "bugbot")
187
216
  assert reviews_disabled.main(["--reviewer", "copilot"]) == 1
217
+
218
+
219
+ def test_cli_main_supports_codex_reviewer(
220
+ monkeypatch: pytest.MonkeyPatch,
221
+ ) -> None:
222
+ monkeypatch.setenv("CLAUDE_REVIEWS_DISABLED", "codex")
223
+ assert reviews_disabled.main(["--reviewer", "codex"]) == 0
224
+
225
+
226
+ def test_cli_main_returns_one_for_codex_when_only_bugbot_disabled(
227
+ monkeypatch: pytest.MonkeyPatch,
228
+ ) -> None:
229
+ monkeypatch.setenv("CLAUDE_REVIEWS_DISABLED", "bugbot")
230
+ assert reviews_disabled.main(["--reviewer", "codex"]) == 1
@@ -0,0 +1,186 @@
1
+ # Worker-spawn protocol
2
+
3
+ Single home for the worker-spawn dispatcher contract and for Claude-only
4
+ slash-step host routing. Skills that need a role-bound headless worker call
5
+ [`scripts/resolve_worker_spawn.py`](../../scripts/resolve_worker_spawn.py)
6
+ (or import `resolve_worker_spawn`) and read this page for the tier walk, inputs,
7
+ JSON result shape, host rules, and setup. Skills that run a Claude-only slash
8
+ step (for example `/code-review`) read the code-review host-routing section.
9
+
10
+ Composed helpers (preflight, headless runners, and the code-review invoker)
11
+ stay documented at their own modules; this page states how callers use them.
12
+
13
+ ## Three tiers
14
+
15
+ Walk order is fixed: soft-gate preflight for tier 1, then tier 1, then host
16
+ branching for tiers 2 and 3.
17
+
18
+ | Tier | Name | Who runs it |
19
+ |---|---|---|
20
+ | 1 | Grok headless | Python helper: `grok_worker_preflight` then `grok_headless_runner.run_headless_worker` |
21
+ | 2 | `claude_agent_required` | **Not** the Python helper. A stop signal so the **calling skill / harness** runs the Claude Code Agent tool itself |
22
+ | 3 | Claude headless | Python helper: `claude_chain_runner.run_claude` with `-p --output-format json --agent <stem>`, prompt body on stdin from the prompt file, and subprocess `cwd` set to the caller's working directory |
23
+
24
+ ### `claude_agent_required` boundary
25
+
26
+ A Python helper cannot spawn Claude Code Agent-tool subagents. On a **Claude**
27
+ host, when the claude headless tier is **not** enabled (`--enable-claude-tier`
28
+ absent / `is_claude_tier_enabled=False`), a grok fallthrough records tier 2 with
29
+ reason `claude_agent_required`, leaves `tier_used` null, and returns. The
30
+ calling skill must then run the Agent tool in-process.
31
+
32
+ On a **ThirdParty** host, or when the claude tier is enabled on a Claude host,
33
+ the dispatcher continues to tier 3.
34
+
35
+ ## Tier-1 fleets via grok-spawn
36
+
37
+ For a fleet of tier-1 grok workers (batch spec in, reports out), use the
38
+ **grok-spawn** skill when that skill is installed on the host (it ships on a
39
+ sibling stack and is not part of this dispatcher page). When present, that
40
+ skill owns the batch playbook and flag profiles under its reference docs.
41
+ This page stays the single-worker dispatcher contract (`resolve_worker_spawn`).
42
+
43
+ The calling session owns verification and every commit, push, or GitHub post.
44
+ Workers never commit or post.
45
+
46
+ ## Required inputs
47
+
48
+ ### CLI
49
+
50
+ ```
51
+ python resolve_worker_spawn.py \
52
+ --role <role> \
53
+ --prompt-file <path> \
54
+ --cwd <dir> \
55
+ --run-temp-dir <dir> \
56
+ [--timeout-seconds N] \
57
+ [--enable-claude-tier]
58
+ ```
59
+
60
+ | Flag | Required | Meaning |
61
+ |---|---|---|
62
+ | `--role` | no (default `bugteam`) | Role name for preflight agent-set checks; mapped to the primary agent stem for headless `--agent` (for example `bugteam` → `code-quality-agent`, `clean-coder` → `clean-coder`; unknown roles fall back to the raw role string) |
63
+ | `--prompt-file` | yes | Prompt file path for headless workers |
64
+ | `--cwd` | yes | Working directory for headless grok and claude workers |
65
+ | `--run-temp-dir` | yes | Run-scoped directory for leader sockets and preflight cache (created if missing) |
66
+ | `--timeout-seconds` | no (default `600`) | Per-tier timeout in seconds |
67
+ | `--enable-claude-tier` | no | Allow tier 3 on a Claude host |
68
+
69
+ There is no separate outcome-file flag. The result surface is **stdout JSON**
70
+ plus the process exit code.
71
+
72
+ ### Import API
73
+
74
+ `resolve_worker_spawn(...)` takes keyword-only arguments:
75
+ `role`, `prompt_file`, `working_directory`, `timeout_seconds`,
76
+ `is_claude_tier_enabled`, `run_state_directory`, `max_turns`.
77
+ `encode_spawn_outcome` turns a `SpawnOutcome` into the same JSON shape the CLI
78
+ prints.
79
+
80
+ ## JSON result shape
81
+
82
+ Stdout is one JSON object:
83
+
84
+ ```json
85
+ {
86
+ "tier_used": 1,
87
+ "ok": true,
88
+ "attempts": [
89
+ {"tier": 1, "ok": true, "reason": null}
90
+ ],
91
+ "output": "<serving tier stdout>",
92
+ "returncode": 0
93
+ }
94
+ ```
95
+
96
+ | Key | Meaning |
97
+ |---|---|
98
+ | `tier_used` | Serving tier number (`1` or `3`), or `null` when no tier served |
99
+ | `ok` | Whether the serving call completed successfully |
100
+ | `attempts` | Ordered list of tries: each `{tier, ok, reason}` |
101
+ | `output` | Captured stdout from the serving tier (empty when none) |
102
+ | `returncode` | Process return code from the last tier run (or config-error code) |
103
+
104
+ `attempts` is in tried order. A Claude-host stop records tier `2` with
105
+ `reason: "claude_agent_required"`. Preflight or grok fallthrough reasons use the
106
+ preflight/runner classification strings (for example `grok_binary_missing`,
107
+ `usage_limit`).
108
+
109
+ ### Exit codes
110
+
111
+ | Code | Meaning |
112
+ |---|---|
113
+ | `0` | A tier served (`tier_used` is not null) |
114
+ | `2` | Exhausted — no tier served |
115
+ | `3` | Config error: the `--prompt-file` path is not an existing file (`output` = `prompt_file_missing`) or claude chain configuration missing or invalid |
116
+
117
+ ## Host detection
118
+
119
+ After a grok fallthrough the dispatcher calls `detect_host_profile()` from
120
+ `tier_model_ids` (advisor scripts). Detection order:
121
+
122
+ 1. `ADVISOR_HOST_PROFILE` when set — must be `Claude` or `ThirdParty` (any letter case).
123
+ 2. `THIRD_PARTY` when truthy — `1`, `true`, `yes`, or `on`.
124
+ 3. Default: `Claude`.
125
+
126
+ Profiles are the real names `Claude` and `ThirdParty` (not a separate Grok host
127
+ profile). Grok is tier 1 on every host; host profile only chooses the post-grok
128
+ branch (Agent-tool handoff vs headless claude chain).
129
+
130
+ ## Code-review host routing
131
+
132
+ Claude-only slash steps need a Claude Code harness. The built-in
133
+ `/code-review` command is the main case. The host-routing pair is:
134
+
135
+ - `detect_host_profile()` from `tier_model_ids` — same host detection as the
136
+ worker-spawn dispatcher above
137
+ - `invoke_code_review` — host-aware invoker for `/code-review` (lives next to
138
+ `resolve_worker_spawn` under the package scripts tree)
139
+
140
+ Mode decision:
141
+
142
+ | Host profile | Session model | Mode | Who runs `/code-review` |
143
+ |---|---|---|---|
144
+ | `Claude` | `opus` | `in_session` | The calling skill runs the slash command in-process |
145
+ | `Claude` | any other alias | `chain` | Headless spawn through `claude_chain_runner` |
146
+ | `ThirdParty` | any | `chain` | Headless spawn through `claude_chain_runner` |
147
+
148
+ The review always runs at effort **high** on model **opus**. Chain mode builds
149
+ a single-turn prompt `/code-review high --fix`, pins `--model opus`, sets
150
+ subprocess `cwd` to the PR working tree, and reads stdin from the empty stream
151
+ so the spawn does not wait for input.
152
+
153
+ After a chain run, a non-empty `git status --porcelain` means the review
154
+ applied fixes. The calling host commits and pushes those fixes per the fix
155
+ protocol. The chain process never commits or posts. A clean stamp requires
156
+ `returncode == 0` and a non-null `served_command` plus a clean tree —
157
+ `dirty_tree` false alone is not enough when the chain failed or never served.
158
+
159
+ GitHub transport (`pr-loop-cloud-transport`) is a separate axis from this
160
+ harness branch. That skill routes `gh` vs MCP; it does not change which host
161
+ can run Claude-only slash commands.
162
+
163
+ ## Leader-socket rule
164
+
165
+ Every concurrent grok process gets its own `--leader-socket` path under the
166
+ caller-supplied run temp directory. The headless runner mints a unique
167
+ `grok-leader-<uuid>.sock` per invocation. Preflight auth and ping probes use
168
+ their own fixed socket filenames under the same directory so they never share a
169
+ socket with a live worker.
170
+
171
+ ## Cloud setup
172
+
173
+ Unattended grok auth uses the `XAI_API_KEY` environment variable. Install the
174
+ CLI via the vendor curl installer or the `@xai-official/grok` package. Keep
175
+ agent definitions and hook config in parity with `npx claude-dev-env` so the
176
+ preflight role-agent check and local `~/.claude` layout match what the worker
177
+ expects.
178
+
179
+ Use placeholders for account-specific values; do not hardcode personal paths or
180
+ keys in skills or docs.
181
+
182
+ ## GitHub transport
183
+
184
+ `pr-loop-cloud-transport` is a separate axis. This utility never posts reviews,
185
+ opens PRs, or substitutes `gh` for MCP. GitHub reachability and cloud session
186
+ routing stay outside the worker-spawn contract.