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,294 @@
1
+ #!/usr/bin/env python3
2
+ """Run one worker role as a headless grok process and classify the outcome.
3
+
4
+ Builds a headless argv, mints a unique ``--leader-socket`` path under the
5
+ caller-supplied run state directory, captures stdout/stderr/returncode, kills
6
+ the process on timeout, and classifies failures via signature lists in
7
+ ``dev_env_scripts_constants.grok_worker_constants``.
8
+
9
+ Dual-match policy matches preflight: when both usage and auth signatures appear
10
+ in the same streams, auth wins (``CLASSIFICATION_AUTH_FAILURE``).
11
+
12
+ Import ``run_headless_worker`` for the outcome object::
13
+
14
+ outcome = run_headless_worker(
15
+ prompt_file=path,
16
+ working_directory=cwd,
17
+ run_state_directory=run_dir,
18
+ max_turns=8,
19
+ timeout_seconds=600,
20
+ agent_name="code-quality-agent",
21
+ )
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import subprocess
27
+ import uuid
28
+ from dataclasses import dataclass
29
+ from pathlib import Path
30
+
31
+ from dev_env_scripts_constants.grok_worker_constants import (
32
+ AGENT_FLAG,
33
+ ALL_AUTH_FAILURE_SIGNATURES,
34
+ ALL_USAGE_LIMIT_SIGNATURES,
35
+ ALWAYS_APPROVE_FLAG,
36
+ CLASSIFICATION_AUTH_FAILURE,
37
+ CLASSIFICATION_ERROR,
38
+ CLASSIFICATION_OK,
39
+ CLASSIFICATION_STREAM_JOIN_SEPARATOR,
40
+ CLASSIFICATION_TIMEOUT,
41
+ CLASSIFICATION_USAGE_LIMIT,
42
+ CWD_FLAG,
43
+ GROK_BINARY_NAME,
44
+ GROK_BINARY_NOT_FOUND_STDERR,
45
+ GROK_MODEL_PIN,
46
+ KILL_GRACE_TIMEOUT_SECONDS,
47
+ LAUNCH_FAILURE_RETURN_CODE,
48
+ LAUNCH_FAILURE_STDERR_PREFIX,
49
+ LEADER_SOCKET_FILENAME_PREFIX,
50
+ LEADER_SOCKET_FILENAME_SUFFIX,
51
+ LEADER_SOCKET_FLAG,
52
+ MAX_TURNS_FLAG,
53
+ MISSING_BINARY_RETURN_CODE,
54
+ MODEL_FLAG,
55
+ OUTPUT_FORMAT_FLAG,
56
+ OUTPUT_FORMAT_JSON,
57
+ PROMPT_FILE_FLAG,
58
+ TIMEOUT_RETURN_CODE,
59
+ UTF8_DECODE_ERRORS,
60
+ UTF8_ENCODING,
61
+ )
62
+
63
+ runner_popen = subprocess.Popen
64
+
65
+
66
+ @dataclass(frozen=True)
67
+ class GrokRunnerOutcome:
68
+ """Outcome of one headless grok worker invocation.
69
+
70
+ ``classification`` is one of the ``CLASSIFICATION_*`` constants from
71
+ ``grok_worker_constants``.
72
+ """
73
+
74
+ is_ok: bool
75
+ returncode: int
76
+ classification: str
77
+ stdout: str
78
+ stderr: str
79
+
80
+
81
+ def _mint_leader_socket_path(run_state_directory: Path) -> Path:
82
+ unique_token = uuid.uuid4().hex
83
+ socket_filename = (
84
+ f"{LEADER_SOCKET_FILENAME_PREFIX}{unique_token}{LEADER_SOCKET_FILENAME_SUFFIX}"
85
+ )
86
+ return run_state_directory / socket_filename
87
+
88
+
89
+ def _build_invocation(
90
+ *,
91
+ prompt_file: Path,
92
+ working_directory: Path,
93
+ max_turns: int,
94
+ leader_socket_path: Path,
95
+ agent_name: str | None,
96
+ all_extra_arguments: tuple[str, ...] = (),
97
+ ) -> list[str]:
98
+ all_arguments = [
99
+ GROK_BINARY_NAME,
100
+ PROMPT_FILE_FLAG,
101
+ str(prompt_file),
102
+ CWD_FLAG,
103
+ str(working_directory),
104
+ OUTPUT_FORMAT_FLAG,
105
+ OUTPUT_FORMAT_JSON,
106
+ ALWAYS_APPROVE_FLAG,
107
+ MAX_TURNS_FLAG,
108
+ str(max_turns),
109
+ LEADER_SOCKET_FLAG,
110
+ str(leader_socket_path),
111
+ ]
112
+ if agent_name:
113
+ all_arguments.extend([AGENT_FLAG, agent_name])
114
+ if GROK_MODEL_PIN:
115
+ all_arguments.extend([MODEL_FLAG, GROK_MODEL_PIN])
116
+ all_arguments.extend(all_extra_arguments)
117
+ return all_arguments
118
+
119
+
120
+ def _combined_text(stdout_text: str, stderr_text: str) -> str:
121
+ return CLASSIFICATION_STREAM_JOIN_SEPARATOR.join(
122
+ (stdout_text, stderr_text)
123
+ ).lower()
124
+
125
+
126
+ def _matches_any_signature(combined_text: str, all_signatures: tuple[str, ...]) -> bool:
127
+ return any(each_signature in combined_text for each_signature in all_signatures)
128
+
129
+
130
+ def _classify_completion(
131
+ returncode: int, stdout_text: str, stderr_text: str
132
+ ) -> GrokRunnerOutcome:
133
+ if returncode == 0:
134
+ return GrokRunnerOutcome(
135
+ is_ok=True,
136
+ returncode=returncode,
137
+ classification=CLASSIFICATION_OK,
138
+ stdout=stdout_text,
139
+ stderr=stderr_text,
140
+ )
141
+ combined_text = _combined_text(stdout_text, stderr_text)
142
+ is_usage_limit = _matches_any_signature(combined_text, ALL_USAGE_LIMIT_SIGNATURES)
143
+ is_auth_failure = _matches_any_signature(combined_text, ALL_AUTH_FAILURE_SIGNATURES)
144
+ if is_usage_limit and not is_auth_failure:
145
+ return GrokRunnerOutcome(
146
+ is_ok=False,
147
+ returncode=returncode,
148
+ classification=CLASSIFICATION_USAGE_LIMIT,
149
+ stdout=stdout_text,
150
+ stderr=stderr_text,
151
+ )
152
+ if is_auth_failure:
153
+ return GrokRunnerOutcome(
154
+ is_ok=False,
155
+ returncode=returncode,
156
+ classification=CLASSIFICATION_AUTH_FAILURE,
157
+ stdout=stdout_text,
158
+ stderr=stderr_text,
159
+ )
160
+ return GrokRunnerOutcome(
161
+ is_ok=False,
162
+ returncode=returncode,
163
+ classification=CLASSIFICATION_ERROR,
164
+ stdout=stdout_text,
165
+ stderr=stderr_text,
166
+ )
167
+
168
+
169
+ def _normalize_stream(stream_payload: str | bytes | None) -> str:
170
+ if stream_payload is None:
171
+ return ""
172
+ if isinstance(stream_payload, bytes):
173
+ return stream_payload.decode(UTF8_ENCODING, errors=UTF8_DECODE_ERRORS)
174
+ return stream_payload
175
+
176
+
177
+ def _resolve_returncode(process: subprocess.Popen[str]) -> int:
178
+ if process.returncode is not None:
179
+ return process.returncode
180
+ return TIMEOUT_RETURN_CODE
181
+
182
+
183
+ def _timeout_outcome(process: subprocess.Popen[str]) -> GrokRunnerOutcome:
184
+ process.kill()
185
+ try:
186
+ captured_stdout, captured_stderr = process.communicate(
187
+ timeout=KILL_GRACE_TIMEOUT_SECONDS
188
+ )
189
+ except subprocess.TimeoutExpired:
190
+ captured_stdout, captured_stderr = "", ""
191
+ stdout_text = _normalize_stream(captured_stdout)
192
+ stderr_text = _normalize_stream(captured_stderr)
193
+ returncode = _resolve_returncode(process)
194
+ if returncode == 0:
195
+ return _classify_completion(returncode, stdout_text, stderr_text)
196
+ return GrokRunnerOutcome(
197
+ is_ok=False,
198
+ returncode=returncode,
199
+ classification=CLASSIFICATION_TIMEOUT,
200
+ stdout=stdout_text,
201
+ stderr=stderr_text,
202
+ )
203
+
204
+
205
+ def _missing_binary_outcome() -> GrokRunnerOutcome:
206
+ return GrokRunnerOutcome(
207
+ is_ok=False,
208
+ returncode=MISSING_BINARY_RETURN_CODE,
209
+ classification=CLASSIFICATION_ERROR,
210
+ stdout="",
211
+ stderr=GROK_BINARY_NOT_FOUND_STDERR,
212
+ )
213
+
214
+
215
+ def _launch_failure_outcome(launch_error: OSError) -> GrokRunnerOutcome:
216
+ diagnostic_text = f"{LAUNCH_FAILURE_STDERR_PREFIX}{launch_error}"
217
+ return GrokRunnerOutcome(
218
+ is_ok=False,
219
+ returncode=LAUNCH_FAILURE_RETURN_CODE,
220
+ classification=CLASSIFICATION_ERROR,
221
+ stdout="",
222
+ stderr=diagnostic_text,
223
+ )
224
+
225
+
226
+ def _invoke_process(
227
+ all_arguments: list[str], *, timeout_seconds: int
228
+ ) -> GrokRunnerOutcome:
229
+ try:
230
+ process = runner_popen(
231
+ all_arguments,
232
+ stdout=subprocess.PIPE,
233
+ stderr=subprocess.PIPE,
234
+ text=True,
235
+ encoding=UTF8_ENCODING,
236
+ errors=UTF8_DECODE_ERRORS,
237
+ )
238
+ except FileNotFoundError:
239
+ return _missing_binary_outcome()
240
+ except OSError as launch_error:
241
+ return _launch_failure_outcome(launch_error)
242
+ try:
243
+ captured_stdout, captured_stderr = process.communicate(timeout=timeout_seconds)
244
+ except subprocess.TimeoutExpired:
245
+ return _timeout_outcome(process)
246
+ stdout_text = _normalize_stream(captured_stdout)
247
+ stderr_text = _normalize_stream(captured_stderr)
248
+ returncode = _resolve_returncode(process)
249
+ return _classify_completion(returncode, stdout_text, stderr_text)
250
+
251
+
252
+ def run_headless_worker(
253
+ *,
254
+ prompt_file: Path,
255
+ working_directory: Path,
256
+ run_state_directory: Path,
257
+ max_turns: int,
258
+ timeout_seconds: int,
259
+ agent_name: str | None = None,
260
+ leader_socket_path: Path | None = None,
261
+ all_extra_arguments: tuple[str, ...] = (),
262
+ ) -> GrokRunnerOutcome:
263
+ """Run one headless grok worker and classify the process outcome.
264
+
265
+ Args:
266
+ prompt_file: Path to the prompt file passed via ``--prompt-file``.
267
+ working_directory: Working directory passed via ``--cwd``.
268
+ run_state_directory: Run-scoped directory the leader socket is minted
269
+ under. Read only when ``leader_socket_path`` is omitted.
270
+ max_turns: Maximum agent turns passed via ``--max-turns``.
271
+ timeout_seconds: Seconds before the process is killed on expiry.
272
+ agent_name: Optional role agent name passed via ``--agent``.
273
+ leader_socket_path: Optional pre-minted leader socket path. When omitted,
274
+ a unique path is minted under ``run_state_directory``.
275
+ all_extra_arguments: Extra CLI tokens appended after the base argv
276
+ (tool-profile flags, debug file, and similar).
277
+
278
+ Returns:
279
+ The classified outcome including return code and captured streams.
280
+ """
281
+ resolved_leader_socket_path = (
282
+ leader_socket_path
283
+ if leader_socket_path is not None
284
+ else _mint_leader_socket_path(run_state_directory)
285
+ )
286
+ all_arguments = _build_invocation(
287
+ prompt_file=prompt_file,
288
+ working_directory=working_directory,
289
+ max_turns=max_turns,
290
+ leader_socket_path=resolved_leader_socket_path,
291
+ agent_name=agent_name,
292
+ all_extra_arguments=all_extra_arguments,
293
+ )
294
+ return _invoke_process(all_arguments, timeout_seconds=timeout_seconds)
@@ -0,0 +1,410 @@
1
+ #!/usr/bin/env python3
2
+ """Static soft gate for whether tier 1 (grok headless) is usable.
3
+
4
+ Callers continue down their fallback chain on a non-zero exit. A non-zero exit
5
+ is never a run failure for the caller — it is only a fallthrough signal.
6
+
7
+ Checks run in order and stop at the first failure:
8
+
9
+ 1. The ``grok`` binary is resolvable on PATH.
10
+ 2. Auth material is present: ``grok models`` exits 0 with decoded text streams.
11
+ The probe uses its own ``--leader-socket`` path so it never contends with
12
+ running workers.
13
+ 3. ``claude-dev-env`` is installed for the requested role: the install manifest
14
+ exists in the user Claude config directory, and every agent definition file
15
+ that role needs is present under ``agents/``.
16
+
17
+ Opt-in ``--ping`` runs one cached live single-turn call with a run-scoped TTL.
18
+ The cache file lives under the caller-supplied run state directory. Auth failure
19
+ invalidates any cached ping. Usage exhaustion is reported under a distinct
20
+ reason from auth failure.
21
+
22
+ ``grok_auth_failed`` is the catch-all machine-readable reason for every non-usage
23
+ failure: non-zero auth, missing or undecoded stdout/stderr, subprocess launch
24
+ errors, probe timeouts, and non-usage ping failures. Only usage exhaustion uses
25
+ ``grok_usage_exhausted``. Machine-readable reason strings stay stable; do not
26
+ split the catch-all unless a caller contract requires a new token.
27
+
28
+ Library callers of ``run_preflight`` need not create ``run_state_directory``
29
+ first: the library attempts to create it (best-effort mkdir) before writing the
30
+ ping cache. A failed cache write is soft — the soft gate can still report
31
+ usable without a cache file.
32
+
33
+ Stdout is one machine-readable line::
34
+
35
+ grok_preflight: ok
36
+ grok_preflight: fallthrough reason=<reason>
37
+
38
+ Import ``run_preflight`` for the outcome object, or run the module as a CLI::
39
+
40
+ python grok_worker_preflight.py [--role bugteam] [--ping]
41
+ --run-temp-dir <dir>
42
+ """
43
+
44
+ from __future__ import annotations
45
+
46
+ import argparse
47
+ import json
48
+ import shutil
49
+ import subprocess
50
+ import sys
51
+ import time
52
+ from dataclasses import dataclass
53
+ from pathlib import Path
54
+
55
+ from dev_env_scripts_constants.grok_worker_constants import (
56
+ AGENTS_SUBDIRECTORY,
57
+ ALL_AGENT_FILENAMES_BY_ROLE,
58
+ ALL_AUTH_FAILURE_SIGNATURES,
59
+ ALL_USAGE_EXHAUSTION_SIGNATURES,
60
+ AUTH_LEADER_SOCKET_FILENAME,
61
+ CLAUDE_HOME_SUBDIRECTORY,
62
+ CLI_PING_FLAG,
63
+ CLI_ROLE_FLAG,
64
+ CLI_RUN_STATE_DIR_FLAG,
65
+ DEFAULT_AUTH_TIMEOUT_SECONDS,
66
+ DEFAULT_PING_TIMEOUT_SECONDS,
67
+ DEFAULT_ROLE,
68
+ EXIT_FALLTHROUGH,
69
+ EXIT_USABLE,
70
+ GROK_BINARY_NAME,
71
+ LEADER_SOCKET_FLAG,
72
+ MANIFEST_FILENAME,
73
+ MAX_TURNS_FLAG,
74
+ MODELS_SUBCOMMAND,
75
+ PING_CACHE_CHECKED_AT_KEY,
76
+ PING_CACHE_FILENAME,
77
+ PING_CACHE_IS_OK_KEY,
78
+ PING_LEADER_SOCKET_FILENAME,
79
+ PING_MAX_TURNS,
80
+ PING_PROMPT,
81
+ PING_TTL_SECONDS,
82
+ REASON_CLAUDE_DEV_ENV_CONFIG_MISSING,
83
+ REASON_GROK_AUTH_FAILED,
84
+ REASON_GROK_BINARY_MISSING,
85
+ REASON_GROK_USAGE_EXHAUSTED,
86
+ SINGLE_TURN_FLAG,
87
+ STDOUT_FALLTHROUGH_TEMPLATE,
88
+ STDOUT_OK_LINE,
89
+ UTF8_DECODE_ERRORS,
90
+ UTF8_ENCODING,
91
+ )
92
+
93
+ preflight_which = shutil.which
94
+ preflight_subprocess_runner = subprocess.run
95
+ preflight_time = time.time
96
+
97
+
98
+ @dataclass(frozen=True)
99
+ class PreflightOutcome:
100
+ """Outcome of one soft-gate evaluation.
101
+
102
+ ``reason`` is ``None`` when tier 1 is usable. Otherwise it is one of the
103
+ fallthrough reason constants from ``grok_worker_constants``.
104
+ """
105
+
106
+ is_usable: bool
107
+ reason: str | None
108
+
109
+
110
+ def claude_config_home() -> Path:
111
+ """Return the per-user Claude configuration directory."""
112
+ return Path.home() / CLAUDE_HOME_SUBDIRECTORY
113
+
114
+
115
+ def _install_manifest_path() -> Path:
116
+ return claude_config_home() / MANIFEST_FILENAME
117
+
118
+
119
+ def _agents_directory() -> Path:
120
+ return claude_config_home() / AGENTS_SUBDIRECTORY
121
+
122
+
123
+ def _ping_cache_path(run_state_directory: Path) -> Path:
124
+ return run_state_directory / PING_CACHE_FILENAME
125
+
126
+
127
+ def _fallthrough(reason: str) -> PreflightOutcome:
128
+ return PreflightOutcome(is_usable=False, reason=reason)
129
+
130
+
131
+ def _usable() -> PreflightOutcome:
132
+ return PreflightOutcome(is_usable=True, reason=None)
133
+
134
+
135
+ def _combined_completion_text(completion: subprocess.CompletedProcess[str]) -> str:
136
+ stdout_text = completion.stdout if completion.stdout is not None else ""
137
+ stderr_text = completion.stderr if completion.stderr is not None else ""
138
+ return f"{stdout_text}{stderr_text}".lower()
139
+
140
+
141
+ def _has_decoded_text_streams(
142
+ completion: subprocess.CompletedProcess[str],
143
+ ) -> bool:
144
+ return completion.stdout is not None and completion.stderr is not None
145
+
146
+
147
+ def _matches_any_signature(
148
+ completion: subprocess.CompletedProcess[str], all_signatures: tuple[str, ...]
149
+ ) -> bool:
150
+ combined_text = _combined_completion_text(completion)
151
+ return any(
152
+ each_signature in combined_text for each_signature in all_signatures
153
+ )
154
+
155
+
156
+ def _is_usage_exhaustion(completion: subprocess.CompletedProcess[str]) -> bool:
157
+ return _matches_any_signature(completion, ALL_USAGE_EXHAUSTION_SIGNATURES)
158
+
159
+
160
+ def _is_auth_failure_text(completion: subprocess.CompletedProcess[str]) -> bool:
161
+ return _matches_any_signature(completion, ALL_AUTH_FAILURE_SIGNATURES)
162
+
163
+
164
+ def _invalidate_ping_cache(run_state_directory: Path) -> None:
165
+ cache_file = _ping_cache_path(run_state_directory)
166
+ try:
167
+ cache_file.unlink(missing_ok=True)
168
+ except OSError:
169
+ return
170
+
171
+
172
+ def _read_ping_cache(run_state_directory: Path) -> dict[str, object] | None:
173
+ cache_file = _ping_cache_path(run_state_directory)
174
+ if not cache_file.is_file():
175
+ return None
176
+ try:
177
+ parsed_payload = json.loads(cache_file.read_text(encoding=UTF8_ENCODING))
178
+ except (OSError, json.JSONDecodeError):
179
+ return None
180
+ if not isinstance(parsed_payload, dict):
181
+ return None
182
+ return parsed_payload
183
+
184
+
185
+ def _is_ping_cache_fresh(all_cache_payload: dict[str, object], now_timestamp: float) -> bool:
186
+ checked_at = all_cache_payload.get(PING_CACHE_CHECKED_AT_KEY)
187
+ is_ok = all_cache_payload.get(PING_CACHE_IS_OK_KEY)
188
+ if not isinstance(checked_at, (int, float)):
189
+ return False
190
+ if is_ok is not True:
191
+ return False
192
+ age_seconds = now_timestamp - float(checked_at)
193
+ return age_seconds >= 0 and age_seconds <= PING_TTL_SECONDS
194
+
195
+
196
+ def _write_successful_ping_cache(
197
+ run_state_directory: Path, checked_at: float
198
+ ) -> None:
199
+ all_cache_payload = {
200
+ PING_CACHE_CHECKED_AT_KEY: checked_at,
201
+ PING_CACHE_IS_OK_KEY: True,
202
+ }
203
+ try:
204
+ run_state_directory.mkdir(parents=True, exist_ok=True)
205
+ _ping_cache_path(run_state_directory).write_text(
206
+ json.dumps(all_cache_payload), encoding=UTF8_ENCODING
207
+ )
208
+ except OSError:
209
+ return
210
+
211
+
212
+ def _is_grok_binary_resolvable() -> bool:
213
+ return preflight_which(GROK_BINARY_NAME) is not None
214
+
215
+
216
+ def _is_claude_dev_env_config_present(role: str) -> bool:
217
+ if not _install_manifest_path().is_file():
218
+ return False
219
+ all_agent_filenames = ALL_AGENT_FILENAMES_BY_ROLE.get(role)
220
+ if all_agent_filenames is None:
221
+ return False
222
+ agents_root = _agents_directory()
223
+ return all(
224
+ (agents_root / each_filename).is_file()
225
+ for each_filename in all_agent_filenames
226
+ )
227
+
228
+
229
+ def _build_models_invocation(leader_socket_path: Path) -> list[str]:
230
+ return [
231
+ GROK_BINARY_NAME,
232
+ MODELS_SUBCOMMAND,
233
+ LEADER_SOCKET_FLAG,
234
+ str(leader_socket_path),
235
+ ]
236
+
237
+
238
+ def _build_ping_invocation(leader_socket_path: Path) -> list[str]:
239
+ return [
240
+ GROK_BINARY_NAME,
241
+ SINGLE_TURN_FLAG,
242
+ PING_PROMPT,
243
+ MAX_TURNS_FLAG,
244
+ PING_MAX_TURNS,
245
+ LEADER_SOCKET_FLAG,
246
+ str(leader_socket_path),
247
+ ]
248
+
249
+
250
+ def _run_grok_command(
251
+ all_arguments: list[str], *, timeout_seconds: int
252
+ ) -> subprocess.CompletedProcess[str] | None:
253
+ try:
254
+ return preflight_subprocess_runner(
255
+ all_arguments,
256
+ capture_output=True,
257
+ text=True,
258
+ encoding=UTF8_ENCODING,
259
+ errors=UTF8_DECODE_ERRORS,
260
+ timeout=timeout_seconds,
261
+ check=False,
262
+ )
263
+ except OSError:
264
+ return None
265
+ except subprocess.TimeoutExpired:
266
+ return subprocess.CompletedProcess(
267
+ args=all_arguments,
268
+ returncode=EXIT_FALLTHROUGH,
269
+ stdout="",
270
+ stderr="",
271
+ )
272
+
273
+
274
+ def _probe_grok_auth(run_state_directory: Path) -> PreflightOutcome:
275
+ leader_socket_path = run_state_directory / AUTH_LEADER_SOCKET_FILENAME
276
+ completion = _run_grok_command(
277
+ _build_models_invocation(leader_socket_path),
278
+ timeout_seconds=DEFAULT_AUTH_TIMEOUT_SECONDS,
279
+ )
280
+ if (
281
+ completion is None
282
+ or completion.returncode != 0
283
+ or not _has_decoded_text_streams(completion)
284
+ ):
285
+ _invalidate_ping_cache(run_state_directory)
286
+ return _fallthrough(REASON_GROK_AUTH_FAILED)
287
+ return _usable()
288
+
289
+
290
+ def _classify_failed_ping(
291
+ completion: subprocess.CompletedProcess[str],
292
+ run_state_directory: Path,
293
+ ) -> PreflightOutcome:
294
+ if _is_usage_exhaustion(completion) and not _is_auth_failure_text(completion):
295
+ return _fallthrough(REASON_GROK_USAGE_EXHAUSTED)
296
+ _invalidate_ping_cache(run_state_directory)
297
+ return _fallthrough(REASON_GROK_AUTH_FAILED)
298
+
299
+
300
+ def _probe_grok_ping(run_state_directory: Path) -> PreflightOutcome:
301
+ now_timestamp = preflight_time()
302
+ all_cache_payload = _read_ping_cache(run_state_directory)
303
+ if all_cache_payload is not None and _is_ping_cache_fresh(all_cache_payload, now_timestamp):
304
+ return _usable()
305
+ leader_socket_path = run_state_directory / PING_LEADER_SOCKET_FILENAME
306
+ completion = _run_grok_command(
307
+ _build_ping_invocation(leader_socket_path),
308
+ timeout_seconds=DEFAULT_PING_TIMEOUT_SECONDS,
309
+ )
310
+ if completion is None or not _has_decoded_text_streams(completion):
311
+ _invalidate_ping_cache(run_state_directory)
312
+ return _fallthrough(REASON_GROK_AUTH_FAILED)
313
+ if completion.returncode != 0:
314
+ return _classify_failed_ping(completion, run_state_directory)
315
+ _write_successful_ping_cache(run_state_directory, now_timestamp)
316
+ return _usable()
317
+
318
+
319
+ def run_preflight(
320
+ *,
321
+ role: str,
322
+ should_ping: bool,
323
+ run_state_directory: Path,
324
+ ) -> PreflightOutcome:
325
+ """Evaluate whether tier 1 (grok headless) is usable.
326
+
327
+ Args:
328
+ role: Role whose agent definition set must be installed.
329
+ should_ping: When True, run the opt-in cached live single-turn ping.
330
+ run_state_directory: Run-scoped directory for leader sockets and cache.
331
+ Best-effort mkdir when missing; callers need not create it first.
332
+
333
+ Returns:
334
+ The soft-gate outcome. Callers treat a non-usable outcome as fallthrough,
335
+ not as a hard run failure.
336
+ """
337
+ try:
338
+ run_state_directory.mkdir(parents=True, exist_ok=True)
339
+ except OSError:
340
+ pass
341
+ if not _is_grok_binary_resolvable():
342
+ return _fallthrough(REASON_GROK_BINARY_MISSING)
343
+ auth_outcome = _probe_grok_auth(run_state_directory)
344
+ if not auth_outcome.is_usable:
345
+ return auth_outcome
346
+ if not _is_claude_dev_env_config_present(role):
347
+ return _fallthrough(REASON_CLAUDE_DEV_ENV_CONFIG_MISSING)
348
+ if should_ping:
349
+ return _probe_grok_ping(run_state_directory)
350
+ return _usable()
351
+
352
+
353
+ def _format_stdout_line(outcome: PreflightOutcome) -> str:
354
+ if outcome.is_usable:
355
+ return STDOUT_OK_LINE
356
+ return STDOUT_FALLTHROUGH_TEMPLATE.format(reason=outcome.reason)
357
+
358
+
359
+ def _build_argument_parser() -> argparse.ArgumentParser:
360
+ parser = argparse.ArgumentParser(
361
+ description=(
362
+ "Soft gate that decides whether tier 1 (grok headless) is usable."
363
+ )
364
+ )
365
+ parser.add_argument(
366
+ CLI_ROLE_FLAG,
367
+ default=DEFAULT_ROLE,
368
+ help="Role whose agent definition files must be present.",
369
+ )
370
+ parser.add_argument(
371
+ CLI_PING_FLAG,
372
+ action="store_true",
373
+ default=False,
374
+ help="Run one cached live single-turn ping after the static checks.",
375
+ )
376
+ parser.add_argument(
377
+ CLI_RUN_STATE_DIR_FLAG,
378
+ dest="run_state_directory",
379
+ required=True,
380
+ type=Path,
381
+ help="Run-scoped state directory for leader sockets and the ping cache.",
382
+ )
383
+ return parser
384
+
385
+
386
+ def main(all_command_arguments: list[str]) -> int:
387
+ """Run the soft gate for CLI arguments and return the process exit code.
388
+
389
+ Args:
390
+ all_command_arguments: The argument vector after the program name.
391
+
392
+ Returns:
393
+ ``0`` when tier 1 is usable; non-zero when callers should fall through.
394
+ """
395
+ parser = _build_argument_parser()
396
+ parsed_arguments = parser.parse_args(all_command_arguments)
397
+ run_state_directory = parsed_arguments.run_state_directory
398
+ outcome = run_preflight(
399
+ role=parsed_arguments.role,
400
+ should_ping=parsed_arguments.ping,
401
+ run_state_directory=run_state_directory,
402
+ )
403
+ print(_format_stdout_line(outcome))
404
+ if outcome.is_usable:
405
+ return EXIT_USABLE
406
+ return EXIT_FALLTHROUGH
407
+
408
+
409
+ if __name__ == "__main__":
410
+ sys.exit(main(sys.argv[1:]))