claude-dev-env 2.5.0 → 2.7.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/CLAUDE.md +20 -57
- package/_shared/pr-loop/scripts/code_rules_gate.py +2 -1
- package/_shared/pr-loop/scripts/code_rules_gate_parts/CLAUDE.md +12 -2
- package/_shared/pr-loop/scripts/code_rules_gate_parts/baseline_import_isolation.py +309 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_regression.py +540 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_running.py +206 -70
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/__init__.py +1 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/_repo_test_helpers.py +76 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_baseline_import_isolation.py +248 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_staged_test_regression.py +309 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_staged_test_running.py +91 -58
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +202 -0
- package/_shared/pr-loop/worker-spawn.md +3 -1
- package/agents/CLAUDE.md +1 -1
- package/agents/code-verifier.md +36 -7
- package/bin/codex-compat.mjs +104 -0
- package/bin/codex-compat.test.mjs +51 -0
- package/codex-capability-map.json +13 -0
- package/docs/CODE_RULES.md +2 -0
- package/docs/codex-compatibility.md +25 -0
- package/docs/nas-ssh-invocation.md +96 -12
- package/docs/references/code-review-enforcement.md +31 -6
- package/hooks/blocking/CLAUDE.md +3 -0
- package/hooks/blocking/config/code_review_enforcement_constants.py +40 -10
- package/hooks/blocking/config/test_code_review_enforcement_constants.py +56 -3
- package/hooks/blocking/eli11_reply_enforcer.py +479 -0
- package/hooks/blocking/gh_body_arg_blocker.py +1 -1
- package/hooks/blocking/nas_ssh_binary_enforcer.py +8 -46
- package/hooks/blocking/shell_substitution_blocker.py +129 -0
- package/hooks/blocking/state_description_blocker.py +1 -1
- package/hooks/blocking/stop_dispatcher.py +1 -1
- package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +2 -3
- package/hooks/blocking/test_eli11_reply_enforcer.py +457 -0
- package/hooks/blocking/test_shell_substitution_blocker.py +124 -0
- package/hooks/blocking/test_stop_dispatcher.py +23 -0
- package/hooks/blocking/test_unscoped_search_blocker.py +102 -0
- package/hooks/blocking/test_verdict_directory_write_blocker.py +4 -8
- package/hooks/blocking/unscoped_search_blocker.py +391 -0
- package/hooks/git-hooks/CLAUDE.md +3 -0
- package/hooks/git-hooks/conftest.py +30 -0
- package/hooks/git-hooks/gate_utils.py +2 -2
- package/hooks/git-hooks/git_hooks_constants/__init__.py +41 -2
- package/hooks/git-hooks/pre_push.py +75 -4
- package/hooks/git-hooks/pre_push_base_reference.py +166 -0
- package/hooks/git-hooks/test_config.py +0 -15
- package/hooks/git-hooks/test_gate_utils.py +3 -15
- package/hooks/git-hooks/test_pre_commit.py +1 -15
- package/hooks/git-hooks/test_pre_push.py +236 -27
- package/hooks/git-hooks/test_pre_push_base_reference.py +339 -0
- package/hooks/hooks.json +0 -12
- package/hooks/hooks_constants/CLAUDE.md +5 -1
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +4 -4
- package/hooks/hooks_constants/eli11_reply_enforcer_constants.py +101 -0
- package/hooks/hooks_constants/nas_ssh_binary_enforcer_constants.py +2 -8
- package/hooks/hooks_constants/shell_command_segments.py +82 -0
- package/hooks/hooks_constants/shell_substitution_blocker_constants.py +67 -0
- package/hooks/hooks_constants/stop_dispatcher_constants.py +1 -0
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +5 -6
- package/hooks/hooks_constants/test_stop_dispatcher_constants.py +1 -0
- package/hooks/hooks_constants/unscoped_search_blocker_constants.py +153 -0
- package/package.json +4 -2
- package/rules/CLAUDE.md +17 -23
- package/rules/agent-spawn-protocol.md +6 -6
- package/rules/anti-corollary-tests.md +1 -1
- package/rules/bdd.md +1 -1
- package/rules/cleanup-temp-files.md +10 -4
- package/rules/code-standards.md +7 -0
- package/rules/conservative-action.md +1 -5
- package/rules/context7.md +0 -4
- package/rules/destructive-commands.md +47 -0
- package/rules/doc-inventory-integrity.md +48 -0
- package/rules/doc-prose-cuts.md +58 -0
- package/rules/docstring-prose-matches-implementation.md +10 -2
- package/rules/durable-post-artifacts.md +0 -4
- package/rules/eli11-replies.md +31 -0
- package/rules/explore-thoroughly.md +4 -4
- package/rules/falsify-before-green.md +68 -0
- package/rules/file-global-constants.md +1 -1
- package/rules/filesystem-search.md +51 -0
- package/rules/gh-cli-conventions.md +27 -0
- package/rules/git-workflow.md +26 -0
- package/rules/hedging-claims.md +9 -0
- package/rules/long-horizon-autonomy.md +0 -4
- package/rules/measurement-denominators.md +48 -0
- package/rules/nas-ssh-invocation.md +23 -5
- package/rules/parallel-tools.md +2 -2
- package/rules/plain-illustrative-docstrings.md +3 -7
- package/rules/plain-language.md +2 -0
- package/rules/proof-of-work-pr-comments.md +0 -4
- package/rules/re-stage-before-commit.md +2 -0
- package/rules/research-mode.md +10 -0
- package/rules/shell-invocation.md +21 -0
- package/rules/testing.md +4 -0
- package/rules/verified-commit-gate-skip.md +3 -27
- package/rules/verify-before-asking.md +5 -0
- package/rules/windows-filesystem-safe.md +1 -1
- package/rules/workers-done-before-complete.md +4 -0
- package/scripts/CLAUDE.md +3 -3
- package/scripts/Migrate-ShellPolicy.ps1 +1 -1
- package/scripts/codex_capability_bridge.py +171 -0
- package/scripts/codex_compat_materializer.py +1087 -0
- package/scripts/codex_compat_watcher.py +502 -0
- package/scripts/dev_env_scripts_constants/CLAUDE.md +1 -1
- package/scripts/dev_env_scripts_constants/code_review_constants.py +37 -0
- package/scripts/dev_env_scripts_constants/grok_worker_constants.py +79 -13
- package/scripts/grok_headless_runner.py +213 -16
- package/scripts/invoke_code_review.py +11 -4
- package/scripts/resolve_worker_spawn.py +56 -10
- package/scripts/spawn_grok_batch.py +43 -22
- package/scripts/sync_to_cursor/rules.py +0 -10
- package/scripts/test_grok_headless_runner.py +592 -10
- package/scripts/test_invoke_code_review.py +143 -0
- package/scripts/test_invoke_code_review_chain.py +1 -1
- package/scripts/test_invoke_code_review_contract.py +1 -1
- package/scripts/test_resolve_worker_spawn.py +179 -15
- package/scripts/test_spawn_grok_batch.py +225 -22
- package/scripts/tests/test_code_review_constants.py +80 -0
- package/scripts/tests/test_codex_capability_bridge.py +91 -0
- package/scripts/tests/test_codex_compat_materializer.py +632 -0
- package/scripts/tests/test_codex_compat_watcher.py +599 -0
- package/scripts/tests/test_sync_to_cursor.py +0 -1
- package/skills/autoconverge/workflow/converge.contract.test.mjs +28 -6
- package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +73 -0
- package/skills/autoconverge/workflow/converge.mjs +64 -15
- package/skills/bugteam/reference/copilot-gap-analysis.md +1 -1
- package/skills/condensing-instructions/SKILL.md +42 -51
- package/skills/fresh-branch/CLAUDE.md +1 -1
- package/skills/fresh-branch/SKILL.md +5 -6
- package/skills/fresh-branch/scripts/create_fresh_branch.py +42 -24
- package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +1 -3
- package/skills/fresh-branch/scripts/test_create_fresh_branch.py +30 -126
- package/skills/grok-spawn/SKILL.md +5 -3
- package/skills/grok-spawn/reference/flag-profiles.md +3 -1
- package/skills/orchestrator/SKILL.md +23 -9
- package/skills/orchestrator-refresh/SKILL.md +20 -1
- package/skills/privacy-hygiene/reference/sweep-procedure.md +1 -1
- package/skills/session-log/SKILL.md +1 -1
- package/rules/claude-md-orphan-file.md +0 -28
- package/rules/cleanup-command-forms.md +0 -23
- package/rules/code-reviews.md +0 -11
- package/rules/env-var-table-code-drift.md +0 -10
- package/rules/gh-body-file.md +0 -5
- package/rules/gh-paginate.md +0 -3
- package/rules/hook-prose-matches-detector.md +0 -15
- package/rules/no-historical-clutter.md +0 -26
- package/rules/no-inline-destructive-literals.md +0 -9
- package/rules/no-justification-noise.md +0 -61
- package/rules/package-inventory-stale-entry.md +0 -25
- package/rules/right-sized-engineering.md +0 -28
- package/rules/self-contained-docs.md +0 -17
- package/rules/shell-invocation-policy.md +0 -5
- package/rules/state-what-is.md +0 -25
- package/rules/tdd.md +0 -7
|
@@ -203,9 +203,6 @@ CLASSIFICATION_STREAM_JOIN_SEPARATOR: str = "\n"
|
|
|
203
203
|
DEFAULT_WORKER_TIMEOUT_SECONDS: int = 600
|
|
204
204
|
"""Default timeout applied to one headless worker invocation, in seconds."""
|
|
205
205
|
|
|
206
|
-
DEFAULT_WORKER_MAX_TURNS: int = 8
|
|
207
|
-
"""Default max-turns value applied to one headless worker invocation."""
|
|
208
|
-
|
|
209
206
|
TIMEOUT_RETURN_CODE: int = -1
|
|
210
207
|
"""Return code recorded on the outcome when a timed-out process leaves no return code."""
|
|
211
208
|
|
|
@@ -216,13 +213,88 @@ LAUNCH_FAILURE_STDERR_PREFIX: str = "failed to launch: "
|
|
|
216
213
|
"""Prefix for the non-empty stderr diagnostic returned on a launch OSError."""
|
|
217
214
|
|
|
218
215
|
KILL_GRACE_TIMEOUT_SECONDS: int = 10
|
|
219
|
-
"""Seconds to wait for a killed process to reap its pipes before giving up on its streams.
|
|
216
|
+
"""Seconds to wait for a killed process to reap its pipes before giving up on its streams.
|
|
217
|
+
|
|
218
|
+
Gates the drain that follows a kill. ``PROCESS_TREE_KILL_TIMEOUT_SECONDS``
|
|
219
|
+
gates the kill command itself, so the two bound different operations.
|
|
220
|
+
"""
|
|
221
|
+
|
|
222
|
+
MAXIMUM_WORKER_TIMEOUT_SECONDS: int = 5400
|
|
223
|
+
"""Ceiling the batch launcher puts on a worker's ``timeout_seconds`` (90 minutes).
|
|
224
|
+
|
|
225
|
+
Enforced at three sites: the batch launcher refuses an over-ceiling
|
|
226
|
+
specification while parsing it, ``run_headless_worker`` refuses one from any
|
|
227
|
+
caller, and ``resolve_worker_spawn`` refuses one before its preflight so the
|
|
228
|
+
bound holds on every tier. A worker that reaches this ceiling is killed and
|
|
229
|
+
classified ``CLASSIFICATION_TIMEOUT``.
|
|
230
|
+
"""
|
|
231
|
+
|
|
232
|
+
MAXIMUM_WORKER_TIMEOUT_ERROR_TEMPLATE: str = (
|
|
233
|
+
"worker {field_name} {requested_seconds} exceeds "
|
|
234
|
+
"MAXIMUM_WORKER_TIMEOUT_SECONDS ({maximum_seconds})"
|
|
235
|
+
)
|
|
236
|
+
"""Rejection message for an over-ceiling timeout, shared by the batch parse and the runner."""
|
|
237
|
+
|
|
238
|
+
WINDOWS_OS_NAME: str = "nt"
|
|
239
|
+
"""``os.name`` value that selects the Windows branch of the process-tree kill."""
|
|
240
|
+
|
|
241
|
+
WINDOWS_TASKKILL_COMMAND: str = "taskkill"
|
|
242
|
+
"""Windows command that ends a process by id."""
|
|
243
|
+
|
|
244
|
+
WINDOWS_TASKKILL_TREE_FLAG: str = "/T"
|
|
245
|
+
"""``taskkill`` flag that extends the kill to every descendant process."""
|
|
246
|
+
|
|
247
|
+
WINDOWS_TASKKILL_FORCE_FLAG: str = "/F"
|
|
248
|
+
"""``taskkill`` flag that forces termination rather than requesting it."""
|
|
249
|
+
|
|
250
|
+
WINDOWS_TASKKILL_PID_FLAG: str = "/PID"
|
|
251
|
+
"""``taskkill`` flag that names the target process id."""
|
|
252
|
+
|
|
253
|
+
PROCESS_TREE_KILL_TIMEOUT_SECONDS: int = 10
|
|
254
|
+
"""Seconds allowed for the tree-kill command itself before it is abandoned.
|
|
255
|
+
|
|
256
|
+
Gates the kill command. ``KILL_GRACE_TIMEOUT_SECONDS`` gates the drain that
|
|
257
|
+
follows a kill, so the two bound different operations.
|
|
258
|
+
"""
|
|
220
259
|
|
|
221
260
|
MIN_WORKER_TIMEOUT_SECONDS: int = 1
|
|
222
|
-
"""Minimum accepted worker timeout_seconds in
|
|
261
|
+
"""Minimum accepted worker timeout_seconds, in seconds.
|
|
223
262
|
|
|
224
|
-
|
|
225
|
-
"""
|
|
263
|
+
Enforced at the same three sites as ``MAXIMUM_WORKER_TIMEOUT_SECONDS``.
|
|
264
|
+
"""
|
|
265
|
+
|
|
266
|
+
MINIMUM_WORKER_TIMEOUT_ERROR_TEMPLATE: str = (
|
|
267
|
+
"worker {field_name} {requested_seconds} is below "
|
|
268
|
+
"MIN_WORKER_TIMEOUT_SECONDS ({minimum_seconds})"
|
|
269
|
+
)
|
|
270
|
+
"""Rejection message for a below-floor timeout, shared by the batch parse and the runner."""
|
|
271
|
+
|
|
272
|
+
CLASSIFICATION_KILL_FAILED: str = "kill_failed"
|
|
273
|
+
"""Outcome classification when a timed-out worker's streams never drain.
|
|
274
|
+
|
|
275
|
+
Distinct from ``CLASSIFICATION_TIMEOUT``: that worker's tree was cleared, this
|
|
276
|
+
one's was not, so a caller can tell an abandoned process tree from a stopped
|
|
277
|
+
one.
|
|
278
|
+
"""
|
|
279
|
+
|
|
280
|
+
KILL_FAILED_RETURN_CODE: int = -4
|
|
281
|
+
"""Return code recorded when a timed-out worker's streams never drain."""
|
|
282
|
+
|
|
283
|
+
KILL_FAILED_STDERR_TEMPLATE: str = (
|
|
284
|
+
"worker timed out and its streams never drained across {attempt_count} "
|
|
285
|
+
"kill-and-drain attempts; process {process_identifier} may still be running"
|
|
286
|
+
)
|
|
287
|
+
"""Stderr diagnostic naming the surviving process id after every attempt fails."""
|
|
288
|
+
|
|
289
|
+
PROCESS_TREE_KILL_ATTEMPT_LIMIT: int = 2
|
|
290
|
+
"""Kill-and-drain rounds run on a timed-out worker before the kill is reported failed.
|
|
291
|
+
|
|
292
|
+
Each round issues the tree kill while the worker process is still alive, then
|
|
293
|
+
waits ``KILL_GRACE_TIMEOUT_SECONDS`` for the streams to drain.
|
|
294
|
+
"""
|
|
295
|
+
|
|
296
|
+
REASON_TIMEOUT_OUT_OF_BOUNDS: str = "timeout_out_of_bounds"
|
|
297
|
+
"""Dispatcher attempt reason when the requested timeout is below the floor or above the ceiling."""
|
|
226
298
|
|
|
227
299
|
WORKER_EXCEPTION_RETURN_CODE: int = -3
|
|
228
300
|
"""Return code recorded on a WorkerReport when the worker body raises before a process runs.
|
|
@@ -321,9 +393,6 @@ WORKER_SPEC_TIMEOUT_KEY: str = "timeout_seconds"
|
|
|
321
393
|
WORKER_SPEC_IS_REPO_ONLY_KEY: str = "is_repo_only"
|
|
322
394
|
"""JSON key for whether a readonly worker also disables web search."""
|
|
323
395
|
|
|
324
|
-
WORKER_SPEC_MAX_TURNS_KEY: str = "max_turns"
|
|
325
|
-
"""JSON key for one worker's max-turns cap."""
|
|
326
|
-
|
|
327
396
|
WORKER_SPEC_AGENT_NAME_KEY: str = "agent_name"
|
|
328
397
|
"""JSON key for one worker's optional agent definition name."""
|
|
329
398
|
|
|
@@ -389,9 +458,6 @@ REASON_CLAUDE_AGENT_REQUIRED: str = "claude_agent_required"
|
|
|
389
458
|
REASON_PROMPT_FILE_MISSING: str = "prompt_file_missing"
|
|
390
459
|
"""Config reason when the dispatcher CLI prompt file path is absent or unreadable."""
|
|
391
460
|
|
|
392
|
-
DEFAULT_SPAWN_MAX_TURNS: int = 8
|
|
393
|
-
"""Default max-turns applied to the headless grok worker when the caller names none."""
|
|
394
|
-
|
|
395
461
|
SPAWN_SERVED_EXIT_CODE: int = 0
|
|
396
462
|
"""CLI exit code when a dispatcher tier served the call."""
|
|
397
463
|
|
|
@@ -3,9 +3,12 @@
|
|
|
3
3
|
|
|
4
4
|
Builds a headless argv, mints a unique ``--leader-socket`` path under the
|
|
5
5
|
caller-supplied run state directory, captures stdout/stderr/returncode, kills
|
|
6
|
-
the process on timeout, and classifies failures via signature lists in
|
|
6
|
+
the process tree on timeout, and classifies failures via signature lists in
|
|
7
7
|
``dev_env_scripts_constants.grok_worker_constants``.
|
|
8
8
|
|
|
9
|
+
The timeout is the only bound on a worker's length; the argv carries no turn
|
|
10
|
+
cap.
|
|
11
|
+
|
|
9
12
|
Dual-match policy matches preflight: when both usage and auth signatures appear
|
|
10
13
|
in the same streams, auth wins (``CLASSIFICATION_AUTH_FAILURE``).
|
|
11
14
|
|
|
@@ -15,14 +18,15 @@ Import ``run_headless_worker`` for the outcome object::
|
|
|
15
18
|
prompt_file=path,
|
|
16
19
|
working_directory=cwd,
|
|
17
20
|
run_state_directory=run_dir,
|
|
18
|
-
|
|
19
|
-
timeout_seconds=600,
|
|
21
|
+
timeout_seconds=5400,
|
|
20
22
|
agent_name="code-quality-agent",
|
|
21
23
|
)
|
|
22
24
|
"""
|
|
23
25
|
|
|
24
26
|
from __future__ import annotations
|
|
25
27
|
|
|
28
|
+
import os
|
|
29
|
+
import signal
|
|
26
30
|
import subprocess
|
|
27
31
|
import uuid
|
|
28
32
|
from dataclasses import dataclass
|
|
@@ -35,6 +39,7 @@ from dev_env_scripts_constants.grok_worker_constants import (
|
|
|
35
39
|
ALWAYS_APPROVE_FLAG,
|
|
36
40
|
CLASSIFICATION_AUTH_FAILURE,
|
|
37
41
|
CLASSIFICATION_ERROR,
|
|
42
|
+
CLASSIFICATION_KILL_FAILED,
|
|
38
43
|
CLASSIFICATION_OK,
|
|
39
44
|
CLASSIFICATION_STREAM_JOIN_SEPARATOR,
|
|
40
45
|
CLASSIFICATION_TIMEOUT,
|
|
@@ -43,24 +48,46 @@ from dev_env_scripts_constants.grok_worker_constants import (
|
|
|
43
48
|
GROK_BINARY_NAME,
|
|
44
49
|
GROK_BINARY_NOT_FOUND_STDERR,
|
|
45
50
|
GROK_MODEL_PIN,
|
|
51
|
+
KILL_FAILED_RETURN_CODE,
|
|
52
|
+
KILL_FAILED_STDERR_TEMPLATE,
|
|
46
53
|
KILL_GRACE_TIMEOUT_SECONDS,
|
|
47
54
|
LAUNCH_FAILURE_RETURN_CODE,
|
|
48
55
|
LAUNCH_FAILURE_STDERR_PREFIX,
|
|
49
56
|
LEADER_SOCKET_FILENAME_PREFIX,
|
|
50
57
|
LEADER_SOCKET_FILENAME_SUFFIX,
|
|
51
58
|
LEADER_SOCKET_FLAG,
|
|
52
|
-
|
|
59
|
+
MAXIMUM_WORKER_TIMEOUT_ERROR_TEMPLATE,
|
|
60
|
+
MAXIMUM_WORKER_TIMEOUT_SECONDS,
|
|
61
|
+
MIN_WORKER_TIMEOUT_SECONDS,
|
|
62
|
+
MINIMUM_WORKER_TIMEOUT_ERROR_TEMPLATE,
|
|
53
63
|
MISSING_BINARY_RETURN_CODE,
|
|
54
64
|
MODEL_FLAG,
|
|
55
65
|
OUTPUT_FORMAT_FLAG,
|
|
56
66
|
OUTPUT_FORMAT_JSON,
|
|
67
|
+
PROCESS_TREE_KILL_ATTEMPT_LIMIT,
|
|
68
|
+
PROCESS_TREE_KILL_TIMEOUT_SECONDS,
|
|
57
69
|
PROMPT_FILE_FLAG,
|
|
58
70
|
TIMEOUT_RETURN_CODE,
|
|
59
71
|
UTF8_DECODE_ERRORS,
|
|
60
72
|
UTF8_ENCODING,
|
|
73
|
+
WINDOWS_OS_NAME,
|
|
74
|
+
WINDOWS_TASKKILL_COMMAND,
|
|
75
|
+
WINDOWS_TASKKILL_FORCE_FLAG,
|
|
76
|
+
WINDOWS_TASKKILL_PID_FLAG,
|
|
77
|
+
WINDOWS_TASKKILL_TREE_FLAG,
|
|
78
|
+
WORKER_SPEC_TIMEOUT_KEY,
|
|
61
79
|
)
|
|
62
80
|
|
|
63
81
|
runner_popen = subprocess.Popen
|
|
82
|
+
runner_subprocess_run = subprocess.run
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class WorkerTimeoutOutOfBoundsError(ValueError):
|
|
86
|
+
"""Raised when a requested worker timeout falls outside the accepted bounds.
|
|
87
|
+
|
|
88
|
+
A ``ValueError`` subclass so existing callers that catch ``ValueError``
|
|
89
|
+
keep working, while a caller that wants only this fault can name it.
|
|
90
|
+
"""
|
|
64
91
|
|
|
65
92
|
|
|
66
93
|
@dataclass(frozen=True)
|
|
@@ -90,7 +117,6 @@ def _build_invocation(
|
|
|
90
117
|
*,
|
|
91
118
|
prompt_file: Path,
|
|
92
119
|
working_directory: Path,
|
|
93
|
-
max_turns: int,
|
|
94
120
|
leader_socket_path: Path,
|
|
95
121
|
agent_name: str | None,
|
|
96
122
|
all_extra_arguments: tuple[str, ...] = (),
|
|
@@ -104,8 +130,6 @@ def _build_invocation(
|
|
|
104
130
|
OUTPUT_FORMAT_FLAG,
|
|
105
131
|
OUTPUT_FORMAT_JSON,
|
|
106
132
|
ALWAYS_APPROVE_FLAG,
|
|
107
|
-
MAX_TURNS_FLAG,
|
|
108
|
-
str(max_turns),
|
|
109
133
|
LEADER_SOCKET_FLAG,
|
|
110
134
|
str(leader_socket_path),
|
|
111
135
|
]
|
|
@@ -180,14 +204,180 @@ def _resolve_returncode(process: subprocess.Popen[str]) -> int:
|
|
|
180
204
|
return TIMEOUT_RETURN_CODE
|
|
181
205
|
|
|
182
206
|
|
|
183
|
-
def
|
|
184
|
-
process.
|
|
207
|
+
def _kill_windows_process_tree(process_identifier: int) -> None:
|
|
208
|
+
"""End a Windows process and every descendant it started, by process id.
|
|
209
|
+
|
|
210
|
+
Swallows taskkill failures so the caller still falls back to
|
|
211
|
+
``Popen.kill()`` and a timed drain.
|
|
212
|
+
"""
|
|
185
213
|
try:
|
|
186
|
-
|
|
187
|
-
|
|
214
|
+
runner_subprocess_run(
|
|
215
|
+
[
|
|
216
|
+
WINDOWS_TASKKILL_COMMAND,
|
|
217
|
+
WINDOWS_TASKKILL_TREE_FLAG,
|
|
218
|
+
WINDOWS_TASKKILL_FORCE_FLAG,
|
|
219
|
+
WINDOWS_TASKKILL_PID_FLAG,
|
|
220
|
+
str(process_identifier),
|
|
221
|
+
],
|
|
222
|
+
stdout=subprocess.DEVNULL,
|
|
223
|
+
stderr=subprocess.DEVNULL,
|
|
224
|
+
check=False,
|
|
225
|
+
timeout=PROCESS_TREE_KILL_TIMEOUT_SECONDS,
|
|
188
226
|
)
|
|
227
|
+
except (subprocess.TimeoutExpired, OSError):
|
|
228
|
+
return
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _kill_posix_process_group(process_identifier: int) -> None:
|
|
232
|
+
"""End a POSIX process group so no grandchild keeps the capture pipe open.
|
|
233
|
+
|
|
234
|
+
Reached only through the caller's ``os.name`` branch, so the process-group
|
|
235
|
+
calls run on the platforms that define them.
|
|
236
|
+
|
|
237
|
+
A process id that is already reaped raises ``OSError``; this swallows it and
|
|
238
|
+
returns, so the caller falls back to ``Popen.kill()``.
|
|
239
|
+
"""
|
|
240
|
+
try:
|
|
241
|
+
process_group_identifier = os.getpgid(process_identifier) # type: ignore[attr-defined] # POSIX-only, reached via the caller's os.name branch
|
|
242
|
+
os.killpg(process_group_identifier, signal.SIGKILL) # type: ignore[attr-defined] # POSIX-only, reached via the caller's os.name branch
|
|
243
|
+
except OSError:
|
|
244
|
+
return
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _kill_process_tree_by_identifier(process_identifier: int) -> None:
|
|
248
|
+
"""Issue the platform's tree kill for one process id, with no liveness check."""
|
|
249
|
+
if os.name == WINDOWS_OS_NAME:
|
|
250
|
+
_kill_windows_process_tree(process_identifier)
|
|
251
|
+
return
|
|
252
|
+
_kill_posix_process_group(process_identifier)
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def _terminate_process_tree(process: subprocess.Popen[str]) -> None:
|
|
256
|
+
"""End the worker process and every descendant it spawned.
|
|
257
|
+
|
|
258
|
+
::
|
|
259
|
+
|
|
260
|
+
tree kill (taskkill /T or killpg) ok: grandchildren die, pipes close
|
|
261
|
+
Popen.kill() alone flag: grandchildren outlive the worker
|
|
262
|
+
|
|
263
|
+
Falls back to ``Popen.kill()`` when the direct child survives the tree kill,
|
|
264
|
+
so the caller never waits on a live process.
|
|
265
|
+
"""
|
|
266
|
+
if process.poll() is not None:
|
|
267
|
+
return
|
|
268
|
+
_kill_process_tree_by_identifier(process.pid)
|
|
269
|
+
if process.poll() is not None:
|
|
270
|
+
return
|
|
271
|
+
try:
|
|
272
|
+
process.kill()
|
|
273
|
+
except ProcessLookupError:
|
|
274
|
+
return
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def require_timeout_within_bounds(timeout_seconds: int | None) -> None:
|
|
278
|
+
"""Refuse a timeout that is missing, below the floor, or above the ceiling.
|
|
279
|
+
|
|
280
|
+
::
|
|
281
|
+
|
|
282
|
+
None or 0 flag: ValueError naming MIN_WORKER_TIMEOUT_SECONDS
|
|
283
|
+
5401 flag: ValueError naming MAXIMUM_WORKER_TIMEOUT_SECONDS
|
|
284
|
+
1 .. 5400 ok: returns
|
|
285
|
+
|
|
286
|
+
Public so a dispatcher can apply the same bounds on a path that never
|
|
287
|
+
reaches ``run_headless_worker``.
|
|
288
|
+
|
|
289
|
+
Args:
|
|
290
|
+
timeout_seconds: The requested per-worker timeout in seconds.
|
|
291
|
+
|
|
292
|
+
Raises:
|
|
293
|
+
WorkerTimeoutOutOfBoundsError: When the value falls outside the bounds.
|
|
294
|
+
"""
|
|
295
|
+
if timeout_seconds is None or timeout_seconds < MIN_WORKER_TIMEOUT_SECONDS:
|
|
296
|
+
raise WorkerTimeoutOutOfBoundsError(
|
|
297
|
+
MINIMUM_WORKER_TIMEOUT_ERROR_TEMPLATE.format(
|
|
298
|
+
field_name=WORKER_SPEC_TIMEOUT_KEY,
|
|
299
|
+
requested_seconds=timeout_seconds,
|
|
300
|
+
minimum_seconds=MIN_WORKER_TIMEOUT_SECONDS,
|
|
301
|
+
)
|
|
302
|
+
)
|
|
303
|
+
if timeout_seconds > MAXIMUM_WORKER_TIMEOUT_SECONDS:
|
|
304
|
+
raise WorkerTimeoutOutOfBoundsError(
|
|
305
|
+
MAXIMUM_WORKER_TIMEOUT_ERROR_TEMPLATE.format(
|
|
306
|
+
field_name=WORKER_SPEC_TIMEOUT_KEY,
|
|
307
|
+
requested_seconds=timeout_seconds,
|
|
308
|
+
maximum_seconds=MAXIMUM_WORKER_TIMEOUT_SECONDS,
|
|
309
|
+
)
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _drain_after_kill(process: subprocess.Popen[str]) -> tuple[str, str] | None:
|
|
314
|
+
"""Read the killed process's streams, or None when the grace window expires."""
|
|
315
|
+
try:
|
|
316
|
+
return process.communicate(timeout=KILL_GRACE_TIMEOUT_SECONDS)
|
|
189
317
|
except subprocess.TimeoutExpired:
|
|
190
|
-
|
|
318
|
+
return None
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _kill_and_drain_within_attempt_limit(
|
|
322
|
+
process: subprocess.Popen[str],
|
|
323
|
+
) -> tuple[str, str] | None:
|
|
324
|
+
"""Kill the process tree and drain it, retrying up to the attempt limit.
|
|
325
|
+
|
|
326
|
+
::
|
|
327
|
+
|
|
328
|
+
attempt 1 drains ok: streams, one attempt made
|
|
329
|
+
attempt 1 times out,
|
|
330
|
+
attempt 2 drains ok: streams, two attempts made
|
|
331
|
+
every attempt times out flag: None
|
|
332
|
+
|
|
333
|
+
A tree kill that returns without taking leaves the drain waiting on a live
|
|
334
|
+
pipe, so a timed-out drain is followed by another kill-and-drain round.
|
|
335
|
+
``_terminate_process_tree`` re-issues the kill only while the worker
|
|
336
|
+
process is still alive; once it has exited, the next round is a second
|
|
337
|
+
drain window for the descendants still holding the pipe open.
|
|
338
|
+
|
|
339
|
+
Args:
|
|
340
|
+
process: The timed-out worker process to kill and read.
|
|
341
|
+
|
|
342
|
+
Returns:
|
|
343
|
+
The captured stdout and stderr, or None when every attempt timed out.
|
|
344
|
+
"""
|
|
345
|
+
attempts_made = 0
|
|
346
|
+
while attempts_made < PROCESS_TREE_KILL_ATTEMPT_LIMIT:
|
|
347
|
+
_terminate_process_tree(process)
|
|
348
|
+
all_captured_streams = _drain_after_kill(process)
|
|
349
|
+
if all_captured_streams is not None:
|
|
350
|
+
return all_captured_streams
|
|
351
|
+
attempts_made += 1
|
|
352
|
+
return None
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def _kill_failed_outcome(process: subprocess.Popen[str]) -> GrokRunnerOutcome:
|
|
356
|
+
diagnostic_text = KILL_FAILED_STDERR_TEMPLATE.format(
|
|
357
|
+
attempt_count=PROCESS_TREE_KILL_ATTEMPT_LIMIT,
|
|
358
|
+
process_identifier=process.pid,
|
|
359
|
+
)
|
|
360
|
+
return GrokRunnerOutcome(
|
|
361
|
+
is_ok=False,
|
|
362
|
+
returncode=KILL_FAILED_RETURN_CODE,
|
|
363
|
+
classification=CLASSIFICATION_KILL_FAILED,
|
|
364
|
+
stdout="",
|
|
365
|
+
stderr=diagnostic_text,
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def _timeout_outcome(process: subprocess.Popen[str]) -> GrokRunnerOutcome:
|
|
370
|
+
"""Kill a timed-out worker's tree, then classify what the kill achieved.
|
|
371
|
+
|
|
372
|
+
::
|
|
373
|
+
|
|
374
|
+
drain clears on attempt 1 or 2 ok: classification timeout
|
|
375
|
+
both attempts leave it draining flag: classification kill_failed
|
|
376
|
+
"""
|
|
377
|
+
all_captured_streams = _kill_and_drain_within_attempt_limit(process)
|
|
378
|
+
if all_captured_streams is None:
|
|
379
|
+
return _kill_failed_outcome(process)
|
|
380
|
+
captured_stdout, captured_stderr = all_captured_streams
|
|
191
381
|
stdout_text = _normalize_stream(captured_stdout)
|
|
192
382
|
stderr_text = _normalize_stream(captured_stderr)
|
|
193
383
|
returncode = _resolve_returncode(process)
|
|
@@ -234,6 +424,7 @@ def _invoke_process(
|
|
|
234
424
|
text=True,
|
|
235
425
|
encoding=UTF8_ENCODING,
|
|
236
426
|
errors=UTF8_DECODE_ERRORS,
|
|
427
|
+
start_new_session=os.name != WINDOWS_OS_NAME,
|
|
237
428
|
)
|
|
238
429
|
except FileNotFoundError:
|
|
239
430
|
return _missing_binary_outcome()
|
|
@@ -254,7 +445,6 @@ def run_headless_worker(
|
|
|
254
445
|
prompt_file: Path,
|
|
255
446
|
working_directory: Path,
|
|
256
447
|
run_state_directory: Path,
|
|
257
|
-
max_turns: int,
|
|
258
448
|
timeout_seconds: int,
|
|
259
449
|
agent_name: str | None = None,
|
|
260
450
|
leader_socket_path: Path | None = None,
|
|
@@ -262,13 +452,16 @@ def run_headless_worker(
|
|
|
262
452
|
) -> GrokRunnerOutcome:
|
|
263
453
|
"""Run one headless grok worker and classify the process outcome.
|
|
264
454
|
|
|
455
|
+
The timeout is the worker's only bound; the argv carries no turn cap.
|
|
456
|
+
|
|
265
457
|
Args:
|
|
266
458
|
prompt_file: Path to the prompt file passed via ``--prompt-file``.
|
|
267
459
|
working_directory: Working directory passed via ``--cwd``.
|
|
268
460
|
run_state_directory: Run-scoped directory the leader socket is minted
|
|
269
461
|
under. Read only when ``leader_socket_path`` is omitted.
|
|
270
|
-
|
|
271
|
-
|
|
462
|
+
timeout_seconds: Seconds before the process tree is killed on expiry.
|
|
463
|
+
Must sit between ``MIN_WORKER_TIMEOUT_SECONDS`` and
|
|
464
|
+
``MAXIMUM_WORKER_TIMEOUT_SECONDS`` inclusive.
|
|
272
465
|
agent_name: Optional role agent name passed via ``--agent``.
|
|
273
466
|
leader_socket_path: Optional pre-minted leader socket path. When omitted,
|
|
274
467
|
a unique path is minted under ``run_state_directory``.
|
|
@@ -277,7 +470,12 @@ def run_headless_worker(
|
|
|
277
470
|
|
|
278
471
|
Returns:
|
|
279
472
|
The classified outcome including return code and captured streams.
|
|
473
|
+
|
|
474
|
+
Raises:
|
|
475
|
+
WorkerTimeoutOutOfBoundsError: When ``timeout_seconds`` is missing,
|
|
476
|
+
below the floor, or above the ceiling.
|
|
280
477
|
"""
|
|
478
|
+
require_timeout_within_bounds(timeout_seconds)
|
|
281
479
|
resolved_leader_socket_path = (
|
|
282
480
|
leader_socket_path
|
|
283
481
|
if leader_socket_path is not None
|
|
@@ -286,7 +484,6 @@ def run_headless_worker(
|
|
|
286
484
|
all_arguments = _build_invocation(
|
|
287
485
|
prompt_file=prompt_file,
|
|
288
486
|
working_directory=working_directory,
|
|
289
|
-
max_turns=max_turns,
|
|
290
487
|
leader_socket_path=resolved_leader_socket_path,
|
|
291
488
|
agent_name=agent_name,
|
|
292
489
|
all_extra_arguments=all_extra_arguments,
|
|
@@ -8,7 +8,8 @@ Mode decision::
|
|
|
8
8
|
host=ThirdParty, any model -> mode chain
|
|
9
9
|
|
|
10
10
|
Chain mode runs ``run_claude`` with argv from ``build_code_review_arguments``
|
|
11
|
-
(single-turn prompt, model opus, json output,
|
|
11
|
+
(single-turn prompt, model opus, json output, and the permission mode this
|
|
12
|
+
caller is allowed to ask the review binary for).
|
|
12
13
|
|
|
13
14
|
cwd is the PR working tree and stdin is redirected from the empty stream so
|
|
14
15
|
the spawn does not wait for interactive input. Result JSON on stdout only::
|
|
@@ -80,7 +81,7 @@ from dev_env_scripts_constants.code_review_constants import ( # noqa: E402
|
|
|
80
81
|
MAXIMUM_STAMP_MINT_PASSES,
|
|
81
82
|
MODE_CHAIN,
|
|
82
83
|
MODE_IN_SESSION,
|
|
83
|
-
PERMISSION_MODE_BYPASS,
|
|
84
|
+
REVIEW_PERMISSION_MODE as PERMISSION_MODE_BYPASS,
|
|
84
85
|
PERMISSION_MODE_FLAG,
|
|
85
86
|
RECORD_STAMP_FLAG,
|
|
86
87
|
RESULT_KEY_BOUND_HASH,
|
|
@@ -418,6 +419,7 @@ def is_code_review_clean_stamp_allowed(review_outcome: CodeReviewOutcome) -> boo
|
|
|
418
419
|
return False
|
|
419
420
|
return True
|
|
420
421
|
|
|
422
|
+
|
|
421
423
|
def _run_claude_with_empty_stdin(
|
|
422
424
|
all_claude_arguments: list[str],
|
|
423
425
|
*,
|
|
@@ -495,6 +497,7 @@ def _failure_code_review_outcome(returncode: int) -> CodeReviewOutcome:
|
|
|
495
497
|
is_dirty_tree=False,
|
|
496
498
|
)
|
|
497
499
|
|
|
500
|
+
|
|
498
501
|
def _run_chain_review(
|
|
499
502
|
*,
|
|
500
503
|
working_directory: Path,
|
|
@@ -507,6 +510,8 @@ def _run_chain_review(
|
|
|
507
510
|
timeout_seconds=timeout_seconds,
|
|
508
511
|
working_directory=working_directory,
|
|
509
512
|
)
|
|
513
|
+
if chain_outcome.returncode != SUCCESSFUL_REVIEW_RETURNCODE and chain_outcome.stderr:
|
|
514
|
+
sys.stderr.write(chain_outcome.stderr.strip() + "\n")
|
|
510
515
|
return _chain_outcome(chain_outcome, working_directory=working_directory)
|
|
511
516
|
|
|
512
517
|
|
|
@@ -869,9 +874,11 @@ def _mint_or_config_outcome(
|
|
|
869
874
|
timeout_seconds=timeout_seconds,
|
|
870
875
|
effort=effort,
|
|
871
876
|
)
|
|
872
|
-
except ChainConfigurationError:
|
|
877
|
+
except ChainConfigurationError as configuration_error:
|
|
878
|
+
sys.stderr.write(str(configuration_error) + "\n")
|
|
873
879
|
return _no_mint_outcome(CHAIN_CONFIG_ERROR_EXIT_CODE)
|
|
874
|
-
except ValueError:
|
|
880
|
+
except ValueError as host_profile_error:
|
|
881
|
+
sys.stderr.write(str(host_profile_error) + "\n")
|
|
875
882
|
return _no_mint_outcome(HOST_PROFILE_ERROR_RETURNCODE)
|
|
876
883
|
|
|
877
884
|
|
|
@@ -65,7 +65,6 @@ from dev_env_scripts_constants.grok_worker_constants import ( # noqa: E402
|
|
|
65
65
|
CLI_TIMEOUT_FLAG,
|
|
66
66
|
CWD_FLAG,
|
|
67
67
|
DEFAULT_ROLE,
|
|
68
|
-
DEFAULT_SPAWN_MAX_TURNS,
|
|
69
68
|
DEFAULT_WORKER_TIMEOUT_SECONDS,
|
|
70
69
|
EMPTY_OUTPUT,
|
|
71
70
|
OUTPUT_FORMAT_FLAG,
|
|
@@ -73,6 +72,7 @@ from dev_env_scripts_constants.grok_worker_constants import ( # noqa: E402
|
|
|
73
72
|
PROMPT_FILE_FLAG,
|
|
74
73
|
REASON_CLAUDE_AGENT_REQUIRED,
|
|
75
74
|
REASON_PROMPT_FILE_MISSING,
|
|
75
|
+
REASON_TIMEOUT_OUT_OF_BOUNDS,
|
|
76
76
|
RESULT_KEY_ATTEMPTS,
|
|
77
77
|
RESULT_KEY_OK,
|
|
78
78
|
RESULT_KEY_OUTPUT,
|
|
@@ -87,7 +87,12 @@ from dev_env_scripts_constants.grok_worker_constants import ( # noqa: E402
|
|
|
87
87
|
TIER_GROK,
|
|
88
88
|
UTF8_ENCODING,
|
|
89
89
|
)
|
|
90
|
-
from grok_headless_runner import
|
|
90
|
+
from grok_headless_runner import ( # noqa: E402
|
|
91
|
+
GrokRunnerOutcome,
|
|
92
|
+
WorkerTimeoutOutOfBoundsError,
|
|
93
|
+
require_timeout_within_bounds,
|
|
94
|
+
run_headless_worker,
|
|
95
|
+
)
|
|
91
96
|
from grok_worker_preflight import PreflightOutcome, run_preflight # noqa: E402
|
|
92
97
|
from tier_model_ids import detect_host_profile # noqa: E402
|
|
93
98
|
|
|
@@ -336,7 +341,6 @@ def _run_tier_grok(
|
|
|
336
341
|
prompt_file: Path,
|
|
337
342
|
working_directory: Path,
|
|
338
343
|
run_state_directory: Path,
|
|
339
|
-
max_turns: int,
|
|
340
344
|
timeout_seconds: int,
|
|
341
345
|
) -> GrokRunnerOutcome:
|
|
342
346
|
agent_name = _primary_agent_name_for_role(role)
|
|
@@ -344,7 +348,6 @@ def _run_tier_grok(
|
|
|
344
348
|
prompt_file=prompt_file,
|
|
345
349
|
working_directory=working_directory,
|
|
346
350
|
run_state_directory=run_state_directory,
|
|
347
|
-
max_turns=max_turns,
|
|
348
351
|
timeout_seconds=timeout_seconds,
|
|
349
352
|
agent_name=agent_name,
|
|
350
353
|
)
|
|
@@ -418,10 +421,13 @@ def resolve_worker_spawn(
|
|
|
418
421
|
timeout_seconds: int,
|
|
419
422
|
is_claude_tier_enabled: bool,
|
|
420
423
|
run_state_directory: Path,
|
|
421
|
-
max_turns: int,
|
|
422
424
|
) -> SpawnOutcome:
|
|
423
425
|
"""Walk the worker-spawn tiers and return the structured outcome.
|
|
424
426
|
|
|
427
|
+
The timeout bounds each tier; no turn cap reaches the headless grok worker.
|
|
428
|
+
The bounds check runs before the preflight, so a refused timeout is refused
|
|
429
|
+
on the claude tier too, not only on the path that reaches the grok runner.
|
|
430
|
+
|
|
425
431
|
Args:
|
|
426
432
|
role: Worker role name for preflight; mapped to a primary agent stem.
|
|
427
433
|
prompt_file: Path to the prompt file for headless workers.
|
|
@@ -429,11 +435,16 @@ def resolve_worker_spawn(
|
|
|
429
435
|
timeout_seconds: Timeout applied to each tier invocation.
|
|
430
436
|
is_claude_tier_enabled: When True, allow tier 3 on a Claude host.
|
|
431
437
|
run_state_directory: Run-scoped directory for leader sockets and cache.
|
|
432
|
-
max_turns: Maximum agent turns for the headless grok worker.
|
|
433
438
|
|
|
434
439
|
Returns:
|
|
435
440
|
The dispatcher outcome including the ordered attempts trail.
|
|
441
|
+
|
|
442
|
+
Raises:
|
|
443
|
+
WorkerTimeoutOutOfBoundsError: When ``timeout_seconds`` is missing,
|
|
444
|
+
below the floor, or above the ceiling. The CLI maps this to the
|
|
445
|
+
config-error exit code.
|
|
436
446
|
"""
|
|
447
|
+
require_timeout_within_bounds(timeout_seconds)
|
|
437
448
|
preflight_outcome: PreflightOutcome = spawn_preflight_runner(
|
|
438
449
|
role=role,
|
|
439
450
|
should_ping=False,
|
|
@@ -454,7 +465,6 @@ def resolve_worker_spawn(
|
|
|
454
465
|
prompt_file=prompt_file,
|
|
455
466
|
working_directory=working_directory,
|
|
456
467
|
run_state_directory=run_state_directory,
|
|
457
|
-
max_turns=max_turns,
|
|
458
468
|
timeout_seconds=timeout_seconds,
|
|
459
469
|
)
|
|
460
470
|
if grok_outcome.is_ok:
|
|
@@ -552,7 +562,7 @@ def _exit_code_for_outcome(
|
|
|
552
562
|
return SPAWN_SERVED_EXIT_CODE
|
|
553
563
|
|
|
554
564
|
|
|
555
|
-
def _config_error_outcome(configuration_error:
|
|
565
|
+
def _config_error_outcome(configuration_error: Exception) -> SpawnOutcome:
|
|
556
566
|
return SpawnOutcome(
|
|
557
567
|
tier_used=None,
|
|
558
568
|
is_ok=False,
|
|
@@ -574,6 +584,34 @@ def _missing_prompt_file_outcome() -> SpawnOutcome:
|
|
|
574
584
|
)
|
|
575
585
|
|
|
576
586
|
|
|
587
|
+
def _timeout_out_of_bounds_outcome(
|
|
588
|
+
bounds_error: WorkerTimeoutOutOfBoundsError,
|
|
589
|
+
) -> SpawnOutcome:
|
|
590
|
+
"""Report a refused timeout as a config fault rather than a worker failure.
|
|
591
|
+
|
|
592
|
+
::
|
|
593
|
+
|
|
594
|
+
--timeout-seconds 0 ok: attempts[0].reason timeout_out_of_bounds
|
|
595
|
+
--timeout-seconds 5401 ok: attempts[0].reason timeout_out_of_bounds
|
|
596
|
+
|
|
597
|
+
Args:
|
|
598
|
+
bounds_error: The refusal raised by the headless runner.
|
|
599
|
+
|
|
600
|
+
Returns:
|
|
601
|
+
A config-error outcome whose attempt reason names the violation.
|
|
602
|
+
"""
|
|
603
|
+
return SpawnOutcome(
|
|
604
|
+
tier_used=None,
|
|
605
|
+
is_ok=False,
|
|
606
|
+
all_attempts=(
|
|
607
|
+
_attempt(TIER_GROK, is_ok=False, reason=REASON_TIMEOUT_OUT_OF_BOUNDS),
|
|
608
|
+
),
|
|
609
|
+
captured_stdout=str(bounds_error),
|
|
610
|
+
returncode=SPAWN_CONFIG_ERROR_EXIT_CODE,
|
|
611
|
+
is_config_error=True,
|
|
612
|
+
)
|
|
613
|
+
|
|
614
|
+
|
|
577
615
|
def _write_spawn_outcome_and_exit_code(
|
|
578
616
|
spawn_outcome: SpawnOutcome, *, is_config_error: bool
|
|
579
617
|
) -> int:
|
|
@@ -588,6 +626,12 @@ def _write_spawn_outcome_and_exit_code(
|
|
|
588
626
|
def main(all_command_arguments: list[str]) -> int:
|
|
589
627
|
"""Run the dispatcher for CLI arguments and print the JSON outcome.
|
|
590
628
|
|
|
629
|
+
A timeout outside the accepted bounds is a config fault: the run prints a
|
|
630
|
+
structured outcome whose attempt reason is ``timeout_out_of_bounds`` and
|
|
631
|
+
exits ``3``, rather than raising out of the CLI. A chain misconfiguration,
|
|
632
|
+
or an unknown host profile refused by ``detect_host_profile``, prints the
|
|
633
|
+
same exit code with the refusal text as its output.
|
|
634
|
+
|
|
591
635
|
Args:
|
|
592
636
|
all_command_arguments: The argument vector after the program name.
|
|
593
637
|
|
|
@@ -612,9 +656,11 @@ def main(all_command_arguments: list[str]) -> int:
|
|
|
612
656
|
timeout_seconds=parsed_arguments.timeout_seconds,
|
|
613
657
|
is_claude_tier_enabled=parsed_arguments.is_claude_tier_enabled,
|
|
614
658
|
run_state_directory=run_state_directory,
|
|
615
|
-
max_turns=DEFAULT_SPAWN_MAX_TURNS,
|
|
616
659
|
)
|
|
617
|
-
except
|
|
660
|
+
except WorkerTimeoutOutOfBoundsError as bounds_error:
|
|
661
|
+
is_config_error = True
|
|
662
|
+
spawn_outcome = _timeout_out_of_bounds_outcome(bounds_error)
|
|
663
|
+
except (ChainConfigurationError, ValueError) as configuration_error:
|
|
618
664
|
is_config_error = True
|
|
619
665
|
spawn_outcome = _config_error_outcome(configuration_error)
|
|
620
666
|
return _write_spawn_outcome_and_exit_code(
|