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,619 @@
1
+ #!/usr/bin/env python3
2
+ """Dispatch a worker role through the grok then claude fallback tiers.
3
+
4
+ Walk order:
5
+
6
+ 1. Soft-gate preflight for tier 1 (grok headless).
7
+ 2. Tier 1 via ``grok_headless_runner.run_headless_worker``.
8
+ 3. On fall-through, ``detect_host_profile()`` decides the next step.
9
+ On a Claude host with the claude tier disabled, stop with
10
+ ``claude_agent_required`` so the calling skill runs the Agent tool.
11
+ On a third-party host, or when the claude tier is enabled, tier 3 runs
12
+ ``claude_chain_runner.run_claude`` with ``-p --output-format json --agent
13
+ <stem>``, prompt body on stdin from the prompt file, and subprocess
14
+ ``cwd`` set to the caller's working directory.
15
+
16
+ Import ``resolve_worker_spawn`` for the outcome object, or run as a CLI::
17
+
18
+ python resolve_worker_spawn.py --role <role> --prompt-file <path>
19
+ --cwd <dir> --timeout-seconds N --run-temp-dir <dir>
20
+ [--enable-claude-tier]
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import argparse
26
+ import json
27
+ import subprocess
28
+ import sys
29
+ import threading
30
+ from collections.abc import Callable, Sequence
31
+ from dataclasses import dataclass
32
+ from pathlib import Path
33
+ from typing import IO
34
+
35
+ if str(Path(__file__).resolve().parent) not in sys.path:
36
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
37
+
38
+ _advisor_scripts_path = str(
39
+ Path(__file__).resolve().parent.parent / "_shared" / "advisor" / "scripts"
40
+ )
41
+ if _advisor_scripts_path not in sys.path:
42
+ sys.path.insert(0, _advisor_scripts_path)
43
+
44
+ from advisor_scripts_constants.model_tier_run_validator_constants import ( # noqa: E402
45
+ HOST_PROFILE_CLAUDE,
46
+ )
47
+ import claude_chain_runner as chain_runner # noqa: E402
48
+ from claude_chain_runner import ( # noqa: E402
49
+ ChainConfigurationError,
50
+ ChainInvocationOutcome,
51
+ run_claude,
52
+ )
53
+ from dev_env_scripts_constants.grok_worker_constants import ( # noqa: E402
54
+ AGENT_FLAG,
55
+ ALL_AGENT_FILENAMES_BY_ROLE,
56
+ ATTEMPT_KEY_OK,
57
+ ATTEMPT_KEY_REASON,
58
+ ATTEMPT_KEY_TIER,
59
+ CLI_ENABLE_CLAUDE_TIER_FLAG,
60
+ CLI_ROLE_FLAG,
61
+ CLI_RUN_STATE_DIR_FLAG,
62
+ CLI_TIMEOUT_FLAG,
63
+ CWD_FLAG,
64
+ DEFAULT_ROLE,
65
+ DEFAULT_SPAWN_MAX_TURNS,
66
+ DEFAULT_WORKER_TIMEOUT_SECONDS,
67
+ EMPTY_OUTPUT,
68
+ OUTPUT_FORMAT_FLAG,
69
+ OUTPUT_FORMAT_JSON,
70
+ PROMPT_FILE_FLAG,
71
+ REASON_CLAUDE_AGENT_REQUIRED,
72
+ REASON_PROMPT_FILE_MISSING,
73
+ RESULT_KEY_ATTEMPTS,
74
+ RESULT_KEY_OK,
75
+ RESULT_KEY_OUTPUT,
76
+ RESULT_KEY_RETURNCODE,
77
+ RESULT_KEY_TIER_USED,
78
+ SINGLE_TURN_FLAG,
79
+ SPAWN_CONFIG_ERROR_EXIT_CODE,
80
+ SPAWN_EXHAUSTED_EXIT_CODE,
81
+ SPAWN_SERVED_EXIT_CODE,
82
+ TIER_CLAUDE_AGENT,
83
+ TIER_CLAUDE_HEADLESS,
84
+ TIER_GROK,
85
+ UTF8_ENCODING,
86
+ )
87
+ from grok_headless_runner import GrokRunnerOutcome, run_headless_worker # noqa: E402
88
+ from grok_worker_preflight import PreflightOutcome, run_preflight # noqa: E402
89
+ from tier_model_ids import detect_host_profile # noqa: E402
90
+
91
+ _HEADLESS_CHAIN_RUNNER_LOCK = threading.Lock()
92
+
93
+
94
+ def _headless_chain_runner_lock() -> threading.Lock:
95
+ """Return the module lock that serializes headless chain-runner swaps."""
96
+ return _HEADLESS_CHAIN_RUNNER_LOCK
97
+
98
+
99
+ @dataclass(frozen=True)
100
+ class SpawnAttempt:
101
+ """One tier try recorded in the dispatcher attempts trail."""
102
+
103
+ tier: int
104
+ is_ok: bool
105
+ reason: str | None
106
+
107
+
108
+ @dataclass(frozen=True)
109
+ class SpawnOutcome:
110
+ """Outcome of walking the worker-spawn tier chain.
111
+
112
+ ``tier_used`` is the tier number that served the call, or ``None`` when no
113
+ tier served. ``all_attempts`` lists every tier tried in order.
114
+ ``captured_stdout`` holds the serving tier's stdout (or empty).
115
+ ``is_config_error`` is True when the walk stopped on a configuration fault
116
+ such as a missing or unreadable prompt file.
117
+ """
118
+
119
+ tier_used: int | None
120
+ is_ok: bool
121
+ all_attempts: tuple[SpawnAttempt, ...]
122
+ captured_stdout: str
123
+ returncode: int
124
+ is_config_error: bool = False
125
+
126
+
127
+ spawn_preflight_runner = run_preflight
128
+ spawn_grok_runner = run_headless_worker
129
+ spawn_claude_runner = run_claude
130
+ spawn_host_profile_detector = detect_host_profile
131
+
132
+
133
+ TextCapturingSubprocessRunner = Callable[
134
+ ...,
135
+ subprocess.CompletedProcess[str],
136
+ ]
137
+
138
+
139
+ def _attempt(tier: int, *, is_ok: bool, reason: str | None) -> SpawnAttempt:
140
+ return SpawnAttempt(tier=tier, is_ok=is_ok, reason=reason)
141
+
142
+
143
+ def _primary_agent_name_for_role(role: str) -> str:
144
+ all_agent_filenames = ALL_AGENT_FILENAMES_BY_ROLE.get(role)
145
+ if all_agent_filenames is None:
146
+ return role
147
+ if not all_agent_filenames:
148
+ return role
149
+ return Path(all_agent_filenames[0]).stem
150
+
151
+
152
+ def _build_claude_arguments(*, agent_name: str) -> list[str]:
153
+ return [
154
+ SINGLE_TURN_FLAG,
155
+ OUTPUT_FORMAT_FLAG,
156
+ OUTPUT_FORMAT_JSON,
157
+ AGENT_FLAG,
158
+ agent_name,
159
+ ]
160
+
161
+
162
+ def _timeout_seconds_from_keywords(
163
+ all_keywords: dict[str, object],
164
+ ) -> float | None:
165
+ maybe_timeout = all_keywords.get("timeout")
166
+ if isinstance(maybe_timeout, (int, float)):
167
+ return float(maybe_timeout)
168
+ return None
169
+
170
+
171
+ def _run_claude_with_headless_overrides(
172
+ all_claude_arguments: list[str],
173
+ *,
174
+ timeout_seconds: int,
175
+ working_directory: Path,
176
+ prompt_stdin: IO[str],
177
+ ) -> ChainInvocationOutcome:
178
+ working_directory_path = str(working_directory)
179
+ with _HEADLESS_CHAIN_RUNNER_LOCK:
180
+ previous_runner = chain_runner.chain_subprocess_runner
181
+
182
+ def _runner_with_headless_overrides(
183
+ all_invocation_tokens: Sequence[str],
184
+ *all_positionals: object,
185
+ **all_keywords: object,
186
+ ) -> subprocess.CompletedProcess[str]:
187
+ del all_positionals
188
+ prompt_stdin.seek(0)
189
+ completed_process: subprocess.CompletedProcess[str] = previous_runner(
190
+ all_invocation_tokens,
191
+ capture_output=True,
192
+ text=True,
193
+ timeout=_timeout_seconds_from_keywords(all_keywords),
194
+ check=False,
195
+ stdin=prompt_stdin,
196
+ cwd=working_directory_path,
197
+ )
198
+ return completed_process
199
+
200
+ headless_runner: TextCapturingSubprocessRunner = (
201
+ _runner_with_headless_overrides
202
+ )
203
+ setattr(chain_runner, "chain_subprocess_runner", headless_runner)
204
+ try:
205
+ return spawn_claude_runner(
206
+ all_claude_arguments, timeout_seconds=timeout_seconds
207
+ )
208
+ finally:
209
+ setattr(chain_runner, "chain_subprocess_runner", previous_runner)
210
+
211
+
212
+ def _prompt_file_unreadable_outcome(
213
+ all_attempts: list[SpawnAttempt],
214
+ ) -> SpawnOutcome:
215
+ all_attempts.append(
216
+ _attempt(
217
+ TIER_CLAUDE_HEADLESS,
218
+ is_ok=False,
219
+ reason=REASON_PROMPT_FILE_MISSING,
220
+ )
221
+ )
222
+ return SpawnOutcome(
223
+ tier_used=None,
224
+ is_ok=False,
225
+ all_attempts=tuple(all_attempts),
226
+ captured_stdout=REASON_PROMPT_FILE_MISSING,
227
+ returncode=SPAWN_CONFIG_ERROR_EXIT_CODE,
228
+ is_config_error=True,
229
+ )
230
+
231
+
232
+ def _claude_agent_required_outcome(
233
+ all_attempts: list[SpawnAttempt], *, returncode: int
234
+ ) -> SpawnOutcome:
235
+ all_attempts.append(
236
+ _attempt(
237
+ TIER_CLAUDE_AGENT,
238
+ is_ok=False,
239
+ reason=REASON_CLAUDE_AGENT_REQUIRED,
240
+ )
241
+ )
242
+ return SpawnOutcome(
243
+ tier_used=None,
244
+ is_ok=False,
245
+ all_attempts=tuple(all_attempts),
246
+ captured_stdout=EMPTY_OUTPUT,
247
+ returncode=returncode,
248
+ )
249
+
250
+
251
+ def _served_claude_outcome(
252
+ all_attempts: list[SpawnAttempt],
253
+ chain_outcome: ChainInvocationOutcome,
254
+ ) -> SpawnOutcome:
255
+ is_served = chain_outcome.served_command is not None
256
+ is_ok = is_served and chain_outcome.returncode == SPAWN_SERVED_EXIT_CODE
257
+ all_attempts.append(_attempt(TIER_CLAUDE_HEADLESS, is_ok=is_ok, reason=None))
258
+ if not is_served:
259
+ return SpawnOutcome(
260
+ tier_used=None,
261
+ is_ok=False,
262
+ all_attempts=tuple(all_attempts),
263
+ captured_stdout=chain_outcome.stdout,
264
+ returncode=chain_outcome.returncode,
265
+ )
266
+ return SpawnOutcome(
267
+ tier_used=TIER_CLAUDE_HEADLESS,
268
+ is_ok=is_ok,
269
+ all_attempts=tuple(all_attempts),
270
+ captured_stdout=chain_outcome.stdout,
271
+ returncode=chain_outcome.returncode,
272
+ )
273
+
274
+
275
+ def _run_tier_claude_headless(
276
+ *,
277
+ role: str,
278
+ prompt_file: Path,
279
+ working_directory: Path,
280
+ timeout_seconds: int,
281
+ all_attempts: list[SpawnAttempt],
282
+ ) -> SpawnOutcome:
283
+ agent_name = _primary_agent_name_for_role(role)
284
+ all_claude_arguments = _build_claude_arguments(agent_name=agent_name)
285
+ try:
286
+ prompt_stdin = prompt_file.open(encoding=UTF8_ENCODING)
287
+ except OSError:
288
+ return _prompt_file_unreadable_outcome(all_attempts)
289
+ try:
290
+ chain_outcome = _run_claude_with_headless_overrides(
291
+ all_claude_arguments,
292
+ timeout_seconds=timeout_seconds,
293
+ working_directory=working_directory,
294
+ prompt_stdin=prompt_stdin,
295
+ )
296
+ finally:
297
+ prompt_stdin.close()
298
+ return _served_claude_outcome(all_attempts, chain_outcome)
299
+
300
+
301
+ def _after_grok_fallthrough(
302
+ *,
303
+ role: str,
304
+ prompt_file: Path,
305
+ working_directory: Path,
306
+ timeout_seconds: int,
307
+ is_claude_tier_enabled: bool,
308
+ all_attempts: list[SpawnAttempt],
309
+ fallthrough_returncode: int,
310
+ ) -> SpawnOutcome:
311
+ host_profile = spawn_host_profile_detector()
312
+ is_claude_host = host_profile == HOST_PROFILE_CLAUDE
313
+ if is_claude_host and not is_claude_tier_enabled:
314
+ return _claude_agent_required_outcome(
315
+ all_attempts, returncode=fallthrough_returncode
316
+ )
317
+ return _run_tier_claude_headless(
318
+ role=role,
319
+ prompt_file=prompt_file,
320
+ working_directory=working_directory,
321
+ timeout_seconds=timeout_seconds,
322
+ all_attempts=all_attempts,
323
+ )
324
+
325
+
326
+ def _run_tier_grok(
327
+ *,
328
+ role: str,
329
+ prompt_file: Path,
330
+ working_directory: Path,
331
+ run_state_directory: Path,
332
+ max_turns: int,
333
+ timeout_seconds: int,
334
+ ) -> GrokRunnerOutcome:
335
+ agent_name = _primary_agent_name_for_role(role)
336
+ return spawn_grok_runner(
337
+ prompt_file=prompt_file,
338
+ working_directory=working_directory,
339
+ run_state_directory=run_state_directory,
340
+ max_turns=max_turns,
341
+ timeout_seconds=timeout_seconds,
342
+ agent_name=agent_name,
343
+ )
344
+
345
+
346
+ def _record_preflight_fallthrough(
347
+ preflight_outcome: PreflightOutcome,
348
+ *,
349
+ role: str,
350
+ prompt_file: Path,
351
+ working_directory: Path,
352
+ timeout_seconds: int,
353
+ is_claude_tier_enabled: bool,
354
+ ) -> SpawnOutcome:
355
+ all_attempts = [_attempt(TIER_GROK, is_ok=False, reason=preflight_outcome.reason)]
356
+ return _after_grok_fallthrough(
357
+ role=role,
358
+ prompt_file=prompt_file,
359
+ working_directory=working_directory,
360
+ timeout_seconds=timeout_seconds,
361
+ is_claude_tier_enabled=is_claude_tier_enabled,
362
+ all_attempts=all_attempts,
363
+ fallthrough_returncode=SPAWN_EXHAUSTED_EXIT_CODE,
364
+ )
365
+
366
+
367
+ def _record_grok_success(
368
+ grok_outcome: GrokRunnerOutcome,
369
+ ) -> SpawnOutcome:
370
+ return SpawnOutcome(
371
+ tier_used=TIER_GROK,
372
+ is_ok=True,
373
+ all_attempts=(_attempt(TIER_GROK, is_ok=True, reason=None),),
374
+ captured_stdout=grok_outcome.stdout,
375
+ returncode=grok_outcome.returncode,
376
+ )
377
+
378
+
379
+ def _record_grok_fallthrough(
380
+ grok_outcome: GrokRunnerOutcome,
381
+ *,
382
+ role: str,
383
+ prompt_file: Path,
384
+ working_directory: Path,
385
+ timeout_seconds: int,
386
+ is_claude_tier_enabled: bool,
387
+ ) -> SpawnOutcome:
388
+ all_attempts = [
389
+ _attempt(
390
+ TIER_GROK,
391
+ is_ok=False,
392
+ reason=grok_outcome.classification,
393
+ )
394
+ ]
395
+ return _after_grok_fallthrough(
396
+ role=role,
397
+ prompt_file=prompt_file,
398
+ working_directory=working_directory,
399
+ timeout_seconds=timeout_seconds,
400
+ is_claude_tier_enabled=is_claude_tier_enabled,
401
+ all_attempts=all_attempts,
402
+ fallthrough_returncode=grok_outcome.returncode,
403
+ )
404
+
405
+
406
+ def resolve_worker_spawn(
407
+ *,
408
+ role: str,
409
+ prompt_file: Path,
410
+ working_directory: Path,
411
+ timeout_seconds: int,
412
+ is_claude_tier_enabled: bool,
413
+ run_state_directory: Path,
414
+ max_turns: int,
415
+ ) -> SpawnOutcome:
416
+ """Walk the worker-spawn tiers and return the structured outcome.
417
+
418
+ Args:
419
+ role: Worker role name for preflight; mapped to a primary agent stem.
420
+ prompt_file: Path to the prompt file for headless workers.
421
+ working_directory: Working directory for grok and claude headless tiers.
422
+ timeout_seconds: Timeout applied to each tier invocation.
423
+ is_claude_tier_enabled: When True, allow tier 3 on a Claude host.
424
+ run_state_directory: Run-scoped directory for leader sockets and cache.
425
+ max_turns: Maximum agent turns for the headless grok worker.
426
+
427
+ Returns:
428
+ The dispatcher outcome including the ordered attempts trail.
429
+ """
430
+ preflight_outcome: PreflightOutcome = spawn_preflight_runner(
431
+ role=role,
432
+ should_ping=False,
433
+ run_state_directory=run_state_directory,
434
+ )
435
+ if not preflight_outcome.is_usable:
436
+ return _record_preflight_fallthrough(
437
+ preflight_outcome,
438
+ role=role,
439
+ prompt_file=prompt_file,
440
+ working_directory=working_directory,
441
+ timeout_seconds=timeout_seconds,
442
+ is_claude_tier_enabled=is_claude_tier_enabled,
443
+ )
444
+
445
+ grok_outcome = _run_tier_grok(
446
+ role=role,
447
+ prompt_file=prompt_file,
448
+ working_directory=working_directory,
449
+ run_state_directory=run_state_directory,
450
+ max_turns=max_turns,
451
+ timeout_seconds=timeout_seconds,
452
+ )
453
+ if grok_outcome.is_ok:
454
+ return _record_grok_success(grok_outcome)
455
+ return _record_grok_fallthrough(
456
+ grok_outcome,
457
+ role=role,
458
+ prompt_file=prompt_file,
459
+ working_directory=working_directory,
460
+ timeout_seconds=timeout_seconds,
461
+ is_claude_tier_enabled=is_claude_tier_enabled,
462
+ )
463
+
464
+
465
+ def encode_spawn_outcome(spawn_outcome: SpawnOutcome) -> dict[str, object]:
466
+ """Encode a spawn outcome as the JSON-serializable payload.
467
+
468
+ Args:
469
+ spawn_outcome: The dispatcher outcome to encode.
470
+
471
+ Returns:
472
+ A plain dict matching the CLI JSON shape.
473
+ """
474
+ all_encoded_attempts = [
475
+ {
476
+ ATTEMPT_KEY_TIER: each_attempt.tier,
477
+ ATTEMPT_KEY_OK: each_attempt.is_ok,
478
+ ATTEMPT_KEY_REASON: each_attempt.reason,
479
+ }
480
+ for each_attempt in spawn_outcome.all_attempts
481
+ ]
482
+ return {
483
+ RESULT_KEY_TIER_USED: spawn_outcome.tier_used,
484
+ RESULT_KEY_OK: spawn_outcome.is_ok,
485
+ RESULT_KEY_ATTEMPTS: all_encoded_attempts,
486
+ RESULT_KEY_OUTPUT: spawn_outcome.captured_stdout,
487
+ RESULT_KEY_RETURNCODE: spawn_outcome.returncode,
488
+ }
489
+
490
+
491
+ def _build_argument_parser() -> argparse.ArgumentParser:
492
+ parser = argparse.ArgumentParser(
493
+ description="Dispatch a worker role through the grok then claude tiers."
494
+ )
495
+ parser.add_argument(
496
+ CLI_ROLE_FLAG,
497
+ default=DEFAULT_ROLE,
498
+ help="Worker role name for preflight; mapped to a primary agent for grok.",
499
+ )
500
+ parser.add_argument(
501
+ PROMPT_FILE_FLAG,
502
+ dest="prompt_file",
503
+ required=True,
504
+ type=Path,
505
+ help="Path to the prompt file for headless workers.",
506
+ )
507
+ parser.add_argument(
508
+ CWD_FLAG,
509
+ dest="working_directory",
510
+ required=True,
511
+ type=Path,
512
+ help="Working directory for headless grok and claude workers.",
513
+ )
514
+ parser.add_argument(
515
+ CLI_TIMEOUT_FLAG,
516
+ dest="timeout_seconds",
517
+ type=int,
518
+ default=DEFAULT_WORKER_TIMEOUT_SECONDS,
519
+ help="Timeout in seconds applied to each tier invocation.",
520
+ )
521
+ parser.add_argument(
522
+ CLI_ENABLE_CLAUDE_TIER_FLAG,
523
+ dest="is_claude_tier_enabled",
524
+ action="store_true",
525
+ default=False,
526
+ help="Enable the headless claude chain tier on a Claude host.",
527
+ )
528
+ parser.add_argument(
529
+ CLI_RUN_STATE_DIR_FLAG,
530
+ dest="run_state_directory",
531
+ required=True,
532
+ type=Path,
533
+ help="Run-scoped state directory for leader sockets and cache.",
534
+ )
535
+ return parser
536
+
537
+
538
+ def _exit_code_for_outcome(
539
+ spawn_outcome: SpawnOutcome, *, is_config_error: bool
540
+ ) -> int:
541
+ if is_config_error or spawn_outcome.is_config_error:
542
+ return SPAWN_CONFIG_ERROR_EXIT_CODE
543
+ if spawn_outcome.tier_used is None:
544
+ return SPAWN_EXHAUSTED_EXIT_CODE
545
+ return SPAWN_SERVED_EXIT_CODE
546
+
547
+
548
+ def _config_error_outcome(configuration_error: ChainConfigurationError) -> SpawnOutcome:
549
+ return SpawnOutcome(
550
+ tier_used=None,
551
+ is_ok=False,
552
+ all_attempts=(),
553
+ captured_stdout=str(configuration_error),
554
+ returncode=SPAWN_CONFIG_ERROR_EXIT_CODE,
555
+ is_config_error=True,
556
+ )
557
+
558
+
559
+ def _missing_prompt_file_outcome() -> SpawnOutcome:
560
+ return SpawnOutcome(
561
+ tier_used=None,
562
+ is_ok=False,
563
+ all_attempts=(),
564
+ captured_stdout=REASON_PROMPT_FILE_MISSING,
565
+ returncode=SPAWN_CONFIG_ERROR_EXIT_CODE,
566
+ is_config_error=True,
567
+ )
568
+
569
+
570
+ def _write_spawn_outcome_and_exit_code(
571
+ spawn_outcome: SpawnOutcome, *, is_config_error: bool
572
+ ) -> int:
573
+ encoded_payload = encode_spawn_outcome(spawn_outcome)
574
+ sys.stdout.write(json.dumps(encoded_payload) + "\n")
575
+ return _exit_code_for_outcome(
576
+ spawn_outcome,
577
+ is_config_error=is_config_error or spawn_outcome.is_config_error,
578
+ )
579
+
580
+
581
+ def main(all_command_arguments: list[str]) -> int:
582
+ """Run the dispatcher for CLI arguments and print the JSON outcome.
583
+
584
+ Args:
585
+ all_command_arguments: The argument vector after the program name.
586
+
587
+ Returns:
588
+ ``0`` when a tier served, ``2`` when exhausted, ``3`` on config error.
589
+ """
590
+ parser = _build_argument_parser()
591
+ parsed_arguments = parser.parse_args(all_command_arguments)
592
+ run_state_directory = parsed_arguments.run_state_directory
593
+ run_state_directory.mkdir(parents=True, exist_ok=True)
594
+ if not parsed_arguments.prompt_file.is_file():
595
+ return _write_spawn_outcome_and_exit_code(
596
+ _missing_prompt_file_outcome(),
597
+ is_config_error=True,
598
+ )
599
+ is_config_error = False
600
+ try:
601
+ spawn_outcome = resolve_worker_spawn(
602
+ role=parsed_arguments.role,
603
+ prompt_file=parsed_arguments.prompt_file,
604
+ working_directory=parsed_arguments.working_directory,
605
+ timeout_seconds=parsed_arguments.timeout_seconds,
606
+ is_claude_tier_enabled=parsed_arguments.is_claude_tier_enabled,
607
+ run_state_directory=run_state_directory,
608
+ max_turns=DEFAULT_SPAWN_MAX_TURNS,
609
+ )
610
+ except ChainConfigurationError as configuration_error:
611
+ is_config_error = True
612
+ spawn_outcome = _config_error_outcome(configuration_error)
613
+ return _write_spawn_outcome_and_exit_code(
614
+ spawn_outcome, is_config_error=is_config_error
615
+ )
616
+
617
+
618
+ if __name__ == "__main__":
619
+ sys.exit(main(sys.argv[1:]))