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.
- package/_shared/advisor/CLAUDE.md +2 -2
- package/_shared/advisor/advisor-protocol.md +20 -20
- package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +15 -12
- package/_shared/advisor/scripts/model_tier_run_validator.py +11 -10
- package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +25 -19
- package/_shared/advisor/scripts/tests/test_tier_model_ids.py +17 -17
- package/_shared/advisor/scripts/tier_model_ids.py +18 -18
- package/_shared/pr-loop/CLAUDE.md +1 -0
- package/_shared/pr-loop/scripts/CLAUDE.md +2 -1
- package/_shared/pr-loop/scripts/README.md +1 -0
- package/_shared/pr-loop/scripts/code_rules_gate.py +253 -1980
- package/_shared/pr-loop/scripts/code_rules_gate_parts/CLAUDE.md +32 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/__init__.py +7 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/added_line_maps.py +268 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/enforcer_loading.py +172 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_arguments.py +70 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_running.py +326 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/git_blob_readers.py +85 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/git_file_sets.py +331 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_running.py +369 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/conftest.py +14 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_added_line_maps.py +118 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_enforcer_loading.py +17 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_gate_arguments.py +29 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_gate_running.py +99 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_git_blob_readers.py +69 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_git_file_sets.py +137 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_staged_test_running.py +116 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_violation_scoping.py +75 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_wrapper_plumb_check.py +49 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/violation_scoping.py +328 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/wrapper_plumb_check.py +206 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +24 -17
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/reviews_disabled_constants.py +1 -0
- package/_shared/pr-loop/scripts/reviews_disabled.py +19 -2
- package/_shared/pr-loop/scripts/test_code_rules_gate.py +278 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate_constants.py +6 -39
- package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +43 -0
- package/_shared/pr-loop/worker-spawn.md +186 -0
- package/agents/code-verifier.md +1 -1
- package/bin/ever-shipped-skills.mjs +3 -0
- package/bin/expand_home_directory_tokens.mjs +1 -1
- package/bin/install.mjs +5 -2
- package/hooks/advisory/refactor_guard.py +3 -4
- package/hooks/blocking/CLAUDE.md +7 -1
- package/hooks/blocking/block_main_commit.py +2 -2
- package/hooks/blocking/claude_md_orphan_file_blocker.py +75 -699
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/CLAUDE.md +28 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/__init__.py +1 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/config/__init__.py +1 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/config/orphan_blocker_constants.py +18 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/decision.py +81 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/references.py +307 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/scan_plan.py +124 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/subtree_scan.py +179 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/conftest.py +10 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_decision.py +34 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_references.py +42 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_scan_plan.py +27 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_subtree_scan.py +30 -0
- package/hooks/blocking/code_rules_boolean_mustcheck.py +1 -1
- package/hooks/blocking/code_rules_mock_completeness.py +1 -1
- package/hooks/blocking/code_rules_optional_params.py +2 -2
- package/hooks/blocking/code_rules_shared.py +1 -1
- package/hooks/blocking/code_rules_test_assertions.py +1 -1
- package/hooks/blocking/code_rules_typeddict_stub.py +1 -1
- package/hooks/blocking/gh_pr_author_enforcer.py +1 -1
- package/hooks/blocking/inventory_intent_records/CLAUDE.md +26 -0
- package/hooks/blocking/inventory_intent_records/__init__.py +1 -0
- package/hooks/blocking/inventory_intent_records/config/__init__.py +1 -0
- package/hooks/blocking/inventory_intent_records/config/intent_records_constants.py +20 -0
- package/hooks/blocking/inventory_intent_records/records.py +271 -0
- package/hooks/blocking/inventory_intent_records/tests/conftest.py +10 -0
- package/hooks/blocking/inventory_intent_records/tests/test_records.py +80 -0
- package/hooks/blocking/package_inventory_stale_blocker.py +54 -384
- package/hooks/blocking/package_inventory_stale_blocker_parts/CLAUDE.md +26 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/__init__.py +1 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/config/__init__.py +1 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/config/inventory_blocker_constants.py +16 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/decision.py +84 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/inventory_detection.py +307 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/tests/conftest.py +10 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/tests/test_decision.py +38 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/tests/test_inventory_detection.py +61 -0
- package/hooks/blocking/pii_payload_scan.py +138 -42
- package/hooks/blocking/pii_prevention_blocker.py +185 -291
- package/hooks/blocking/pii_prevention_blocker_parts/CLAUDE.md +24 -0
- package/hooks/blocking/pii_prevention_blocker_parts/__init__.py +1 -0
- package/hooks/blocking/pii_prevention_blocker_parts/config/__init__.py +1 -0
- package/hooks/blocking/pii_prevention_blocker_parts/config/repository_resolution_constants.py +28 -0
- package/hooks/blocking/pii_prevention_blocker_parts/repository_exemption.py +214 -0
- package/hooks/blocking/pii_prevention_blocker_parts/repository_resolution.py +208 -0
- package/hooks/blocking/pr_description_command_parser.py +8 -4
- package/hooks/blocking/precommit_code_rules_gate.py +3 -3
- package/hooks/blocking/tdd_enforcer.py +97 -608
- package/hooks/blocking/tdd_enforcer_parts/CLAUDE.md +30 -0
- package/hooks/blocking/tdd_enforcer_parts/__init__.py +1 -0
- package/hooks/blocking/tdd_enforcer_parts/candidate_paths.py +142 -0
- package/hooks/blocking/tdd_enforcer_parts/config/__init__.py +1 -0
- package/hooks/blocking/tdd_enforcer_parts/config/tdd_enforcer_constants.py +32 -0
- package/hooks/blocking/tdd_enforcer_parts/content_analysis.py +268 -0
- package/hooks/blocking/tdd_enforcer_parts/decisions.py +92 -0
- package/hooks/blocking/tdd_enforcer_parts/freshness.py +80 -0
- package/hooks/blocking/tdd_enforcer_parts/git_tracking.py +63 -0
- package/hooks/blocking/tdd_enforcer_parts/path_classification.py +119 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/conftest.py +10 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/test_candidate_paths.py +31 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/test_content_analysis.py +30 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/test_decisions.py +34 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/test_freshness.py +28 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/test_git_tracking.py +48 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/test_path_classification.py +36 -0
- package/hooks/blocking/test_inventory_deadlock_resolution.py +154 -0
- package/hooks/blocking/test_pii_payload_scan.py +168 -0
- package/hooks/blocking/test_tdd_enforcer_restore.py +108 -0
- package/hooks/blocking/test_verifier_verdict_minter.py +55 -158
- package/hooks/blocking/tests/conftest.py +10 -0
- package/hooks/blocking/tests/test_pii_prevention_blocker.py +260 -0
- package/hooks/blocking/tests/test_repository_exemption.py +105 -0
- package/hooks/blocking/tests/test_repository_resolution.py +108 -0
- package/hooks/diagnostic/hook_log_extractor.py +12 -10
- package/hooks/git-hooks/post_commit.py +3 -4
- package/hooks/hooks_constants/CLAUDE.md +2 -2
- package/hooks/hooks_constants/banned_identifiers_constants.py +0 -1
- package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -1
- package/hooks/hooks_constants/local_identity.py +59 -8
- package/hooks/hooks_constants/pii_prevention_constants.py +0 -6
- package/hooks/hooks_constants/test_local_identity.py +105 -3
- package/hooks/pyproject.toml +13 -36
- package/hooks/session/plugin_data_dir_cleanup.py +0 -1
- package/hooks/validation/mypy_validator.py +2 -2
- package/hooks/validators/health_check.py +1 -0
- package/hooks/validators/mypy_integration.py +2 -0
- package/hooks/validators/ruff_integration.py +3 -0
- package/hooks/workflow/auto_formatter.py +5 -4
- package/package.json +1 -1
- package/scripts/CLAUDE.md +4 -0
- package/scripts/dev_env_scripts_constants/CLAUDE.md +6 -4
- package/scripts/dev_env_scripts_constants/code_review_constants.py +71 -0
- package/scripts/dev_env_scripts_constants/grok_worker_constants.py +435 -0
- package/scripts/dev_env_scripts_constants/timing.py +7 -1
- package/scripts/grok_headless_runner.py +294 -0
- package/scripts/grok_worker_preflight.py +410 -0
- package/scripts/invoke_code_review.py +463 -0
- package/scripts/resolve_worker_spawn.py +619 -0
- package/scripts/spawn_grok_batch.py +672 -0
- package/scripts/test_grok_headless_runner.py +626 -0
- package/scripts/test_grok_worker_preflight.py +1054 -0
- package/scripts/test_invoke_code_review.py +672 -0
- package/scripts/test_resolve_worker_spawn.py +1014 -0
- package/scripts/test_spawn_grok_batch.py +1017 -0
- package/skills/CLAUDE.md +5 -3
- package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +72 -13
- package/skills/_shared/pr-loop/scripts/build_fix_prompt.py +121 -14
- package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +78 -0
- package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +121 -0
- package/skills/_shared/pr-loop/scripts/test_build_fix_prompt.py +196 -6
- package/skills/autoconverge/CLAUDE.md +3 -3
- package/skills/autoconverge/SKILL.md +9 -3
- package/skills/autoconverge/reference/CLAUDE.md +2 -2
- package/skills/autoconverge/reference/convergence.md +33 -11
- package/skills/autoconverge/reference/stop-conditions.md +16 -5
- package/skills/autoconverge/workflow/CLAUDE.md +2 -1
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +7 -2
- package/skills/autoconverge/workflow/converge.codex-gate.test.mjs +300 -0
- package/skills/autoconverge/workflow/converge.contract.test.mjs +5 -5
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +29 -29
- package/skills/autoconverge/workflow/converge.fix-progress.test.mjs +1 -1
- package/skills/autoconverge/workflow/converge.mjs +200 -16
- package/skills/bugteam/CLAUDE.md +2 -2
- package/skills/bugteam/CONSTRAINTS.md +3 -2
- package/skills/bugteam/PROMPTS.md +7 -6
- package/skills/bugteam/SKILL.md +18 -13
- package/skills/bugteam/reference/audit-and-teammates.md +215 -35
- package/skills/bugteam/reference/design-rationale.md +1 -1
- package/skills/bugteam/reference/obstacles/CLAUDE.md +1 -1
- package/skills/bugteam/reference/team-setup.md +8 -2
- package/skills/codex-review/CLAUDE.md +46 -0
- package/skills/codex-review/SKILL.md +181 -0
- package/skills/codex-review/reference/CLAUDE.md +15 -0
- package/skills/codex-review/reference/cli-contract.md +253 -0
- package/skills/codex-review/reference/loop-integration.md +118 -0
- package/skills/codex-review/scripts/codex_down_classifier.py +98 -0
- package/skills/codex-review/scripts/codex_review_scripts_constants/CLAUDE.md +18 -0
- package/skills/codex-review/scripts/codex_review_scripts_constants/__init__.py +1 -0
- package/skills/codex-review/scripts/codex_review_scripts_constants/classifier_constants.py +35 -0
- package/skills/codex-review/scripts/codex_review_scripts_constants/codex_usage_probe_constants.py +86 -0
- package/skills/codex-review/scripts/codex_review_scripts_constants/findings_constants.py +18 -0
- package/skills/codex-review/scripts/codex_review_scripts_constants/run_constants.py +45 -0
- package/skills/codex-review/scripts/codex_usage_probe.py +573 -0
- package/skills/codex-review/scripts/fixtures/auth_failure_synthetic.txt +1 -0
- package/skills/codex-review/scripts/fixtures/config_load_failure_v0.125.0.txt +1 -0
- package/skills/codex-review/scripts/fixtures/freeform_findings_v0.144.3.txt +6 -0
- package/skills/codex-review/scripts/fixtures/model_rejection_v0.125.0.jsonl +5 -0
- package/skills/codex-review/scripts/fixtures/structured_findings.txt +13 -0
- package/skills/codex-review/scripts/fixtures/success_stream_v0.144.3.jsonl +6 -0
- package/skills/codex-review/scripts/fixtures/unknown_failure_synthetic.txt +1 -0
- package/skills/codex-review/scripts/fixtures/usage_limit_synthetic.txt +1 -0
- package/skills/codex-review/scripts/parse_codex_findings.py +207 -0
- package/skills/codex-review/scripts/run_codex_review.py +415 -0
- package/skills/codex-review/scripts/test_codex_down_classifier.py +143 -0
- package/skills/codex-review/scripts/test_codex_usage_probe.py +678 -0
- package/skills/codex-review/scripts/test_parse_codex_findings.py +130 -0
- package/skills/codex-review/scripts/test_run_codex_review.py +812 -0
- package/skills/codex-review/test_skill_scaffold.py +192 -0
- package/skills/grok-spawn/CLAUDE.md +28 -0
- package/skills/grok-spawn/SKILL.md +226 -0
- package/skills/grok-spawn/reference/flag-profiles.md +132 -0
- package/skills/grok-spawn/reference/worker-briefs.md +152 -0
- package/skills/grokify/SKILL.md +9 -1
- package/skills/grokify/capability-claims.test.mjs +28 -0
- package/skills/grokify/evals/README.md +72 -0
- package/skills/grokify/evals/parse-payload.test.mjs +171 -0
- package/skills/grokify/evals/run-capability-evals.mjs +545 -0
- package/skills/orchestrator/SKILL.md +42 -29
- package/skills/orchestrator-refresh/SKILL.md +17 -9
- package/skills/pr-converge/SKILL.md +34 -13
- package/skills/pr-converge/reference/convergence-gates.md +42 -15
- package/skills/pr-converge/reference/fix-protocol.md +1 -1
- package/skills/pr-converge/reference/ground-rules.md +1 -1
- package/skills/pr-converge/reference/per-tick.md +130 -42
- package/skills/pr-converge/reference/state-schema.md +10 -0
- package/skills/pr-converge/scripts/CLAUDE.md +2 -0
- package/skills/pr-converge/scripts/_pr_converge_path_setup.py +5 -1
- package/skills/pr-converge/scripts/check_convergence.py +605 -29
- package/skills/pr-converge/scripts/check_convergence_availability.py +232 -0
- package/skills/pr-converge/scripts/check_convergence_gates.py +279 -235
- package/skills/pr-converge/scripts/check_convergence_thread_gates.py +1 -1
- package/skills/pr-converge/scripts/pr_converge_scripts_constants/convergence_gate_constants.py +36 -2
- package/skills/pr-converge/scripts/test__pr_converge_path_setup.py +4 -0
- package/skills/pr-converge/scripts/test_check_convergence.py +71 -3
- package/skills/pr-converge/scripts/test_check_convergence_availability.py +326 -0
- package/skills/pr-converge/scripts/test_check_convergence_codex.py +507 -0
- package/skills/pr-converge/scripts/test_check_convergence_contract.py +89 -17
- package/skills/pr-converge/scripts/test_check_convergence_fixture.py +179 -0
- package/skills/pr-converge/scripts/test_check_convergence_gates.py +84 -68
- package/skills/pr-converge/scripts/test_check_convergence_thread_gates.py +24 -0
- package/skills/pr-converge/test_step5_host_branch.py +106 -0
- package/skills/pr-loop-cloud-transport/SKILL.md +2 -0
- package/skills/reviewer-gates/SKILL.md +7 -5
- 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:]))
|