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,1014 @@
1
+ """Behavioral tests for the worker-spawn tier dispatcher."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import subprocess
7
+ import sys
8
+ import threading
9
+ from collections.abc import Sequence
10
+ from dataclasses import dataclass, field
11
+ from pathlib import Path
12
+
13
+ import pytest
14
+
15
+ _SCRIPTS_DIR = Path(__file__).resolve().parent
16
+ if str(_SCRIPTS_DIR) not in sys.path:
17
+ sys.path.insert(0, str(_SCRIPTS_DIR))
18
+
19
+ import claude_chain_runner as chain_runner # noqa: E402
20
+ import resolve_worker_spawn as dispatcher # noqa: E402
21
+ from claude_chain_runner import ( # noqa: E402
22
+ ChainAttempt,
23
+ ChainConfigurationError,
24
+ ChainInvocationOutcome,
25
+ )
26
+ from dev_env_scripts_constants.grok_worker_constants import ( # noqa: E402
27
+ AGENT_FLAG,
28
+ ALL_AGENT_FILENAMES_BY_ROLE,
29
+ ATTEMPT_KEY_OK,
30
+ ATTEMPT_KEY_REASON,
31
+ ATTEMPT_KEY_TIER,
32
+ CLI_ENABLE_CLAUDE_TIER_FLAG,
33
+ CLI_ROLE_FLAG,
34
+ CLI_RUN_STATE_DIR_FLAG,
35
+ CLI_TIMEOUT_FLAG,
36
+ CLASSIFICATION_AUTH_FAILURE,
37
+ CLASSIFICATION_OK,
38
+ CLASSIFICATION_USAGE_LIMIT,
39
+ CWD_FLAG,
40
+ DEFAULT_ROLE,
41
+ DEFAULT_SPAWN_MAX_TURNS,
42
+ DEFAULT_WORKER_TIMEOUT_SECONDS,
43
+ OUTPUT_FORMAT_FLAG,
44
+ OUTPUT_FORMAT_JSON,
45
+ PROMPT_FILE_FLAG,
46
+ REASON_CLAUDE_AGENT_REQUIRED,
47
+ REASON_GROK_AUTH_FAILED,
48
+ REASON_PROMPT_FILE_MISSING,
49
+ RESULT_KEY_ATTEMPTS,
50
+ RESULT_KEY_OK,
51
+ RESULT_KEY_OUTPUT,
52
+ RESULT_KEY_RETURNCODE,
53
+ RESULT_KEY_TIER_USED,
54
+ SINGLE_TURN_FLAG,
55
+ SPAWN_CONFIG_ERROR_EXIT_CODE,
56
+ SPAWN_EXHAUSTED_EXIT_CODE,
57
+ SPAWN_SERVED_EXIT_CODE,
58
+ TIER_CLAUDE_AGENT,
59
+ TIER_CLAUDE_HEADLESS,
60
+ TIER_GROK,
61
+ )
62
+ from grok_headless_runner import GrokRunnerOutcome # noqa: E402
63
+ from grok_worker_preflight import PreflightOutcome # noqa: E402
64
+
65
+ HOST_PROFILE_CLAUDE = "Claude"
66
+ HOST_PROFILE_THIRD_PARTY = "ThirdParty"
67
+
68
+ FIXTURE_GROK_STDOUT = '{"tier":"grok","status":"done"}'
69
+ FIXTURE_CLAUDE_STDOUT = '{"tier":"claude","status":"done"}'
70
+ FIXTURE_PROMPT_TEXT = "do the work"
71
+ FIXTURE_GROK_RETURNCODE = 0
72
+ FIXTURE_CLAUDE_RETURNCODE = 0
73
+ FIXTURE_FAILED_RETURNCODE = 1
74
+ FIXTURE_ROLE = "code-quality-agent"
75
+ LARGE_PROMPT_CHARACTER_COUNT = 40000
76
+ WINDOWS_SAFE_ARGV_ELEMENT_CEILING = 8192
77
+ EXPECTED_PRIMARY_AGENT_FOR_DEFAULT_ROLE = Path(
78
+ ALL_AGENT_FILENAMES_BY_ROLE[DEFAULT_ROLE][0]
79
+ ).stem
80
+
81
+
82
+ def _usable_preflight() -> PreflightOutcome:
83
+ return PreflightOutcome(is_usable=True, reason=None)
84
+
85
+
86
+ def _fallthrough_preflight(reason: str) -> PreflightOutcome:
87
+ return PreflightOutcome(is_usable=False, reason=reason)
88
+
89
+
90
+ def _grok_ok(stdout: str = FIXTURE_GROK_STDOUT) -> GrokRunnerOutcome:
91
+ return GrokRunnerOutcome(
92
+ is_ok=True,
93
+ returncode=FIXTURE_GROK_RETURNCODE,
94
+ classification=CLASSIFICATION_OK,
95
+ stdout=stdout,
96
+ stderr="",
97
+ )
98
+
99
+
100
+ def _grok_failure(
101
+ classification: str,
102
+ *,
103
+ returncode: int = FIXTURE_FAILED_RETURNCODE,
104
+ stdout: str = "",
105
+ stderr: str = "",
106
+ ) -> GrokRunnerOutcome:
107
+ return GrokRunnerOutcome(
108
+ is_ok=False,
109
+ returncode=returncode,
110
+ classification=classification,
111
+ stdout=stdout,
112
+ stderr=stderr,
113
+ )
114
+
115
+
116
+ def _claude_served(
117
+ stdout: str = FIXTURE_CLAUDE_STDOUT,
118
+ *,
119
+ returncode: int = FIXTURE_CLAUDE_RETURNCODE,
120
+ ) -> ChainInvocationOutcome:
121
+ return ChainInvocationOutcome(
122
+ served_command="claude",
123
+ returncode=returncode,
124
+ stdout=stdout,
125
+ stderr="",
126
+ attempts=(ChainAttempt(command="claude", status="served"),),
127
+ )
128
+
129
+
130
+ def _claude_exhausted() -> ChainInvocationOutcome:
131
+ return ChainInvocationOutcome(
132
+ served_command=None,
133
+ returncode=FIXTURE_FAILED_RETURNCODE,
134
+ stdout="",
135
+ stderr="usage limit reached",
136
+ attempts=(ChainAttempt(command="claude", status="usage_limited"),),
137
+ )
138
+
139
+
140
+ def _paths(tmp_path: Path) -> tuple[Path, Path, Path]:
141
+ prompt_file = tmp_path / "prompt.txt"
142
+ prompt_file.write_text(FIXTURE_PROMPT_TEXT, encoding="utf-8")
143
+ working_directory = tmp_path / "project"
144
+ working_directory.mkdir()
145
+ run_state_directory = tmp_path / "run-state"
146
+ run_state_directory.mkdir()
147
+ return prompt_file, working_directory, run_state_directory
148
+
149
+
150
+ @dataclass
151
+ class SeamCallLog:
152
+ preflight_calls: int = 0
153
+ grok_calls: int = 0
154
+ claude_calls: int = 0
155
+ claude_arguments: list[str] | None = None
156
+ host_profile_calls: int = 0
157
+ is_stdin_from_prompt_file: bool = False
158
+ claude_stdin_path: Path | None = None
159
+ claude_working_directory: Path | None = None
160
+ grok_keyword_arguments: dict[str, object] | None = None
161
+ max_argv_element_length: int = 0
162
+ all_observed_working_directories: list[Path] = field(default_factory=list)
163
+
164
+
165
+ def _install_seams(
166
+ monkeypatch: pytest.MonkeyPatch,
167
+ *,
168
+ preflight_outcome: PreflightOutcome = _usable_preflight(),
169
+ grok_outcome: GrokRunnerOutcome | None = None,
170
+ claude_outcome: ChainInvocationOutcome | BaseException | None = None,
171
+ host_profile: str = HOST_PROFILE_CLAUDE,
172
+ ) -> SeamCallLog:
173
+ call_log = SeamCallLog()
174
+
175
+ def fake_preflight(**_keyword_arguments: object) -> PreflightOutcome:
176
+ call_log.preflight_calls += 1
177
+ return preflight_outcome
178
+
179
+ def fake_grok(**keyword_arguments: object) -> GrokRunnerOutcome:
180
+ call_log.grok_calls += 1
181
+ call_log.grok_keyword_arguments = dict(keyword_arguments)
182
+ assert grok_outcome is not None
183
+ return grok_outcome
184
+
185
+ def fake_claude(
186
+ all_claude_arguments: list[str], *, timeout_seconds: int
187
+ ) -> ChainInvocationOutcome:
188
+ call_log.claude_calls += 1
189
+ call_log.claude_arguments = list(all_claude_arguments)
190
+ chain_runner.chain_subprocess_runner(
191
+ ["claude", *all_claude_arguments],
192
+ capture_output=True,
193
+ text=True,
194
+ timeout=timeout_seconds,
195
+ check=False,
196
+ )
197
+ if isinstance(claude_outcome, BaseException):
198
+ raise claude_outcome
199
+ assert isinstance(claude_outcome, ChainInvocationOutcome)
200
+ return claude_outcome
201
+
202
+ def fake_host_profile(
203
+ setting_by_name: object | None = None,
204
+ ) -> str:
205
+ del setting_by_name
206
+ call_log.host_profile_calls += 1
207
+ return host_profile
208
+
209
+ monkeypatch.setattr(dispatcher, "spawn_preflight_runner", fake_preflight)
210
+ monkeypatch.setattr(dispatcher, "spawn_grok_runner", fake_grok)
211
+ monkeypatch.setattr(dispatcher, "spawn_claude_runner", fake_claude)
212
+ monkeypatch.setattr(dispatcher, "spawn_host_profile_detector", fake_host_profile)
213
+
214
+ def _tracking_subprocess_runner(
215
+ all_invocation_tokens: Sequence[str],
216
+ *all_positionals: object,
217
+ **all_keywords: object,
218
+ ) -> subprocess.CompletedProcess[str]:
219
+ del all_positionals
220
+ for each_token in all_invocation_tokens:
221
+ call_log.max_argv_element_length = max(
222
+ call_log.max_argv_element_length, len(str(each_token))
223
+ )
224
+ maybe_stdin = all_keywords.get("stdin")
225
+ if maybe_stdin is not None and maybe_stdin is not subprocess.DEVNULL:
226
+ call_log.is_stdin_from_prompt_file = True
227
+ maybe_name = getattr(maybe_stdin, "name", None)
228
+ if maybe_name is not None:
229
+ call_log.claude_stdin_path = Path(str(maybe_name))
230
+ maybe_cwd = all_keywords.get("cwd")
231
+ if maybe_cwd is not None:
232
+ working_directory = Path(str(maybe_cwd))
233
+ call_log.claude_working_directory = working_directory
234
+ call_log.all_observed_working_directories.append(working_directory)
235
+ return subprocess.CompletedProcess(
236
+ args=list(all_invocation_tokens),
237
+ returncode=0,
238
+ stdout="{}",
239
+ stderr="",
240
+ )
241
+
242
+ monkeypatch.setattr(
243
+ chain_runner, "chain_subprocess_runner", _tracking_subprocess_runner
244
+ )
245
+ return call_log
246
+
247
+
248
+ def test_grok_ok_serves_tier_one(
249
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
250
+ ) -> None:
251
+ prompt_file, working_directory, run_state_directory = _paths(tmp_path)
252
+ call_log = _install_seams(
253
+ monkeypatch,
254
+ grok_outcome=_grok_ok(),
255
+ )
256
+
257
+ spawn_outcome = dispatcher.resolve_worker_spawn(
258
+ role=FIXTURE_ROLE,
259
+ prompt_file=prompt_file,
260
+ working_directory=working_directory,
261
+ timeout_seconds=DEFAULT_WORKER_TIMEOUT_SECONDS,
262
+ is_claude_tier_enabled=False,
263
+ run_state_directory=run_state_directory,
264
+ max_turns=DEFAULT_SPAWN_MAX_TURNS,
265
+ )
266
+
267
+ assert spawn_outcome.is_ok is True
268
+ assert spawn_outcome.tier_used == TIER_GROK
269
+ assert spawn_outcome.captured_stdout == FIXTURE_GROK_STDOUT
270
+ assert spawn_outcome.returncode == FIXTURE_GROK_RETURNCODE
271
+ assert call_log.preflight_calls == 1
272
+ assert call_log.grok_calls == 1
273
+ assert call_log.claude_calls == 0
274
+ assert call_log.host_profile_calls == 0
275
+ assert call_log.grok_keyword_arguments is not None
276
+ assert call_log.grok_keyword_arguments["agent_name"] == FIXTURE_ROLE
277
+ assert len(spawn_outcome.all_attempts) == 1
278
+ assert spawn_outcome.all_attempts[0].tier == TIER_GROK
279
+ assert spawn_outcome.all_attempts[0].is_ok is True
280
+ assert spawn_outcome.all_attempts[0].reason is None
281
+
282
+
283
+ def test_grok_usage_limited_on_claude_host_requires_agent(
284
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
285
+ ) -> None:
286
+ prompt_file, working_directory, run_state_directory = _paths(tmp_path)
287
+ call_log = _install_seams(
288
+ monkeypatch,
289
+ grok_outcome=_grok_failure(CLASSIFICATION_USAGE_LIMIT),
290
+ host_profile=HOST_PROFILE_CLAUDE,
291
+ )
292
+
293
+ spawn_outcome = dispatcher.resolve_worker_spawn(
294
+ role=FIXTURE_ROLE,
295
+ prompt_file=prompt_file,
296
+ working_directory=working_directory,
297
+ timeout_seconds=DEFAULT_WORKER_TIMEOUT_SECONDS,
298
+ is_claude_tier_enabled=False,
299
+ run_state_directory=run_state_directory,
300
+ max_turns=DEFAULT_SPAWN_MAX_TURNS,
301
+ )
302
+
303
+ assert spawn_outcome.is_ok is False
304
+ assert spawn_outcome.tier_used is None
305
+ assert call_log.claude_calls == 0
306
+ assert call_log.host_profile_calls == 1
307
+ all_reasons = [each_attempt.reason for each_attempt in spawn_outcome.all_attempts]
308
+ assert REASON_CLAUDE_AGENT_REQUIRED in all_reasons
309
+ assert spawn_outcome.all_attempts[0].tier == TIER_GROK
310
+ assert spawn_outcome.all_attempts[0].is_ok is False
311
+ assert spawn_outcome.all_attempts[0].reason == CLASSIFICATION_USAGE_LIMIT
312
+ assert spawn_outcome.all_attempts[1].tier == TIER_CLAUDE_AGENT
313
+ assert spawn_outcome.all_attempts[1].reason == REASON_CLAUDE_AGENT_REQUIRED
314
+
315
+
316
+ def test_grok_auth_failed_on_third_party_runs_tier_three(
317
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
318
+ ) -> None:
319
+ prompt_file, working_directory, run_state_directory = _paths(tmp_path)
320
+ call_log = _install_seams(
321
+ monkeypatch,
322
+ grok_outcome=_grok_failure(CLASSIFICATION_AUTH_FAILURE),
323
+ claude_outcome=_claude_served(),
324
+ host_profile=HOST_PROFILE_THIRD_PARTY,
325
+ )
326
+
327
+ spawn_outcome = dispatcher.resolve_worker_spawn(
328
+ role=FIXTURE_ROLE,
329
+ prompt_file=prompt_file,
330
+ working_directory=working_directory,
331
+ timeout_seconds=DEFAULT_WORKER_TIMEOUT_SECONDS,
332
+ is_claude_tier_enabled=False,
333
+ run_state_directory=run_state_directory,
334
+ max_turns=DEFAULT_SPAWN_MAX_TURNS,
335
+ )
336
+
337
+ assert spawn_outcome.is_ok is True
338
+ assert spawn_outcome.tier_used == TIER_CLAUDE_HEADLESS
339
+ assert spawn_outcome.captured_stdout == FIXTURE_CLAUDE_STDOUT
340
+ assert call_log.claude_calls == 1
341
+ assert call_log.claude_arguments is not None
342
+ assert PROMPT_FILE_FLAG not in call_log.claude_arguments
343
+ assert FIXTURE_PROMPT_TEXT not in call_log.claude_arguments
344
+ assert call_log.claude_arguments == [
345
+ SINGLE_TURN_FLAG,
346
+ OUTPUT_FORMAT_FLAG,
347
+ OUTPUT_FORMAT_JSON,
348
+ AGENT_FLAG,
349
+ FIXTURE_ROLE,
350
+ ]
351
+ assert call_log.is_stdin_from_prompt_file is True
352
+ assert call_log.claude_stdin_path == prompt_file
353
+ assert call_log.claude_working_directory == working_directory
354
+ assert spawn_outcome.all_attempts[0].tier == TIER_GROK
355
+ assert spawn_outcome.all_attempts[0].reason == CLASSIFICATION_AUTH_FAILURE
356
+ assert spawn_outcome.all_attempts[1].tier == TIER_CLAUDE_HEADLESS
357
+ assert spawn_outcome.all_attempts[1].is_ok is True
358
+
359
+
360
+ def test_tier_three_exhausted_returns_exit_two(
361
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path, capsys: pytest.CaptureFixture[str]
362
+ ) -> None:
363
+ prompt_file, working_directory, run_state_directory = _paths(tmp_path)
364
+ _install_seams(
365
+ monkeypatch,
366
+ grok_outcome=_grok_failure(CLASSIFICATION_AUTH_FAILURE),
367
+ claude_outcome=_claude_exhausted(),
368
+ host_profile=HOST_PROFILE_THIRD_PARTY,
369
+ )
370
+
371
+ exit_code = dispatcher.main(
372
+ [
373
+ CLI_ROLE_FLAG,
374
+ FIXTURE_ROLE,
375
+ PROMPT_FILE_FLAG,
376
+ str(prompt_file),
377
+ CWD_FLAG,
378
+ str(working_directory),
379
+ CLI_TIMEOUT_FLAG,
380
+ str(DEFAULT_WORKER_TIMEOUT_SECONDS),
381
+ CLI_RUN_STATE_DIR_FLAG,
382
+ str(run_state_directory),
383
+ ]
384
+ )
385
+
386
+ assert exit_code == SPAWN_EXHAUSTED_EXIT_CODE
387
+ captured = capsys.readouterr()
388
+ parsed_payload = json.loads(captured.out)
389
+ assert parsed_payload[RESULT_KEY_OK] is False
390
+ assert parsed_payload[RESULT_KEY_TIER_USED] is None
391
+
392
+
393
+ def test_config_error_returns_exit_three(
394
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path, capsys: pytest.CaptureFixture[str]
395
+ ) -> None:
396
+ prompt_file, working_directory, run_state_directory = _paths(tmp_path)
397
+ _install_seams(
398
+ monkeypatch,
399
+ grok_outcome=_grok_failure(CLASSIFICATION_AUTH_FAILURE),
400
+ claude_outcome=ChainConfigurationError("chain config missing"),
401
+ host_profile=HOST_PROFILE_THIRD_PARTY,
402
+ )
403
+
404
+ exit_code = dispatcher.main(
405
+ [
406
+ CLI_ROLE_FLAG,
407
+ FIXTURE_ROLE,
408
+ PROMPT_FILE_FLAG,
409
+ str(prompt_file),
410
+ CWD_FLAG,
411
+ str(working_directory),
412
+ CLI_TIMEOUT_FLAG,
413
+ str(DEFAULT_WORKER_TIMEOUT_SECONDS),
414
+ CLI_RUN_STATE_DIR_FLAG,
415
+ str(run_state_directory),
416
+ ]
417
+ )
418
+
419
+ assert exit_code == SPAWN_CONFIG_ERROR_EXIT_CODE
420
+ captured = capsys.readouterr()
421
+ parsed_payload = json.loads(captured.out)
422
+ assert parsed_payload[RESULT_KEY_OK] is False
423
+ assert parsed_payload[RESULT_KEY_RETURNCODE] == SPAWN_CONFIG_ERROR_EXIT_CODE
424
+
425
+
426
+ def test_attempts_array_ordering_across_tiers(
427
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
428
+ ) -> None:
429
+ prompt_file, working_directory, run_state_directory = _paths(tmp_path)
430
+ _install_seams(
431
+ monkeypatch,
432
+ grok_outcome=_grok_failure(CLASSIFICATION_USAGE_LIMIT),
433
+ claude_outcome=_claude_served(),
434
+ host_profile=HOST_PROFILE_CLAUDE,
435
+ )
436
+
437
+ spawn_outcome = dispatcher.resolve_worker_spawn(
438
+ role=FIXTURE_ROLE,
439
+ prompt_file=prompt_file,
440
+ working_directory=working_directory,
441
+ timeout_seconds=DEFAULT_WORKER_TIMEOUT_SECONDS,
442
+ is_claude_tier_enabled=True,
443
+ run_state_directory=run_state_directory,
444
+ max_turns=DEFAULT_SPAWN_MAX_TURNS,
445
+ )
446
+
447
+ all_tiers = [each_attempt.tier for each_attempt in spawn_outcome.all_attempts]
448
+ assert all_tiers == [TIER_GROK, TIER_CLAUDE_HEADLESS]
449
+ assert spawn_outcome.tier_used == TIER_CLAUDE_HEADLESS
450
+
451
+
452
+ def test_cli_stdout_carries_only_json_result(
453
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path, capsys: pytest.CaptureFixture[str]
454
+ ) -> None:
455
+ prompt_file, working_directory, run_state_directory = _paths(tmp_path)
456
+ _install_seams(
457
+ monkeypatch,
458
+ grok_outcome=_grok_ok(),
459
+ )
460
+
461
+ exit_code = dispatcher.main(
462
+ [
463
+ CLI_ROLE_FLAG,
464
+ FIXTURE_ROLE,
465
+ PROMPT_FILE_FLAG,
466
+ str(prompt_file),
467
+ CWD_FLAG,
468
+ str(working_directory),
469
+ CLI_TIMEOUT_FLAG,
470
+ str(DEFAULT_WORKER_TIMEOUT_SECONDS),
471
+ CLI_RUN_STATE_DIR_FLAG,
472
+ str(run_state_directory),
473
+ ]
474
+ )
475
+
476
+ assert exit_code == SPAWN_SERVED_EXIT_CODE
477
+ captured = capsys.readouterr()
478
+ assert captured.err == ""
479
+ parsed_payload = json.loads(captured.out)
480
+ assert captured.out == json.dumps(
481
+ parsed_payload
482
+ ) + "\n" or captured.out.strip() == json.dumps(parsed_payload)
483
+ reparsed = json.loads(captured.out.strip())
484
+ assert reparsed is not None
485
+ assert set(reparsed.keys()) == {
486
+ RESULT_KEY_TIER_USED,
487
+ RESULT_KEY_OK,
488
+ RESULT_KEY_ATTEMPTS,
489
+ RESULT_KEY_OUTPUT,
490
+ RESULT_KEY_RETURNCODE,
491
+ }
492
+ assert reparsed[RESULT_KEY_TIER_USED] == TIER_GROK
493
+ assert reparsed[RESULT_KEY_OK] is True
494
+ assert reparsed[RESULT_KEY_OUTPUT] == FIXTURE_GROK_STDOUT
495
+ first_attempt = reparsed[RESULT_KEY_ATTEMPTS][0]
496
+ assert first_attempt[ATTEMPT_KEY_TIER] == TIER_GROK
497
+ assert first_attempt[ATTEMPT_KEY_OK] is True
498
+ assert first_attempt[ATTEMPT_KEY_REASON] is None
499
+
500
+
501
+ def test_preflight_fallthrough_skips_grok_runner(
502
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
503
+ ) -> None:
504
+ prompt_file, working_directory, run_state_directory = _paths(tmp_path)
505
+ call_log = _install_seams(
506
+ monkeypatch,
507
+ preflight_outcome=_fallthrough_preflight(REASON_GROK_AUTH_FAILED),
508
+ host_profile=HOST_PROFILE_CLAUDE,
509
+ )
510
+
511
+ spawn_outcome = dispatcher.resolve_worker_spawn(
512
+ role=DEFAULT_ROLE,
513
+ prompt_file=prompt_file,
514
+ working_directory=working_directory,
515
+ timeout_seconds=DEFAULT_WORKER_TIMEOUT_SECONDS,
516
+ is_claude_tier_enabled=False,
517
+ run_state_directory=run_state_directory,
518
+ max_turns=DEFAULT_SPAWN_MAX_TURNS,
519
+ )
520
+
521
+ assert call_log.grok_calls == 0
522
+ assert spawn_outcome.all_attempts[0].reason == REASON_GROK_AUTH_FAILED
523
+ assert REASON_CLAUDE_AGENT_REQUIRED in [
524
+ each_attempt.reason for each_attempt in spawn_outcome.all_attempts
525
+ ]
526
+
527
+
528
+ def test_detect_host_profile_is_consumed_not_reimplemented() -> None:
529
+ source_text = Path(dispatcher.__file__).read_text(encoding="utf-8")
530
+ assert "spawn_host_profile_detector" in source_text
531
+ assert "detect_host_profile" in source_text
532
+ assert "ADVISOR_HOST_PROFILE" not in source_text
533
+ assert "THIRD_PARTY" not in source_text
534
+
535
+
536
+ def test_default_max_turns_reaches_grok_kwargs(
537
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path, capsys: pytest.CaptureFixture[str]
538
+ ) -> None:
539
+ prompt_file, working_directory, run_state_directory = _paths(tmp_path)
540
+ call_log = _install_seams(
541
+ monkeypatch,
542
+ grok_outcome=_grok_ok(),
543
+ )
544
+
545
+ exit_code = dispatcher.main(
546
+ [
547
+ CLI_ROLE_FLAG,
548
+ FIXTURE_ROLE,
549
+ PROMPT_FILE_FLAG,
550
+ str(prompt_file),
551
+ CWD_FLAG,
552
+ str(working_directory),
553
+ CLI_TIMEOUT_FLAG,
554
+ str(DEFAULT_WORKER_TIMEOUT_SECONDS),
555
+ CLI_RUN_STATE_DIR_FLAG,
556
+ str(run_state_directory),
557
+ ]
558
+ )
559
+
560
+ assert exit_code == SPAWN_SERVED_EXIT_CODE
561
+ assert call_log.grok_keyword_arguments is not None
562
+ assert call_log.grok_keyword_arguments["max_turns"] == DEFAULT_SPAWN_MAX_TURNS
563
+ parsed_payload = json.loads(capsys.readouterr().out)
564
+ assert parsed_payload[RESULT_KEY_OK] is True
565
+
566
+
567
+ def test_encode_spawn_outcome_shape() -> None:
568
+ spawn_outcome = dispatcher.SpawnOutcome(
569
+ tier_used=TIER_GROK,
570
+ is_ok=True,
571
+ all_attempts=(
572
+ dispatcher.SpawnAttempt(tier=TIER_GROK, is_ok=True, reason=None),
573
+ ),
574
+ captured_stdout=FIXTURE_GROK_STDOUT,
575
+ returncode=FIXTURE_GROK_RETURNCODE,
576
+ )
577
+ encoded_payload = dispatcher.encode_spawn_outcome(spawn_outcome)
578
+ assert encoded_payload[RESULT_KEY_TIER_USED] == TIER_GROK
579
+ assert encoded_payload[RESULT_KEY_OK] is True
580
+ assert encoded_payload[RESULT_KEY_OUTPUT] == FIXTURE_GROK_STDOUT
581
+ assert encoded_payload[RESULT_KEY_RETURNCODE] == FIXTURE_GROK_RETURNCODE
582
+ assert encoded_payload[RESULT_KEY_ATTEMPTS] == [
583
+ {
584
+ ATTEMPT_KEY_TIER: TIER_GROK,
585
+ ATTEMPT_KEY_OK: True,
586
+ ATTEMPT_KEY_REASON: None,
587
+ }
588
+ ]
589
+
590
+
591
+ def test_enable_claude_tier_flag_reaches_tier_three_on_claude_host(
592
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path, capsys: pytest.CaptureFixture[str]
593
+ ) -> None:
594
+ prompt_file, working_directory, run_state_directory = _paths(tmp_path)
595
+ call_log = _install_seams(
596
+ monkeypatch,
597
+ grok_outcome=_grok_failure(CLASSIFICATION_USAGE_LIMIT),
598
+ claude_outcome=_claude_served(),
599
+ host_profile=HOST_PROFILE_CLAUDE,
600
+ )
601
+
602
+ exit_code = dispatcher.main(
603
+ [
604
+ CLI_ROLE_FLAG,
605
+ FIXTURE_ROLE,
606
+ PROMPT_FILE_FLAG,
607
+ str(prompt_file),
608
+ CWD_FLAG,
609
+ str(working_directory),
610
+ CLI_TIMEOUT_FLAG,
611
+ str(DEFAULT_WORKER_TIMEOUT_SECONDS),
612
+ CLI_ENABLE_CLAUDE_TIER_FLAG,
613
+ CLI_RUN_STATE_DIR_FLAG,
614
+ str(run_state_directory),
615
+ ]
616
+ )
617
+
618
+ assert exit_code == SPAWN_SERVED_EXIT_CODE
619
+ assert call_log.claude_calls == 1
620
+ assert call_log.is_stdin_from_prompt_file is True
621
+ assert call_log.claude_stdin_path == prompt_file
622
+ assert call_log.claude_working_directory == working_directory
623
+ assert call_log.claude_arguments is not None
624
+ assert PROMPT_FILE_FLAG not in call_log.claude_arguments
625
+ assert SINGLE_TURN_FLAG in call_log.claude_arguments
626
+ assert OUTPUT_FORMAT_JSON in call_log.claude_arguments
627
+ assert AGENT_FLAG in call_log.claude_arguments
628
+ parsed_payload = json.loads(capsys.readouterr().out)
629
+ assert parsed_payload[RESULT_KEY_TIER_USED] == TIER_CLAUDE_HEADLESS
630
+
631
+
632
+ def test_default_role_maps_to_primary_agent_stem(
633
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
634
+ ) -> None:
635
+ prompt_file, working_directory, run_state_directory = _paths(tmp_path)
636
+ call_log = _install_seams(
637
+ monkeypatch,
638
+ grok_outcome=_grok_ok(),
639
+ )
640
+
641
+ spawn_outcome = dispatcher.resolve_worker_spawn(
642
+ role=DEFAULT_ROLE,
643
+ prompt_file=prompt_file,
644
+ working_directory=working_directory,
645
+ timeout_seconds=DEFAULT_WORKER_TIMEOUT_SECONDS,
646
+ is_claude_tier_enabled=False,
647
+ run_state_directory=run_state_directory,
648
+ max_turns=DEFAULT_SPAWN_MAX_TURNS,
649
+ )
650
+
651
+ assert spawn_outcome.is_ok is True
652
+ assert call_log.grok_keyword_arguments is not None
653
+ agent_name = call_log.grok_keyword_arguments["agent_name"]
654
+ assert agent_name == EXPECTED_PRIMARY_AGENT_FOR_DEFAULT_ROLE
655
+ assert agent_name != DEFAULT_ROLE
656
+ assert agent_name != "bugteam"
657
+
658
+
659
+ def test_tier_three_argv_includes_agent_for_default_role(
660
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
661
+ ) -> None:
662
+ prompt_file, working_directory, run_state_directory = _paths(tmp_path)
663
+ call_log = _install_seams(
664
+ monkeypatch,
665
+ grok_outcome=_grok_failure(CLASSIFICATION_AUTH_FAILURE),
666
+ claude_outcome=_claude_served(),
667
+ host_profile=HOST_PROFILE_THIRD_PARTY,
668
+ )
669
+
670
+ spawn_outcome = dispatcher.resolve_worker_spawn(
671
+ role=DEFAULT_ROLE,
672
+ prompt_file=prompt_file,
673
+ working_directory=working_directory,
674
+ timeout_seconds=DEFAULT_WORKER_TIMEOUT_SECONDS,
675
+ is_claude_tier_enabled=False,
676
+ run_state_directory=run_state_directory,
677
+ max_turns=DEFAULT_SPAWN_MAX_TURNS,
678
+ )
679
+
680
+ assert spawn_outcome.tier_used == TIER_CLAUDE_HEADLESS
681
+ assert call_log.claude_arguments is not None
682
+ assert AGENT_FLAG in call_log.claude_arguments
683
+ assert EXPECTED_PRIMARY_AGENT_FOR_DEFAULT_ROLE in call_log.claude_arguments
684
+ agent_flag_index = call_log.claude_arguments.index(AGENT_FLAG)
685
+ assert (
686
+ call_log.claude_arguments[agent_flag_index + 1]
687
+ == EXPECTED_PRIMARY_AGENT_FOR_DEFAULT_ROLE
688
+ )
689
+
690
+
691
+ def test_large_prompt_stays_out_of_claude_argv(
692
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
693
+ ) -> None:
694
+ prompt_file, working_directory, run_state_directory = _paths(tmp_path)
695
+ large_prompt_text = "x" * LARGE_PROMPT_CHARACTER_COUNT
696
+ prompt_file.write_text(large_prompt_text, encoding="utf-8")
697
+ call_log = _install_seams(
698
+ monkeypatch,
699
+ grok_outcome=_grok_failure(CLASSIFICATION_AUTH_FAILURE),
700
+ claude_outcome=_claude_served(),
701
+ host_profile=HOST_PROFILE_THIRD_PARTY,
702
+ )
703
+
704
+ spawn_outcome = dispatcher.resolve_worker_spawn(
705
+ role=DEFAULT_ROLE,
706
+ prompt_file=prompt_file,
707
+ working_directory=working_directory,
708
+ timeout_seconds=DEFAULT_WORKER_TIMEOUT_SECONDS,
709
+ is_claude_tier_enabled=False,
710
+ run_state_directory=run_state_directory,
711
+ max_turns=DEFAULT_SPAWN_MAX_TURNS,
712
+ )
713
+
714
+ assert spawn_outcome.tier_used == TIER_CLAUDE_HEADLESS
715
+ assert call_log.claude_arguments is not None
716
+ assert large_prompt_text not in call_log.claude_arguments
717
+ assert call_log.max_argv_element_length < WINDOWS_SAFE_ARGV_ELEMENT_CEILING
718
+ assert call_log.is_stdin_from_prompt_file is True
719
+ assert call_log.claude_stdin_path == prompt_file
720
+
721
+
722
+ def test_missing_prompt_file_returns_json_config_exit(
723
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path, capsys: pytest.CaptureFixture[str]
724
+ ) -> None:
725
+ working_directory = tmp_path / "project"
726
+ working_directory.mkdir()
727
+ run_state_directory = tmp_path / "run-state"
728
+ run_state_directory.mkdir()
729
+ missing_prompt_file = tmp_path / "absent-prompt.txt"
730
+ _install_seams(monkeypatch, grok_outcome=_grok_ok())
731
+
732
+ exit_code = dispatcher.main(
733
+ [
734
+ CLI_ROLE_FLAG,
735
+ FIXTURE_ROLE,
736
+ PROMPT_FILE_FLAG,
737
+ str(missing_prompt_file),
738
+ CWD_FLAG,
739
+ str(working_directory),
740
+ CLI_TIMEOUT_FLAG,
741
+ str(DEFAULT_WORKER_TIMEOUT_SECONDS),
742
+ CLI_RUN_STATE_DIR_FLAG,
743
+ str(run_state_directory),
744
+ ]
745
+ )
746
+
747
+ assert exit_code == SPAWN_CONFIG_ERROR_EXIT_CODE
748
+ captured = capsys.readouterr()
749
+ assert captured.err == ""
750
+ parsed_payload = json.loads(captured.out)
751
+ assert parsed_payload[RESULT_KEY_OK] is False
752
+ assert parsed_payload[RESULT_KEY_TIER_USED] is None
753
+ assert parsed_payload[RESULT_KEY_OUTPUT] == REASON_PROMPT_FILE_MISSING
754
+ assert parsed_payload[RESULT_KEY_RETURNCODE] == SPAWN_CONFIG_ERROR_EXIT_CODE
755
+
756
+
757
+ def _deny_prompt_open(
758
+ monkeypatch: pytest.MonkeyPatch,
759
+ prompt_file: Path,
760
+ open_error: OSError,
761
+ ) -> None:
762
+ real_open = Path.open
763
+ resolved_prompt = prompt_file.resolve()
764
+
765
+ def open_with_denied_prompt(
766
+ self: Path,
767
+ *all_positionals: object,
768
+ **all_keywords: object,
769
+ ) -> object:
770
+ if self.resolve() == resolved_prompt:
771
+ raise open_error
772
+ return real_open(self, *all_positionals, **all_keywords)
773
+
774
+ monkeypatch.setattr(Path, "open", open_with_denied_prompt)
775
+
776
+
777
+ def _main_arguments_for_paths(
778
+ *,
779
+ prompt_file: Path,
780
+ working_directory: Path,
781
+ run_state_directory: Path,
782
+ ) -> list[str]:
783
+ return [
784
+ CLI_ROLE_FLAG,
785
+ FIXTURE_ROLE,
786
+ PROMPT_FILE_FLAG,
787
+ str(prompt_file),
788
+ CWD_FLAG,
789
+ str(working_directory),
790
+ CLI_TIMEOUT_FLAG,
791
+ str(DEFAULT_WORKER_TIMEOUT_SECONDS),
792
+ CLI_RUN_STATE_DIR_FLAG,
793
+ str(run_state_directory),
794
+ ]
795
+
796
+
797
+ def test_unreadable_prompt_file_returns_json_config_exit(
798
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path, capsys: pytest.CaptureFixture[str]
799
+ ) -> None:
800
+ prompt_file, working_directory, run_state_directory = _paths(tmp_path)
801
+ _install_seams(
802
+ monkeypatch,
803
+ grok_outcome=_grok_failure(CLASSIFICATION_AUTH_FAILURE),
804
+ claude_outcome=_claude_served(),
805
+ host_profile=HOST_PROFILE_THIRD_PARTY,
806
+ )
807
+ _deny_prompt_open(
808
+ monkeypatch,
809
+ prompt_file,
810
+ PermissionError("Access is denied"),
811
+ )
812
+
813
+ exit_code = dispatcher.main(
814
+ _main_arguments_for_paths(
815
+ prompt_file=prompt_file,
816
+ working_directory=working_directory,
817
+ run_state_directory=run_state_directory,
818
+ )
819
+ )
820
+
821
+ assert exit_code == SPAWN_CONFIG_ERROR_EXIT_CODE
822
+ captured = capsys.readouterr()
823
+ assert captured.err == ""
824
+ parsed_payload = json.loads(captured.out)
825
+ assert parsed_payload[RESULT_KEY_OK] is False
826
+ assert parsed_payload[RESULT_KEY_TIER_USED] is None
827
+ assert parsed_payload[RESULT_KEY_OUTPUT] == REASON_PROMPT_FILE_MISSING
828
+ assert parsed_payload[RESULT_KEY_RETURNCODE] == SPAWN_CONFIG_ERROR_EXIT_CODE
829
+ assert "executable_not_found" not in json.dumps(parsed_payload)
830
+ all_attempt_reasons = [
831
+ each_attempt.get(ATTEMPT_KEY_REASON)
832
+ for each_attempt in parsed_payload[RESULT_KEY_ATTEMPTS]
833
+ ]
834
+ assert REASON_PROMPT_FILE_MISSING in all_attempt_reasons
835
+ assert "executable_not_found" not in all_attempt_reasons
836
+
837
+
838
+ def test_prompt_open_failure_not_classified_as_executable_not_found(
839
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path, capsys: pytest.CaptureFixture[str]
840
+ ) -> None:
841
+ prompt_file, working_directory, run_state_directory = _paths(tmp_path)
842
+ monkeypatch.setattr(
843
+ dispatcher,
844
+ "spawn_preflight_runner",
845
+ lambda **_keyword_arguments: _fallthrough_preflight(REASON_GROK_AUTH_FAILED),
846
+ )
847
+ monkeypatch.setattr(
848
+ dispatcher,
849
+ "spawn_host_profile_detector",
850
+ lambda *all_positionals, **all_keywords: HOST_PROFILE_THIRD_PARTY,
851
+ )
852
+ monkeypatch.setattr(
853
+ chain_runner,
854
+ "load_chain",
855
+ lambda _config_path: [chain_runner.ChainEntry(command="claude", extra_args=())],
856
+ )
857
+ monkeypatch.setattr(
858
+ chain_runner,
859
+ "chain_subprocess_runner",
860
+ lambda *all_positionals, **all_keywords: subprocess.CompletedProcess(
861
+ args=["claude"],
862
+ returncode=0,
863
+ stdout="{}",
864
+ stderr="",
865
+ ),
866
+ )
867
+ _deny_prompt_open(
868
+ monkeypatch,
869
+ prompt_file,
870
+ FileNotFoundError(2, "No such file or directory", str(prompt_file)),
871
+ )
872
+
873
+ exit_code = dispatcher.main(
874
+ _main_arguments_for_paths(
875
+ prompt_file=prompt_file,
876
+ working_directory=working_directory,
877
+ run_state_directory=run_state_directory,
878
+ )
879
+ )
880
+
881
+ assert exit_code == SPAWN_CONFIG_ERROR_EXIT_CODE
882
+ captured = capsys.readouterr()
883
+ assert captured.err == ""
884
+ parsed_payload = json.loads(captured.out)
885
+ assert parsed_payload[RESULT_KEY_OK] is False
886
+ assert parsed_payload[RESULT_KEY_TIER_USED] is None
887
+ assert parsed_payload[RESULT_KEY_OUTPUT] == REASON_PROMPT_FILE_MISSING
888
+ assert parsed_payload[RESULT_KEY_RETURNCODE] == SPAWN_CONFIG_ERROR_EXIT_CODE
889
+ serialized_payload = json.dumps(parsed_payload)
890
+ assert "executable_not_found" not in serialized_payload
891
+ all_attempt_reasons = [
892
+ each_attempt.get(ATTEMPT_KEY_REASON)
893
+ for each_attempt in parsed_payload[RESULT_KEY_ATTEMPTS]
894
+ ]
895
+ assert REASON_PROMPT_FILE_MISSING in all_attempt_reasons
896
+ assert "executable_not_found" not in all_attempt_reasons
897
+
898
+
899
+ def test_headless_chain_runner_lock_serializes_distinct_cwds(
900
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
901
+ ) -> None:
902
+ prompt_file = tmp_path / "prompt.txt"
903
+ prompt_file.write_text(FIXTURE_PROMPT_TEXT, encoding="utf-8")
904
+ first_working_directory = tmp_path / "project-a"
905
+ second_working_directory = tmp_path / "project-b"
906
+ first_working_directory.mkdir()
907
+ second_working_directory.mkdir()
908
+ run_state_directory = tmp_path / "run-state"
909
+ run_state_directory.mkdir()
910
+ call_log = _install_seams(
911
+ monkeypatch,
912
+ grok_outcome=_grok_failure(CLASSIFICATION_AUTH_FAILURE),
913
+ claude_outcome=_claude_served(),
914
+ host_profile=HOST_PROFILE_THIRD_PARTY,
915
+ )
916
+ all_errors: list[BaseException] = []
917
+ barrier = threading.Barrier(2)
918
+
919
+ def _run_with_working_directory(working_directory: Path) -> None:
920
+ try:
921
+ barrier.wait(timeout=5)
922
+ dispatcher.resolve_worker_spawn(
923
+ role=DEFAULT_ROLE,
924
+ prompt_file=prompt_file,
925
+ working_directory=working_directory,
926
+ timeout_seconds=DEFAULT_WORKER_TIMEOUT_SECONDS,
927
+ is_claude_tier_enabled=False,
928
+ run_state_directory=run_state_directory,
929
+ max_turns=DEFAULT_SPAWN_MAX_TURNS,
930
+ )
931
+ except (OSError, RuntimeError, ValueError, AssertionError) as raised_error:
932
+ all_errors.append(raised_error)
933
+
934
+ first_thread = threading.Thread(
935
+ target=_run_with_working_directory, args=(first_working_directory,)
936
+ )
937
+ second_thread = threading.Thread(
938
+ target=_run_with_working_directory, args=(second_working_directory,)
939
+ )
940
+ first_thread.start()
941
+ second_thread.start()
942
+ first_thread.join(timeout=10)
943
+ second_thread.join(timeout=10)
944
+
945
+ assert all_errors == []
946
+ assert call_log.claude_calls == 2
947
+ assert set(call_log.all_observed_working_directories) == {
948
+ first_working_directory,
949
+ second_working_directory,
950
+ }
951
+ assert chain_runner.chain_subprocess_runner is not None
952
+
953
+
954
+ def test_usage_limit_fallover_delivers_full_prompt_to_each_binary(
955
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
956
+ ) -> None:
957
+ prompt_file, working_directory, run_state_directory = _paths(tmp_path)
958
+ monkeypatch.setattr(
959
+ dispatcher,
960
+ "spawn_preflight_runner",
961
+ lambda **_keyword_arguments: _fallthrough_preflight(REASON_GROK_AUTH_FAILED),
962
+ )
963
+ monkeypatch.setattr(
964
+ dispatcher,
965
+ "spawn_host_profile_detector",
966
+ lambda *all_positionals, **all_keywords: HOST_PROFILE_THIRD_PARTY,
967
+ )
968
+ monkeypatch.setattr(
969
+ chain_runner,
970
+ "load_chain",
971
+ lambda _config_path: [
972
+ chain_runner.ChainEntry(command="claude", extra_args=()),
973
+ chain_runner.ChainEntry(command="claude-ev", extra_args=()),
974
+ ],
975
+ )
976
+ prompt_text_by_command: dict[str, str] = {}
977
+
978
+ def _reading_subprocess_runner(
979
+ all_invocation_tokens: Sequence[str],
980
+ *all_positionals: object,
981
+ **all_keywords: object,
982
+ ) -> subprocess.CompletedProcess[str]:
983
+ del all_positionals
984
+ command_name = str(all_invocation_tokens[0])
985
+ read_prompt = getattr(all_keywords.get("stdin"), "read", None)
986
+ prompt_text_by_command[command_name] = read_prompt() if read_prompt else ""
987
+ is_primary = command_name == "claude"
988
+ return subprocess.CompletedProcess(
989
+ args=list(all_invocation_tokens),
990
+ returncode=(
991
+ FIXTURE_FAILED_RETURNCODE if is_primary else SPAWN_SERVED_EXIT_CODE
992
+ ),
993
+ stdout="usage limit reached" if is_primary else FIXTURE_CLAUDE_STDOUT,
994
+ stderr="",
995
+ )
996
+
997
+ monkeypatch.setattr(
998
+ chain_runner, "chain_subprocess_runner", _reading_subprocess_runner
999
+ )
1000
+
1001
+ outcome = dispatcher.resolve_worker_spawn(
1002
+ role=DEFAULT_ROLE,
1003
+ prompt_file=prompt_file,
1004
+ working_directory=working_directory,
1005
+ timeout_seconds=DEFAULT_WORKER_TIMEOUT_SECONDS,
1006
+ is_claude_tier_enabled=False,
1007
+ run_state_directory=run_state_directory,
1008
+ max_turns=DEFAULT_SPAWN_MAX_TURNS,
1009
+ )
1010
+
1011
+ assert prompt_text_by_command["claude"] == FIXTURE_PROMPT_TEXT
1012
+ assert prompt_text_by_command["claude-ev"] == FIXTURE_PROMPT_TEXT
1013
+ assert outcome.tier_used == TIER_CLAUDE_HEADLESS
1014
+ assert outcome.is_ok is True