claude-dev-env 1.95.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (241) hide show
  1. package/_shared/advisor/CLAUDE.md +2 -2
  2. package/_shared/advisor/advisor-protocol.md +20 -20
  3. package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +15 -12
  4. package/_shared/advisor/scripts/model_tier_run_validator.py +11 -10
  5. package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +25 -19
  6. package/_shared/advisor/scripts/tests/test_tier_model_ids.py +17 -17
  7. package/_shared/advisor/scripts/tier_model_ids.py +18 -18
  8. package/_shared/pr-loop/CLAUDE.md +1 -0
  9. package/_shared/pr-loop/scripts/CLAUDE.md +2 -1
  10. package/_shared/pr-loop/scripts/README.md +1 -0
  11. package/_shared/pr-loop/scripts/code_rules_gate.py +253 -1980
  12. package/_shared/pr-loop/scripts/code_rules_gate_parts/CLAUDE.md +32 -0
  13. package/_shared/pr-loop/scripts/code_rules_gate_parts/__init__.py +7 -0
  14. package/_shared/pr-loop/scripts/code_rules_gate_parts/added_line_maps.py +268 -0
  15. package/_shared/pr-loop/scripts/code_rules_gate_parts/enforcer_loading.py +172 -0
  16. package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_arguments.py +70 -0
  17. package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_running.py +326 -0
  18. package/_shared/pr-loop/scripts/code_rules_gate_parts/git_blob_readers.py +85 -0
  19. package/_shared/pr-loop/scripts/code_rules_gate_parts/git_file_sets.py +331 -0
  20. package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_running.py +369 -0
  21. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/conftest.py +14 -0
  22. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_added_line_maps.py +118 -0
  23. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_enforcer_loading.py +17 -0
  24. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_gate_arguments.py +29 -0
  25. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_gate_running.py +99 -0
  26. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_git_blob_readers.py +69 -0
  27. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_git_file_sets.py +137 -0
  28. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_staged_test_running.py +116 -0
  29. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_violation_scoping.py +75 -0
  30. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_wrapper_plumb_check.py +49 -0
  31. package/_shared/pr-loop/scripts/code_rules_gate_parts/violation_scoping.py +328 -0
  32. package/_shared/pr-loop/scripts/code_rules_gate_parts/wrapper_plumb_check.py +206 -0
  33. package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +24 -17
  34. package/_shared/pr-loop/scripts/pr_loop_shared_constants/reviews_disabled_constants.py +1 -0
  35. package/_shared/pr-loop/scripts/reviews_disabled.py +19 -2
  36. package/_shared/pr-loop/scripts/test_code_rules_gate.py +278 -0
  37. package/_shared/pr-loop/scripts/tests/test_code_rules_gate_constants.py +6 -39
  38. package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +43 -0
  39. package/_shared/pr-loop/worker-spawn.md +186 -0
  40. package/agents/code-verifier.md +1 -1
  41. package/bin/ever-shipped-skills.mjs +3 -0
  42. package/bin/expand_home_directory_tokens.mjs +1 -1
  43. package/bin/install.mjs +5 -2
  44. package/hooks/advisory/refactor_guard.py +3 -4
  45. package/hooks/blocking/CLAUDE.md +7 -1
  46. package/hooks/blocking/block_main_commit.py +2 -2
  47. package/hooks/blocking/claude_md_orphan_file_blocker.py +75 -699
  48. package/hooks/blocking/claude_md_orphan_file_blocker_parts/CLAUDE.md +28 -0
  49. package/hooks/blocking/claude_md_orphan_file_blocker_parts/__init__.py +1 -0
  50. package/hooks/blocking/claude_md_orphan_file_blocker_parts/config/__init__.py +1 -0
  51. package/hooks/blocking/claude_md_orphan_file_blocker_parts/config/orphan_blocker_constants.py +18 -0
  52. package/hooks/blocking/claude_md_orphan_file_blocker_parts/decision.py +81 -0
  53. package/hooks/blocking/claude_md_orphan_file_blocker_parts/references.py +307 -0
  54. package/hooks/blocking/claude_md_orphan_file_blocker_parts/scan_plan.py +124 -0
  55. package/hooks/blocking/claude_md_orphan_file_blocker_parts/subtree_scan.py +179 -0
  56. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/conftest.py +10 -0
  57. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_decision.py +34 -0
  58. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_references.py +42 -0
  59. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_scan_plan.py +27 -0
  60. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_subtree_scan.py +30 -0
  61. package/hooks/blocking/code_rules_boolean_mustcheck.py +1 -1
  62. package/hooks/blocking/code_rules_mock_completeness.py +1 -1
  63. package/hooks/blocking/code_rules_optional_params.py +2 -2
  64. package/hooks/blocking/code_rules_shared.py +1 -1
  65. package/hooks/blocking/code_rules_test_assertions.py +1 -1
  66. package/hooks/blocking/code_rules_typeddict_stub.py +1 -1
  67. package/hooks/blocking/gh_pr_author_enforcer.py +1 -1
  68. package/hooks/blocking/inventory_intent_records/CLAUDE.md +26 -0
  69. package/hooks/blocking/inventory_intent_records/__init__.py +1 -0
  70. package/hooks/blocking/inventory_intent_records/config/__init__.py +1 -0
  71. package/hooks/blocking/inventory_intent_records/config/intent_records_constants.py +20 -0
  72. package/hooks/blocking/inventory_intent_records/records.py +271 -0
  73. package/hooks/blocking/inventory_intent_records/tests/conftest.py +10 -0
  74. package/hooks/blocking/inventory_intent_records/tests/test_records.py +80 -0
  75. package/hooks/blocking/package_inventory_stale_blocker.py +54 -384
  76. package/hooks/blocking/package_inventory_stale_blocker_parts/CLAUDE.md +26 -0
  77. package/hooks/blocking/package_inventory_stale_blocker_parts/__init__.py +1 -0
  78. package/hooks/blocking/package_inventory_stale_blocker_parts/config/__init__.py +1 -0
  79. package/hooks/blocking/package_inventory_stale_blocker_parts/config/inventory_blocker_constants.py +16 -0
  80. package/hooks/blocking/package_inventory_stale_blocker_parts/decision.py +84 -0
  81. package/hooks/blocking/package_inventory_stale_blocker_parts/inventory_detection.py +307 -0
  82. package/hooks/blocking/package_inventory_stale_blocker_parts/tests/conftest.py +10 -0
  83. package/hooks/blocking/package_inventory_stale_blocker_parts/tests/test_decision.py +38 -0
  84. package/hooks/blocking/package_inventory_stale_blocker_parts/tests/test_inventory_detection.py +61 -0
  85. package/hooks/blocking/pii_payload_scan.py +138 -42
  86. package/hooks/blocking/pii_prevention_blocker.py +185 -291
  87. package/hooks/blocking/pii_prevention_blocker_parts/CLAUDE.md +24 -0
  88. package/hooks/blocking/pii_prevention_blocker_parts/__init__.py +1 -0
  89. package/hooks/blocking/pii_prevention_blocker_parts/config/__init__.py +1 -0
  90. package/hooks/blocking/pii_prevention_blocker_parts/config/repository_resolution_constants.py +28 -0
  91. package/hooks/blocking/pii_prevention_blocker_parts/repository_exemption.py +214 -0
  92. package/hooks/blocking/pii_prevention_blocker_parts/repository_resolution.py +208 -0
  93. package/hooks/blocking/pr_description_command_parser.py +8 -4
  94. package/hooks/blocking/precommit_code_rules_gate.py +3 -3
  95. package/hooks/blocking/tdd_enforcer.py +97 -608
  96. package/hooks/blocking/tdd_enforcer_parts/CLAUDE.md +30 -0
  97. package/hooks/blocking/tdd_enforcer_parts/__init__.py +1 -0
  98. package/hooks/blocking/tdd_enforcer_parts/candidate_paths.py +142 -0
  99. package/hooks/blocking/tdd_enforcer_parts/config/__init__.py +1 -0
  100. package/hooks/blocking/tdd_enforcer_parts/config/tdd_enforcer_constants.py +32 -0
  101. package/hooks/blocking/tdd_enforcer_parts/content_analysis.py +268 -0
  102. package/hooks/blocking/tdd_enforcer_parts/decisions.py +92 -0
  103. package/hooks/blocking/tdd_enforcer_parts/freshness.py +80 -0
  104. package/hooks/blocking/tdd_enforcer_parts/git_tracking.py +63 -0
  105. package/hooks/blocking/tdd_enforcer_parts/path_classification.py +119 -0
  106. package/hooks/blocking/tdd_enforcer_parts/tests/conftest.py +10 -0
  107. package/hooks/blocking/tdd_enforcer_parts/tests/test_candidate_paths.py +31 -0
  108. package/hooks/blocking/tdd_enforcer_parts/tests/test_content_analysis.py +30 -0
  109. package/hooks/blocking/tdd_enforcer_parts/tests/test_decisions.py +34 -0
  110. package/hooks/blocking/tdd_enforcer_parts/tests/test_freshness.py +28 -0
  111. package/hooks/blocking/tdd_enforcer_parts/tests/test_git_tracking.py +48 -0
  112. package/hooks/blocking/tdd_enforcer_parts/tests/test_path_classification.py +36 -0
  113. package/hooks/blocking/test_inventory_deadlock_resolution.py +154 -0
  114. package/hooks/blocking/test_pii_payload_scan.py +168 -0
  115. package/hooks/blocking/test_tdd_enforcer_restore.py +108 -0
  116. package/hooks/blocking/test_verifier_verdict_minter.py +55 -158
  117. package/hooks/blocking/tests/conftest.py +10 -0
  118. package/hooks/blocking/tests/test_pii_prevention_blocker.py +260 -0
  119. package/hooks/blocking/tests/test_repository_exemption.py +105 -0
  120. package/hooks/blocking/tests/test_repository_resolution.py +108 -0
  121. package/hooks/diagnostic/hook_log_extractor.py +12 -10
  122. package/hooks/git-hooks/post_commit.py +3 -4
  123. package/hooks/hooks_constants/CLAUDE.md +2 -2
  124. package/hooks/hooks_constants/banned_identifiers_constants.py +0 -1
  125. package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -1
  126. package/hooks/hooks_constants/local_identity.py +59 -8
  127. package/hooks/hooks_constants/pii_prevention_constants.py +0 -6
  128. package/hooks/hooks_constants/test_local_identity.py +105 -3
  129. package/hooks/pyproject.toml +13 -36
  130. package/hooks/session/plugin_data_dir_cleanup.py +0 -1
  131. package/hooks/validation/mypy_validator.py +2 -2
  132. package/hooks/validators/health_check.py +1 -0
  133. package/hooks/validators/mypy_integration.py +2 -0
  134. package/hooks/validators/ruff_integration.py +3 -0
  135. package/hooks/workflow/auto_formatter.py +5 -4
  136. package/package.json +1 -1
  137. package/scripts/CLAUDE.md +4 -0
  138. package/scripts/dev_env_scripts_constants/CLAUDE.md +6 -4
  139. package/scripts/dev_env_scripts_constants/code_review_constants.py +71 -0
  140. package/scripts/dev_env_scripts_constants/grok_worker_constants.py +435 -0
  141. package/scripts/dev_env_scripts_constants/timing.py +7 -1
  142. package/scripts/grok_headless_runner.py +294 -0
  143. package/scripts/grok_worker_preflight.py +410 -0
  144. package/scripts/invoke_code_review.py +463 -0
  145. package/scripts/resolve_worker_spawn.py +619 -0
  146. package/scripts/spawn_grok_batch.py +672 -0
  147. package/scripts/test_grok_headless_runner.py +626 -0
  148. package/scripts/test_grok_worker_preflight.py +1054 -0
  149. package/scripts/test_invoke_code_review.py +672 -0
  150. package/scripts/test_resolve_worker_spawn.py +1014 -0
  151. package/scripts/test_spawn_grok_batch.py +1017 -0
  152. package/skills/CLAUDE.md +5 -3
  153. package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +72 -13
  154. package/skills/_shared/pr-loop/scripts/build_fix_prompt.py +121 -14
  155. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +78 -0
  156. package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +121 -0
  157. package/skills/_shared/pr-loop/scripts/test_build_fix_prompt.py +196 -6
  158. package/skills/autoconverge/CLAUDE.md +3 -3
  159. package/skills/autoconverge/SKILL.md +9 -3
  160. package/skills/autoconverge/reference/CLAUDE.md +2 -2
  161. package/skills/autoconverge/reference/convergence.md +33 -11
  162. package/skills/autoconverge/reference/stop-conditions.md +16 -5
  163. package/skills/autoconverge/workflow/CLAUDE.md +2 -1
  164. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +7 -2
  165. package/skills/autoconverge/workflow/converge.codex-gate.test.mjs +300 -0
  166. package/skills/autoconverge/workflow/converge.contract.test.mjs +5 -5
  167. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +29 -29
  168. package/skills/autoconverge/workflow/converge.fix-progress.test.mjs +1 -1
  169. package/skills/autoconverge/workflow/converge.mjs +200 -16
  170. package/skills/bugteam/CLAUDE.md +2 -2
  171. package/skills/bugteam/CONSTRAINTS.md +3 -2
  172. package/skills/bugteam/PROMPTS.md +7 -6
  173. package/skills/bugteam/SKILL.md +18 -13
  174. package/skills/bugteam/reference/audit-and-teammates.md +215 -35
  175. package/skills/bugteam/reference/design-rationale.md +1 -1
  176. package/skills/bugteam/reference/obstacles/CLAUDE.md +1 -1
  177. package/skills/bugteam/reference/team-setup.md +8 -2
  178. package/skills/codex-review/CLAUDE.md +46 -0
  179. package/skills/codex-review/SKILL.md +181 -0
  180. package/skills/codex-review/reference/CLAUDE.md +15 -0
  181. package/skills/codex-review/reference/cli-contract.md +253 -0
  182. package/skills/codex-review/reference/loop-integration.md +118 -0
  183. package/skills/codex-review/scripts/codex_down_classifier.py +98 -0
  184. package/skills/codex-review/scripts/codex_review_scripts_constants/CLAUDE.md +18 -0
  185. package/skills/codex-review/scripts/codex_review_scripts_constants/__init__.py +1 -0
  186. package/skills/codex-review/scripts/codex_review_scripts_constants/classifier_constants.py +35 -0
  187. package/skills/codex-review/scripts/codex_review_scripts_constants/codex_usage_probe_constants.py +86 -0
  188. package/skills/codex-review/scripts/codex_review_scripts_constants/findings_constants.py +18 -0
  189. package/skills/codex-review/scripts/codex_review_scripts_constants/run_constants.py +45 -0
  190. package/skills/codex-review/scripts/codex_usage_probe.py +573 -0
  191. package/skills/codex-review/scripts/fixtures/auth_failure_synthetic.txt +1 -0
  192. package/skills/codex-review/scripts/fixtures/config_load_failure_v0.125.0.txt +1 -0
  193. package/skills/codex-review/scripts/fixtures/freeform_findings_v0.144.3.txt +6 -0
  194. package/skills/codex-review/scripts/fixtures/model_rejection_v0.125.0.jsonl +5 -0
  195. package/skills/codex-review/scripts/fixtures/structured_findings.txt +13 -0
  196. package/skills/codex-review/scripts/fixtures/success_stream_v0.144.3.jsonl +6 -0
  197. package/skills/codex-review/scripts/fixtures/unknown_failure_synthetic.txt +1 -0
  198. package/skills/codex-review/scripts/fixtures/usage_limit_synthetic.txt +1 -0
  199. package/skills/codex-review/scripts/parse_codex_findings.py +207 -0
  200. package/skills/codex-review/scripts/run_codex_review.py +415 -0
  201. package/skills/codex-review/scripts/test_codex_down_classifier.py +143 -0
  202. package/skills/codex-review/scripts/test_codex_usage_probe.py +678 -0
  203. package/skills/codex-review/scripts/test_parse_codex_findings.py +130 -0
  204. package/skills/codex-review/scripts/test_run_codex_review.py +812 -0
  205. package/skills/codex-review/test_skill_scaffold.py +192 -0
  206. package/skills/grok-spawn/CLAUDE.md +28 -0
  207. package/skills/grok-spawn/SKILL.md +226 -0
  208. package/skills/grok-spawn/reference/flag-profiles.md +132 -0
  209. package/skills/grok-spawn/reference/worker-briefs.md +152 -0
  210. package/skills/grokify/SKILL.md +9 -1
  211. package/skills/grokify/capability-claims.test.mjs +28 -0
  212. package/skills/grokify/evals/README.md +72 -0
  213. package/skills/grokify/evals/parse-payload.test.mjs +171 -0
  214. package/skills/grokify/evals/run-capability-evals.mjs +545 -0
  215. package/skills/orchestrator/SKILL.md +42 -29
  216. package/skills/orchestrator-refresh/SKILL.md +17 -9
  217. package/skills/pr-converge/SKILL.md +34 -13
  218. package/skills/pr-converge/reference/convergence-gates.md +42 -15
  219. package/skills/pr-converge/reference/fix-protocol.md +1 -1
  220. package/skills/pr-converge/reference/ground-rules.md +1 -1
  221. package/skills/pr-converge/reference/per-tick.md +130 -42
  222. package/skills/pr-converge/reference/state-schema.md +10 -0
  223. package/skills/pr-converge/scripts/CLAUDE.md +2 -0
  224. package/skills/pr-converge/scripts/_pr_converge_path_setup.py +5 -1
  225. package/skills/pr-converge/scripts/check_convergence.py +605 -29
  226. package/skills/pr-converge/scripts/check_convergence_availability.py +232 -0
  227. package/skills/pr-converge/scripts/check_convergence_gates.py +279 -235
  228. package/skills/pr-converge/scripts/check_convergence_thread_gates.py +1 -1
  229. package/skills/pr-converge/scripts/pr_converge_scripts_constants/convergence_gate_constants.py +36 -2
  230. package/skills/pr-converge/scripts/test__pr_converge_path_setup.py +4 -0
  231. package/skills/pr-converge/scripts/test_check_convergence.py +71 -3
  232. package/skills/pr-converge/scripts/test_check_convergence_availability.py +326 -0
  233. package/skills/pr-converge/scripts/test_check_convergence_codex.py +507 -0
  234. package/skills/pr-converge/scripts/test_check_convergence_contract.py +89 -17
  235. package/skills/pr-converge/scripts/test_check_convergence_fixture.py +179 -0
  236. package/skills/pr-converge/scripts/test_check_convergence_gates.py +84 -68
  237. package/skills/pr-converge/scripts/test_check_convergence_thread_gates.py +24 -0
  238. package/skills/pr-converge/test_step5_host_branch.py +106 -0
  239. package/skills/pr-loop-cloud-transport/SKILL.md +2 -0
  240. package/skills/reviewer-gates/SKILL.md +7 -5
  241. package/skills/team-advisor/SKILL.md +7 -7
@@ -0,0 +1,672 @@
1
+ #!/usr/bin/env python3
2
+ """Batch launcher and report collector for fleets of headless grok workers.
3
+
4
+ Loads a JSON batch specification, gates once through ``run_preflight``,
5
+ assembles each worker prompt from part files, mints unique prompt, report,
6
+ leader-socket, and debug paths under the run state directory, staggers starts,
7
+ and launches each worker through ``run_headless_worker``. Emits one batch
8
+ summary JSON on stdout.
9
+
10
+ Import ``run_grok_batch`` for the summary object, or run the module as a CLI::
11
+
12
+ python spawn_grok_batch.py --spec batch.json --run-temp-dir <dir>
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import argparse
18
+ import json
19
+ import sys
20
+ import time
21
+ import uuid
22
+ from concurrent.futures import ThreadPoolExecutor
23
+ from contextlib import suppress
24
+ from dataclasses import dataclass
25
+ from pathlib import Path
26
+
27
+ from dev_env_scripts_constants.grok_worker_constants import (
28
+ ALL_KNOWN_TOOL_PROFILES,
29
+ BATCH_LAUNCH_ERROR_STDERR_PREFIX,
30
+ BATCH_SPEC_ROLE_KEY,
31
+ BATCH_SPEC_SHOULD_PING_KEY,
32
+ BATCH_SPEC_WORKERS_KEY,
33
+ BUILD_PROFILE_PROMPT_HEADER,
34
+ CLASSIFICATION_ERROR,
35
+ CLI_BATCH_SPEC_FLAG,
36
+ CLI_RUN_STATE_DIR_FLAG,
37
+ DEBUG_FILE_FLAG,
38
+ DEBUG_FILENAME_PREFIX,
39
+ DEBUG_FILENAME_SUFFIX,
40
+ DEFAULT_ROLE,
41
+ DEFAULT_WORKER_MAX_TURNS,
42
+ DEFAULT_WORKER_TIMEOUT_SECONDS,
43
+ DISABLE_WEB_SEARCH_FLAG,
44
+ DISALLOWED_TOOLS_FLAG,
45
+ LEADER_SOCKET_FILENAME_PREFIX,
46
+ LEADER_SOCKET_FILENAME_SUFFIX,
47
+ MIN_WORKER_MAX_TURNS,
48
+ MIN_WORKER_TIMEOUT_SECONDS,
49
+ OUTPUT_FILENAME_PREFIX,
50
+ OUTPUT_FILENAME_SUFFIX,
51
+ PROMPT_FILENAME_PREFIX,
52
+ PROMPT_FILENAME_SUFFIX,
53
+ PROMPT_PART_JOIN_SEPARATOR,
54
+ READONLY_DISALLOWED_TOOLS_VALUE,
55
+ READONLY_PROFILE_PROMPT_HEADER,
56
+ REPORT_STREAM_JOIN_SEPARATOR,
57
+ SUMMARY_CLASSIFICATION_KEY,
58
+ SUMMARY_DEBUG_FILE_KEY,
59
+ SUMMARY_IS_OK_KEY,
60
+ SUMMARY_IS_PREFLIGHT_USABLE_KEY,
61
+ SUMMARY_LEADER_SOCKET_KEY,
62
+ SUMMARY_OUTPUT_FILE_KEY,
63
+ SUMMARY_PREFLIGHT_REASON_KEY,
64
+ SUMMARY_PROMPT_FILE_KEY,
65
+ SUMMARY_REPORT_TEXT_KEY,
66
+ SUMMARY_RETURNCODE_KEY,
67
+ SUMMARY_ROLE_NAME_KEY,
68
+ SUMMARY_TOOL_PROFILE_KEY,
69
+ SUMMARY_WORKERS_KEY,
70
+ TOOL_PROFILE_BUILD,
71
+ TOOL_PROFILE_READONLY,
72
+ UTF8_ENCODING,
73
+ WORKER_EXCEPTION_RETURN_CODE,
74
+ WORKER_SPEC_AGENT_NAME_KEY,
75
+ WORKER_SPEC_CWD_KEY,
76
+ WORKER_SPEC_IS_REPO_ONLY_KEY,
77
+ WORKER_SPEC_MAX_TURNS_KEY,
78
+ WORKER_SPEC_PROMPT_PARTS_KEY,
79
+ WORKER_SPEC_ROLE_NAME_KEY,
80
+ WORKER_SPEC_TIMEOUT_KEY,
81
+ WORKER_SPEC_TOOL_PROFILE_KEY,
82
+ )
83
+ from dev_env_scripts_constants.timing import WORKER_STAGGER_SECONDS
84
+ from grok_headless_runner import GrokRunnerOutcome, run_headless_worker
85
+ from grok_worker_preflight import PreflightOutcome, run_preflight
86
+
87
+ batch_sleep = time.sleep
88
+ batch_headless_runner = run_headless_worker
89
+ batch_preflight = run_preflight
90
+
91
+
92
+ @dataclass(frozen=True)
93
+ class WorkerSpec:
94
+ """One worker entry from a batch specification."""
95
+
96
+ role_name: str
97
+ all_prompt_part_paths: tuple[Path, ...]
98
+ working_directory: Path
99
+ tool_profile: str
100
+ timeout_seconds: int
101
+ is_repo_only: bool = False
102
+ max_turns: int = DEFAULT_WORKER_MAX_TURNS
103
+ agent_name: str | None = None
104
+
105
+
106
+ @dataclass(frozen=True)
107
+ class BatchSpec:
108
+ """Full batch specification for one fleet launch."""
109
+
110
+ role: str
111
+ should_ping: bool
112
+ all_workers: tuple[WorkerSpec, ...]
113
+
114
+
115
+ @dataclass(frozen=True)
116
+ class WorkerScratchPaths:
117
+ """Per-worker paths minted under the run state directory."""
118
+
119
+ prompt_path: Path
120
+ report_path: Path
121
+ leader_socket_path: Path
122
+ debug_path: Path
123
+
124
+
125
+ @dataclass(frozen=True)
126
+ class WorkerReport:
127
+ """Collected outcome for one launched worker."""
128
+
129
+ role_name: str
130
+ tool_profile: str
131
+ returncode: int
132
+ classification: str
133
+ is_ok: bool
134
+ report_text: str
135
+ report_path: str
136
+ leader_socket: str
137
+ prompt_path: str
138
+ debug_path: str
139
+
140
+
141
+ @dataclass(frozen=True)
142
+ class BatchSummary:
143
+ """Preflight gate result plus per-worker reports for one batch run."""
144
+
145
+ is_preflight_usable: bool
146
+ preflight_reason: str | None
147
+ all_worker_reports: tuple[WorkerReport, ...]
148
+
149
+
150
+ def _profile_prompt_header(tool_profile: str) -> str:
151
+ if tool_profile == TOOL_PROFILE_BUILD:
152
+ return BUILD_PROFILE_PROMPT_HEADER
153
+ return READONLY_PROFILE_PROMPT_HEADER
154
+
155
+
156
+ def assemble_worker_prompt(
157
+ *,
158
+ all_prompt_part_paths: tuple[Path, ...],
159
+ tool_profile: str,
160
+ ) -> str:
161
+ """Assemble one worker prompt from a tool-profile header and part files.
162
+
163
+ ::
164
+
165
+ assemble_worker_prompt(
166
+ all_prompt_part_paths=(header, body), tool_profile="build"
167
+ )
168
+ ok: starts with BUILD_PROFILE_PROMPT_HEADER and joins part bodies
169
+
170
+ Args:
171
+ all_prompt_part_paths: Ordered paths whose text bodies form the prompt.
172
+ tool_profile: ``readonly`` or ``build``; selects the leading header.
173
+
174
+ Returns:
175
+ The full prompt text written to the per-worker prompt file.
176
+ """
177
+ all_part_bodies = [
178
+ each_path.read_text(encoding=UTF8_ENCODING)
179
+ for each_path in all_prompt_part_paths
180
+ ]
181
+ joined_parts = PROMPT_PART_JOIN_SEPARATOR.join(all_part_bodies)
182
+ return f"{_profile_prompt_header(tool_profile)}{joined_parts}"
183
+
184
+
185
+ def build_tool_profile_arguments(
186
+ *,
187
+ tool_profile: str,
188
+ is_repo_only: bool,
189
+ debug_file: Path,
190
+ ) -> tuple[str, ...]:
191
+ """Build the extra CLI tokens for one worker's tool profile.
192
+
193
+ ::
194
+
195
+ build_tool_profile_arguments(tool_profile="readonly", is_repo_only=True, ...)
196
+ ok: includes --disallowed-tools and --disable-web-search
197
+ flag: build profile omits both tool-restriction flags
198
+
199
+ Args:
200
+ tool_profile: ``readonly`` or ``build``.
201
+ is_repo_only: When True with readonly, also disable web search.
202
+ debug_file: Per-worker debug log path passed via ``--debug-file``.
203
+
204
+ Returns:
205
+ Extra argv tokens appended after the runner's base invocation.
206
+ """
207
+ all_extra_arguments: list[str] = [DEBUG_FILE_FLAG, str(debug_file)]
208
+ if tool_profile != TOOL_PROFILE_READONLY:
209
+ return tuple(all_extra_arguments)
210
+ all_extra_arguments.extend(
211
+ [DISALLOWED_TOOLS_FLAG, READONLY_DISALLOWED_TOOLS_VALUE]
212
+ )
213
+ if is_repo_only:
214
+ all_extra_arguments.append(DISABLE_WEB_SEARCH_FLAG)
215
+ return tuple(all_extra_arguments)
216
+
217
+
218
+ def _mint_worker_path(
219
+ run_state_directory: Path, *, prefix: str, suffix: str
220
+ ) -> Path:
221
+ unique_token = uuid.uuid4().hex
222
+ return run_state_directory / f"{prefix}{unique_token}{suffix}"
223
+
224
+
225
+ def _require_string(raw_field: object, field_name: str) -> str:
226
+ if not isinstance(raw_field, str):
227
+ raise ValueError(f"worker {field_name} must be a string")
228
+ return raw_field
229
+
230
+
231
+ def _require_int(raw_field: object, field_name: str) -> int:
232
+ if isinstance(raw_field, bool) or not isinstance(raw_field, int):
233
+ raise ValueError(f"worker {field_name} must be an int")
234
+ return raw_field
235
+
236
+
237
+ def _require_bool(raw_field: object, field_name: str) -> bool:
238
+ if not isinstance(raw_field, bool):
239
+ raise ValueError(f"worker {field_name} must be a bool")
240
+ return raw_field
241
+
242
+
243
+ def _require_worker_field(
244
+ all_worker_fields: dict[str, object], field_name: str
245
+ ) -> object:
246
+ if field_name not in all_worker_fields:
247
+ raise ValueError(f"worker missing required field: {field_name}")
248
+ return all_worker_fields[field_name]
249
+
250
+
251
+ def _require_int_at_least(
252
+ raw_field: object, field_name: str, minimum_accepted: int
253
+ ) -> int:
254
+ parsed_integer = _require_int(raw_field, field_name)
255
+ if parsed_integer < minimum_accepted:
256
+ raise ValueError(
257
+ f"worker {field_name} must be >= {minimum_accepted}"
258
+ )
259
+ return parsed_integer
260
+
261
+
262
+ def _parse_worker_entry(all_worker_fields: dict[str, object]) -> WorkerSpec:
263
+ role_name = _require_string(
264
+ _require_worker_field(all_worker_fields, WORKER_SPEC_ROLE_NAME_KEY),
265
+ WORKER_SPEC_ROLE_NAME_KEY,
266
+ )
267
+ all_prompt_parts = _require_worker_field(
268
+ all_worker_fields, WORKER_SPEC_PROMPT_PARTS_KEY
269
+ )
270
+ working_directory = _require_string(
271
+ _require_worker_field(all_worker_fields, WORKER_SPEC_CWD_KEY),
272
+ WORKER_SPEC_CWD_KEY,
273
+ )
274
+ tool_profile = _require_string(
275
+ _require_worker_field(all_worker_fields, WORKER_SPEC_TOOL_PROFILE_KEY),
276
+ WORKER_SPEC_TOOL_PROFILE_KEY,
277
+ )
278
+ timeout_seconds = _require_int_at_least(
279
+ all_worker_fields.get(
280
+ WORKER_SPEC_TIMEOUT_KEY, DEFAULT_WORKER_TIMEOUT_SECONDS
281
+ ),
282
+ WORKER_SPEC_TIMEOUT_KEY,
283
+ MIN_WORKER_TIMEOUT_SECONDS,
284
+ )
285
+ is_repo_only = _require_bool(
286
+ all_worker_fields.get(WORKER_SPEC_IS_REPO_ONLY_KEY, False),
287
+ WORKER_SPEC_IS_REPO_ONLY_KEY,
288
+ )
289
+ max_turns = _require_int_at_least(
290
+ all_worker_fields.get(WORKER_SPEC_MAX_TURNS_KEY, DEFAULT_WORKER_MAX_TURNS),
291
+ WORKER_SPEC_MAX_TURNS_KEY,
292
+ MIN_WORKER_MAX_TURNS,
293
+ )
294
+ agent_name = all_worker_fields.get(WORKER_SPEC_AGENT_NAME_KEY)
295
+ if not isinstance(all_prompt_parts, list) or not all_prompt_parts:
296
+ raise ValueError("worker prompt_parts must be a non-empty list")
297
+ if tool_profile not in ALL_KNOWN_TOOL_PROFILES:
298
+ raise ValueError(f"unknown tool_profile: {tool_profile}")
299
+ if agent_name is not None and not isinstance(agent_name, str):
300
+ raise ValueError("worker agent_name must be a string or null")
301
+ if isinstance(agent_name, str) and not agent_name:
302
+ raise ValueError("worker agent_name must be non-empty or null")
303
+ all_prompt_part_paths = tuple(
304
+ Path(_require_string(each_part, WORKER_SPEC_PROMPT_PARTS_KEY))
305
+ for each_part in all_prompt_parts
306
+ )
307
+ return WorkerSpec(
308
+ role_name=role_name,
309
+ all_prompt_part_paths=all_prompt_part_paths,
310
+ working_directory=Path(working_directory),
311
+ tool_profile=tool_profile,
312
+ timeout_seconds=timeout_seconds,
313
+ is_repo_only=is_repo_only,
314
+ max_turns=max_turns,
315
+ agent_name=agent_name,
316
+ )
317
+
318
+
319
+ def load_batch_spec(specification_path: Path) -> BatchSpec:
320
+ """Load and validate a JSON batch specification from disk.
321
+
322
+ Args:
323
+ specification_path: Path to the batch specification JSON file.
324
+
325
+ Returns:
326
+ The validated batch specification.
327
+
328
+ Raises:
329
+ ValueError: When the JSON shape is invalid or a required field is wrong.
330
+ OSError: When the specification file cannot be read.
331
+ json.JSONDecodeError: When the file is not valid JSON.
332
+ """
333
+ parsed_payload = json.loads(
334
+ specification_path.read_text(encoding=UTF8_ENCODING)
335
+ )
336
+ if not isinstance(parsed_payload, dict):
337
+ raise ValueError("batch specification must be a JSON object")
338
+ role = parsed_payload.get(BATCH_SPEC_ROLE_KEY, DEFAULT_ROLE)
339
+ should_ping = parsed_payload.get(BATCH_SPEC_SHOULD_PING_KEY, False)
340
+ all_worker_entries = parsed_payload.get(BATCH_SPEC_WORKERS_KEY)
341
+ if not isinstance(role, str):
342
+ raise ValueError("batch role must be a string")
343
+ if not isinstance(should_ping, bool):
344
+ raise ValueError("batch should_ping must be a bool")
345
+ if not isinstance(all_worker_entries, list) or not all_worker_entries:
346
+ raise ValueError("batch workers must be a non-empty list")
347
+ all_parsed_workers: list[WorkerSpec] = []
348
+ for each_entry in all_worker_entries:
349
+ if not isinstance(each_entry, dict):
350
+ raise ValueError("each worker must be an object")
351
+ all_parsed_workers.append(_parse_worker_entry(each_entry))
352
+ return BatchSpec(
353
+ role=role,
354
+ should_ping=should_ping,
355
+ all_workers=tuple(all_parsed_workers),
356
+ )
357
+
358
+
359
+ def _write_report_file(report_path: Path, report_text: str) -> None:
360
+ report_path.write_text(report_text, encoding=UTF8_ENCODING)
361
+
362
+
363
+ def _report_text_from_outcome(outcome: GrokRunnerOutcome) -> str:
364
+ if outcome.is_ok:
365
+ return outcome.stdout or outcome.stderr
366
+ all_present_streams = [
367
+ each_stream
368
+ for each_stream in (outcome.stdout, outcome.stderr)
369
+ if each_stream
370
+ ]
371
+ return REPORT_STREAM_JOIN_SEPARATOR.join(all_present_streams)
372
+
373
+
374
+ def _mint_worker_scratch_paths(run_state_directory: Path) -> WorkerScratchPaths:
375
+ return WorkerScratchPaths(
376
+ prompt_path=_mint_worker_path(
377
+ run_state_directory,
378
+ prefix=PROMPT_FILENAME_PREFIX,
379
+ suffix=PROMPT_FILENAME_SUFFIX,
380
+ ),
381
+ report_path=_mint_worker_path(
382
+ run_state_directory,
383
+ prefix=OUTPUT_FILENAME_PREFIX,
384
+ suffix=OUTPUT_FILENAME_SUFFIX,
385
+ ),
386
+ leader_socket_path=_mint_worker_path(
387
+ run_state_directory,
388
+ prefix=LEADER_SOCKET_FILENAME_PREFIX,
389
+ suffix=LEADER_SOCKET_FILENAME_SUFFIX,
390
+ ),
391
+ debug_path=_mint_worker_path(
392
+ run_state_directory,
393
+ prefix=DEBUG_FILENAME_PREFIX,
394
+ suffix=DEBUG_FILENAME_SUFFIX,
395
+ ),
396
+ )
397
+
398
+
399
+ def _write_assembled_prompt(
400
+ *,
401
+ worker_spec: WorkerSpec,
402
+ prompt_path: Path,
403
+ ) -> None:
404
+ prompt_text = assemble_worker_prompt(
405
+ all_prompt_part_paths=worker_spec.all_prompt_part_paths,
406
+ tool_profile=worker_spec.tool_profile,
407
+ )
408
+ prompt_path.write_text(prompt_text, encoding=UTF8_ENCODING)
409
+
410
+
411
+ def _invoke_worker(
412
+ *,
413
+ worker_spec: WorkerSpec,
414
+ scratch_paths: WorkerScratchPaths,
415
+ run_state_directory: Path,
416
+ ) -> GrokRunnerOutcome:
417
+ all_extra_arguments = build_tool_profile_arguments(
418
+ tool_profile=worker_spec.tool_profile,
419
+ is_repo_only=worker_spec.is_repo_only,
420
+ debug_file=scratch_paths.debug_path,
421
+ )
422
+ return batch_headless_runner(
423
+ prompt_file=scratch_paths.prompt_path,
424
+ working_directory=worker_spec.working_directory,
425
+ run_state_directory=run_state_directory,
426
+ max_turns=worker_spec.max_turns,
427
+ timeout_seconds=worker_spec.timeout_seconds,
428
+ agent_name=worker_spec.agent_name,
429
+ leader_socket_path=scratch_paths.leader_socket_path,
430
+ all_extra_arguments=all_extra_arguments,
431
+ )
432
+
433
+
434
+ def _worker_report(
435
+ *,
436
+ worker_spec: WorkerSpec,
437
+ scratch_paths: WorkerScratchPaths,
438
+ returncode: int,
439
+ classification: str,
440
+ is_ok: bool,
441
+ report_text: str,
442
+ ) -> WorkerReport:
443
+ with suppress(OSError):
444
+ _write_report_file(scratch_paths.report_path, report_text)
445
+ return WorkerReport(
446
+ role_name=worker_spec.role_name,
447
+ tool_profile=worker_spec.tool_profile,
448
+ returncode=returncode,
449
+ classification=classification,
450
+ is_ok=is_ok,
451
+ report_text=report_text,
452
+ report_path=str(scratch_paths.report_path),
453
+ leader_socket=str(scratch_paths.leader_socket_path),
454
+ prompt_path=str(scratch_paths.prompt_path),
455
+ debug_path=str(scratch_paths.debug_path),
456
+ )
457
+
458
+
459
+ def _build_worker_report(
460
+ *,
461
+ worker_spec: WorkerSpec,
462
+ outcome: GrokRunnerOutcome,
463
+ scratch_paths: WorkerScratchPaths,
464
+ ) -> WorkerReport:
465
+ return _worker_report(
466
+ worker_spec=worker_spec,
467
+ scratch_paths=scratch_paths,
468
+ returncode=outcome.returncode,
469
+ classification=outcome.classification,
470
+ is_ok=outcome.is_ok,
471
+ report_text=_report_text_from_outcome(outcome),
472
+ )
473
+
474
+
475
+ def _error_report_for_exception(
476
+ *,
477
+ worker_spec: WorkerSpec,
478
+ scratch_paths: WorkerScratchPaths,
479
+ raised_exception: BaseException,
480
+ ) -> WorkerReport:
481
+ return _worker_report(
482
+ worker_spec=worker_spec,
483
+ scratch_paths=scratch_paths,
484
+ returncode=WORKER_EXCEPTION_RETURN_CODE,
485
+ classification=CLASSIFICATION_ERROR,
486
+ is_ok=False,
487
+ report_text=f"{type(raised_exception).__name__}: {raised_exception}",
488
+ )
489
+
490
+
491
+ def _launch_one_worker(
492
+ *,
493
+ worker_spec: WorkerSpec,
494
+ worker_index: int,
495
+ run_state_directory: Path,
496
+ ) -> WorkerReport:
497
+ batch_sleep(worker_index * WORKER_STAGGER_SECONDS)
498
+ scratch_paths = _mint_worker_scratch_paths(run_state_directory)
499
+ try:
500
+ _write_assembled_prompt(
501
+ worker_spec=worker_spec,
502
+ prompt_path=scratch_paths.prompt_path,
503
+ )
504
+ outcome = _invoke_worker(
505
+ worker_spec=worker_spec,
506
+ scratch_paths=scratch_paths,
507
+ run_state_directory=run_state_directory,
508
+ )
509
+ return _build_worker_report(
510
+ worker_spec=worker_spec,
511
+ outcome=outcome,
512
+ scratch_paths=scratch_paths,
513
+ )
514
+ except (
515
+ OSError,
516
+ ValueError,
517
+ RuntimeError,
518
+ TypeError,
519
+ AttributeError,
520
+ LookupError,
521
+ ) as raised_exception:
522
+ return _error_report_for_exception(
523
+ worker_spec=worker_spec,
524
+ scratch_paths=scratch_paths,
525
+ raised_exception=raised_exception,
526
+ )
527
+
528
+
529
+ def run_grok_batch(
530
+ *,
531
+ batch_spec: BatchSpec,
532
+ run_state_directory: Path,
533
+ ) -> BatchSummary:
534
+ """Gate with preflight, launch all workers staggered, collect reports.
535
+
536
+ Args:
537
+ batch_spec: Validated batch specification.
538
+ run_state_directory: Run-scoped directory for sockets, prompts, reports.
539
+
540
+ Returns:
541
+ The batch summary including preflight status and per-worker reports.
542
+ """
543
+ run_state_directory.mkdir(parents=True, exist_ok=True)
544
+ preflight_outcome: PreflightOutcome = batch_preflight(
545
+ role=batch_spec.role,
546
+ should_ping=batch_spec.should_ping,
547
+ run_state_directory=run_state_directory,
548
+ )
549
+ if not preflight_outcome.is_usable:
550
+ return BatchSummary(
551
+ is_preflight_usable=False,
552
+ preflight_reason=preflight_outcome.reason,
553
+ all_worker_reports=(),
554
+ )
555
+ worker_count = len(batch_spec.all_workers)
556
+ if not worker_count:
557
+ return BatchSummary(
558
+ is_preflight_usable=True,
559
+ preflight_reason=None,
560
+ all_worker_reports=(),
561
+ )
562
+ with ThreadPoolExecutor(max_workers=worker_count) as executor:
563
+ all_futures = [
564
+ executor.submit(
565
+ _launch_one_worker,
566
+ worker_spec=each_worker,
567
+ worker_index=each_index,
568
+ run_state_directory=run_state_directory,
569
+ )
570
+ for each_index, each_worker in enumerate(batch_spec.all_workers)
571
+ ]
572
+ all_worker_reports = tuple(
573
+ each_future.result() for each_future in all_futures
574
+ )
575
+ return BatchSummary(
576
+ is_preflight_usable=True,
577
+ preflight_reason=None,
578
+ all_worker_reports=all_worker_reports,
579
+ )
580
+
581
+
582
+ def batch_summary_as_dict(batch_summary: BatchSummary) -> dict[str, object]:
583
+ """Convert a batch summary into the stdout JSON object shape.
584
+
585
+ Args:
586
+ batch_summary: The summary returned by ``run_grok_batch``.
587
+
588
+ Returns:
589
+ A JSON-serializable dictionary matching the batch summary contract.
590
+ """
591
+ all_worker_payloads = [
592
+ {
593
+ SUMMARY_ROLE_NAME_KEY: each_report.role_name,
594
+ SUMMARY_TOOL_PROFILE_KEY: each_report.tool_profile,
595
+ SUMMARY_RETURNCODE_KEY: each_report.returncode,
596
+ SUMMARY_CLASSIFICATION_KEY: each_report.classification,
597
+ SUMMARY_IS_OK_KEY: each_report.is_ok,
598
+ SUMMARY_REPORT_TEXT_KEY: each_report.report_text,
599
+ SUMMARY_OUTPUT_FILE_KEY: each_report.report_path,
600
+ SUMMARY_LEADER_SOCKET_KEY: each_report.leader_socket,
601
+ SUMMARY_PROMPT_FILE_KEY: each_report.prompt_path,
602
+ SUMMARY_DEBUG_FILE_KEY: each_report.debug_path,
603
+ }
604
+ for each_report in batch_summary.all_worker_reports
605
+ ]
606
+ return {
607
+ SUMMARY_IS_PREFLIGHT_USABLE_KEY: batch_summary.is_preflight_usable,
608
+ SUMMARY_PREFLIGHT_REASON_KEY: batch_summary.preflight_reason,
609
+ SUMMARY_WORKERS_KEY: all_worker_payloads,
610
+ }
611
+
612
+
613
+ def _build_argument_parser() -> argparse.ArgumentParser:
614
+ parser = argparse.ArgumentParser(
615
+ description=(
616
+ "Launch a fleet of headless grok workers and emit a batch summary."
617
+ )
618
+ )
619
+ parser.add_argument(
620
+ CLI_BATCH_SPEC_FLAG,
621
+ dest="specification_path",
622
+ required=True,
623
+ type=Path,
624
+ help="Path to the JSON batch specification file.",
625
+ )
626
+ parser.add_argument(
627
+ CLI_RUN_STATE_DIR_FLAG,
628
+ dest="run_state_directory",
629
+ required=True,
630
+ type=Path,
631
+ help="Run-scoped state directory for sockets, prompts, and reports.",
632
+ )
633
+ return parser
634
+
635
+
636
+ def main(all_command_arguments: list[str]) -> int:
637
+ """Run the batch launcher for CLI arguments and print the summary JSON.
638
+
639
+ An unreadable, malformed, or invalid specification, and a run state
640
+ directory that cannot be created, each print one diagnostic line on stderr
641
+ and exit ``1`` rather than raising out of the CLI.
642
+
643
+ Args:
644
+ all_command_arguments: The argument vector after the program name.
645
+
646
+ Returns:
647
+ ``0`` when preflight is usable and every worker is ok; ``1`` otherwise.
648
+ """
649
+ parser = _build_argument_parser()
650
+ parsed_arguments = parser.parse_args(all_command_arguments)
651
+ try:
652
+ batch_spec = load_batch_spec(parsed_arguments.specification_path)
653
+ batch_summary = run_grok_batch(
654
+ batch_spec=batch_spec,
655
+ run_state_directory=parsed_arguments.run_state_directory,
656
+ )
657
+ except (OSError, ValueError) as launch_error:
658
+ print(
659
+ f"{BATCH_LAUNCH_ERROR_STDERR_PREFIX}{launch_error}",
660
+ file=sys.stderr,
661
+ )
662
+ return 1
663
+ print(json.dumps(batch_summary_as_dict(batch_summary)))
664
+ if not batch_summary.is_preflight_usable:
665
+ return 1
666
+ if not all(each_report.is_ok for each_report in batch_summary.all_worker_reports):
667
+ return 1
668
+ return 0
669
+
670
+
671
+ if __name__ == "__main__":
672
+ sys.exit(main(sys.argv[1:]))