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