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