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
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""Specifications for what a blocked mint tells the person it blocked.
|
|
2
|
+
|
|
3
|
+
Enforcement refuses every push until a clean stamp covers the branch surface,
|
|
4
|
+
and ``invoke_code_review.py --record-stamp`` is the only way to mint one. When
|
|
5
|
+
that call cannot run, the reason it names is the person's only route back to a
|
|
6
|
+
working push.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
|
|
15
|
+
import invoke_code_review as invoker
|
|
16
|
+
from claude_chain_runner import ChainConfigurationError, ChainInvocationOutcome
|
|
17
|
+
from _code_review_test_support import FIXTURE_SESSION_OPUS
|
|
18
|
+
from dev_env_scripts_constants.code_review_constants import (
|
|
19
|
+
DEFAULT_CODE_REVIEW_EFFORT,
|
|
20
|
+
PERMISSION_MODE_ACCEPT_EDITS,
|
|
21
|
+
PERMISSION_MODE_BYPASS,
|
|
22
|
+
REVIEW_PERMISSION_MODE,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_review_arguments_carry_the_permission_mode_this_caller_resolves() -> None:
|
|
27
|
+
"""The review command asks for a permission mode the binary accepts here.
|
|
28
|
+
|
|
29
|
+
The binary refuses the bypass mode outright for a root caller, so asking
|
|
30
|
+
for it there means no review runs and no stamp is ever minted.
|
|
31
|
+
"""
|
|
32
|
+
all_arguments = invoker.build_code_review_arguments(DEFAULT_CODE_REVIEW_EFFORT)
|
|
33
|
+
|
|
34
|
+
assert REVIEW_PERMISSION_MODE in all_arguments
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_the_resolved_permission_mode_is_one_the_binary_knows() -> None:
|
|
38
|
+
assert REVIEW_PERMISSION_MODE in (
|
|
39
|
+
PERMISSION_MODE_ACCEPT_EDITS,
|
|
40
|
+
PERMISSION_MODE_BYPASS,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
CHAIN_CONFIG_REMEDY_TEXT: str = (
|
|
45
|
+
"Claude chain config not found at the path this specification names. "
|
|
46
|
+
"Copy the example config there and list your account binaries."
|
|
47
|
+
)
|
|
48
|
+
HOST_PROFILE_FAILURE_TEXT: str = "session model alias carries no host profile"
|
|
49
|
+
MINT_TIMEOUT_SECONDS: int = 1
|
|
50
|
+
SERVED_COMMAND_NAME: str = "claude"
|
|
51
|
+
REVIEW_BINARY_REFUSAL_TEXT: str = (
|
|
52
|
+
"--dangerously-skip-permissions cannot be used with root privileges"
|
|
53
|
+
)
|
|
54
|
+
REVIEW_FAILURE_RETURNCODE: int = 1
|
|
55
|
+
EMPTY_REVIEW_STDOUT: str = ""
|
|
56
|
+
ROOT_USER_ID: int = 0
|
|
57
|
+
UNPRIVILEGED_USER_ID: int = 1000
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _serve_a_refusing_binary(
|
|
61
|
+
*_all_positional: object, **_all_keyword: object
|
|
62
|
+
) -> ChainInvocationOutcome:
|
|
63
|
+
return ChainInvocationOutcome(
|
|
64
|
+
served_command=SERVED_COMMAND_NAME,
|
|
65
|
+
returncode=REVIEW_FAILURE_RETURNCODE,
|
|
66
|
+
stdout=EMPTY_REVIEW_STDOUT,
|
|
67
|
+
stderr=REVIEW_BINARY_REFUSAL_TEXT,
|
|
68
|
+
attempts=(),
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_failed_review_reports_what_the_served_binary_wrote(
|
|
73
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
74
|
+
tmp_path: Path,
|
|
75
|
+
capsys: pytest.CaptureFixture[str],
|
|
76
|
+
) -> None:
|
|
77
|
+
"""A review that a served binary refused names the refusal.
|
|
78
|
+
|
|
79
|
+
The binary can decline for reasons the caller must act on, such as a
|
|
80
|
+
permission mode it will not accept. Dropping its words leaves a bare
|
|
81
|
+
exit code, and no stamp is minted either way.
|
|
82
|
+
"""
|
|
83
|
+
monkeypatch.setattr(
|
|
84
|
+
invoker, "_run_claude_with_empty_stdin", _serve_a_refusing_binary
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
outcome = invoker._run_chain_review(
|
|
88
|
+
working_directory=tmp_path,
|
|
89
|
+
timeout_seconds=MINT_TIMEOUT_SECONDS,
|
|
90
|
+
effort=DEFAULT_CODE_REVIEW_EFFORT,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
captured_streams = capsys.readouterr()
|
|
94
|
+
assert outcome.returncode == REVIEW_FAILURE_RETURNCODE
|
|
95
|
+
assert REVIEW_BINARY_REFUSAL_TEXT in captured_streams.err
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_missing_chain_config_reports_its_remedy_on_stderr(
|
|
99
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
100
|
+
tmp_path: Path,
|
|
101
|
+
capsys: pytest.CaptureFixture[str],
|
|
102
|
+
) -> None:
|
|
103
|
+
def raise_chain_configuration_error(**_all_keyword_arguments: object) -> None:
|
|
104
|
+
raise ChainConfigurationError(CHAIN_CONFIG_REMEDY_TEXT)
|
|
105
|
+
|
|
106
|
+
monkeypatch.setattr(
|
|
107
|
+
invoker, "invoke_code_review_and_record_stamp", raise_chain_configuration_error
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
outcome = invoker._mint_or_config_outcome(
|
|
111
|
+
working_directory=tmp_path,
|
|
112
|
+
session_model=FIXTURE_SESSION_OPUS,
|
|
113
|
+
timeout_seconds=MINT_TIMEOUT_SECONDS,
|
|
114
|
+
effort=DEFAULT_CODE_REVIEW_EFFORT,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
captured_streams = capsys.readouterr()
|
|
118
|
+
assert outcome.is_stamp_minted is False
|
|
119
|
+
assert CHAIN_CONFIG_REMEDY_TEXT in captured_streams.err
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def test_host_profile_failure_reports_its_reason_on_stderr(
|
|
123
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
124
|
+
tmp_path: Path,
|
|
125
|
+
capsys: pytest.CaptureFixture[str],
|
|
126
|
+
) -> None:
|
|
127
|
+
def raise_host_profile_error(**_all_keyword_arguments: object) -> None:
|
|
128
|
+
raise ValueError(HOST_PROFILE_FAILURE_TEXT)
|
|
129
|
+
|
|
130
|
+
monkeypatch.setattr(
|
|
131
|
+
invoker, "invoke_code_review_and_record_stamp", raise_host_profile_error
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
outcome = invoker._mint_or_config_outcome(
|
|
135
|
+
working_directory=tmp_path,
|
|
136
|
+
session_model=FIXTURE_SESSION_OPUS,
|
|
137
|
+
timeout_seconds=MINT_TIMEOUT_SECONDS,
|
|
138
|
+
effort=DEFAULT_CODE_REVIEW_EFFORT,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
captured_streams = capsys.readouterr()
|
|
142
|
+
assert outcome.is_stamp_minted is False
|
|
143
|
+
assert HOST_PROFILE_FAILURE_TEXT in captured_streams.err
|
|
@@ -19,7 +19,7 @@ from _code_review_test_support import (
|
|
|
19
19
|
from dev_env_scripts_constants.code_review_constants import (
|
|
20
20
|
CODE_REVIEW_MODEL_ALIAS,
|
|
21
21
|
DEFAULT_CODE_REVIEW_EFFORT,
|
|
22
|
-
PERMISSION_MODE_BYPASS,
|
|
22
|
+
REVIEW_PERMISSION_MODE as PERMISSION_MODE_BYPASS,
|
|
23
23
|
PERMISSION_MODE_FLAG,
|
|
24
24
|
)
|
|
25
25
|
from dev_env_scripts_constants.grok_worker_constants import (
|
|
@@ -33,7 +33,7 @@ from dev_env_scripts_constants.code_review_constants import (
|
|
|
33
33
|
MAXIMUM_STAMP_MINT_PASSES,
|
|
34
34
|
MODE_CHAIN,
|
|
35
35
|
MODE_IN_SESSION,
|
|
36
|
-
PERMISSION_MODE_BYPASS,
|
|
36
|
+
REVIEW_PERMISSION_MODE as PERMISSION_MODE_BYPASS,
|
|
37
37
|
PERMISSION_MODE_FLAG,
|
|
38
38
|
RESULT_KEY_BOUND_HASH,
|
|
39
39
|
RESULT_KEY_DIRTY_TREE,
|
|
@@ -35,17 +35,20 @@ from dev_env_scripts_constants.grok_worker_constants import ( # noqa: E402
|
|
|
35
35
|
CLI_TIMEOUT_FLAG,
|
|
36
36
|
CLASSIFICATION_AUTH_FAILURE,
|
|
37
37
|
CLASSIFICATION_OK,
|
|
38
|
+
CLASSIFICATION_TIMEOUT,
|
|
38
39
|
CLASSIFICATION_USAGE_LIMIT,
|
|
39
40
|
CWD_FLAG,
|
|
40
41
|
DEFAULT_ROLE,
|
|
41
|
-
DEFAULT_SPAWN_MAX_TURNS,
|
|
42
42
|
DEFAULT_WORKER_TIMEOUT_SECONDS,
|
|
43
|
+
MAXIMUM_WORKER_TIMEOUT_SECONDS,
|
|
44
|
+
MIN_WORKER_TIMEOUT_SECONDS,
|
|
43
45
|
OUTPUT_FORMAT_FLAG,
|
|
44
46
|
OUTPUT_FORMAT_JSON,
|
|
45
47
|
PROMPT_FILE_FLAG,
|
|
46
48
|
REASON_CLAUDE_AGENT_REQUIRED,
|
|
47
49
|
REASON_GROK_AUTH_FAILED,
|
|
48
50
|
REASON_PROMPT_FILE_MISSING,
|
|
51
|
+
REASON_TIMEOUT_OUT_OF_BOUNDS,
|
|
49
52
|
RESULT_KEY_ATTEMPTS,
|
|
50
53
|
RESULT_KEY_OK,
|
|
51
54
|
RESULT_KEY_OUTPUT,
|
|
@@ -59,11 +62,16 @@ from dev_env_scripts_constants.grok_worker_constants import ( # noqa: E402
|
|
|
59
62
|
TIER_CLAUDE_HEADLESS,
|
|
60
63
|
TIER_GROK,
|
|
61
64
|
)
|
|
62
|
-
|
|
65
|
+
import grok_headless_runner # noqa: E402
|
|
66
|
+
from grok_headless_runner import ( # noqa: E402
|
|
67
|
+
GrokRunnerOutcome,
|
|
68
|
+
run_headless_worker,
|
|
69
|
+
)
|
|
63
70
|
from grok_worker_preflight import PreflightOutcome # noqa: E402
|
|
64
71
|
|
|
65
72
|
HOST_PROFILE_CLAUDE = "Claude"
|
|
66
73
|
HOST_PROFILE_THIRD_PARTY = "ThirdParty"
|
|
74
|
+
NON_POSITIVE_TIMEOUT_SECONDS = 0
|
|
67
75
|
|
|
68
76
|
FIXTURE_GROK_STDOUT = '{"tier":"grok","status":"done"}'
|
|
69
77
|
FIXTURE_CLAUDE_STDOUT = '{"tier":"claude","status":"done"}'
|
|
@@ -72,6 +80,7 @@ FIXTURE_GROK_RETURNCODE = 0
|
|
|
72
80
|
FIXTURE_CLAUDE_RETURNCODE = 0
|
|
73
81
|
FIXTURE_FAILED_RETURNCODE = 1
|
|
74
82
|
FIXTURE_ROLE = "code-quality-agent"
|
|
83
|
+
MIN_WORKER_TIMEOUT_SECONDS_CONSTANT_NAME = "MIN_WORKER_TIMEOUT_SECONDS"
|
|
75
84
|
LARGE_PROMPT_CHARACTER_COUNT = 40000
|
|
76
85
|
WINDOWS_SAFE_ARGV_ELEMENT_CEILING = 8192
|
|
77
86
|
EXPECTED_PRIMARY_AGENT_FOR_DEFAULT_ROLE = Path(
|
|
@@ -261,7 +270,6 @@ def test_grok_ok_serves_tier_one(
|
|
|
261
270
|
timeout_seconds=DEFAULT_WORKER_TIMEOUT_SECONDS,
|
|
262
271
|
is_claude_tier_enabled=False,
|
|
263
272
|
run_state_directory=run_state_directory,
|
|
264
|
-
max_turns=DEFAULT_SPAWN_MAX_TURNS,
|
|
265
273
|
)
|
|
266
274
|
|
|
267
275
|
assert spawn_outcome.is_ok is True
|
|
@@ -297,7 +305,6 @@ def test_grok_usage_limited_on_claude_host_requires_agent(
|
|
|
297
305
|
timeout_seconds=DEFAULT_WORKER_TIMEOUT_SECONDS,
|
|
298
306
|
is_claude_tier_enabled=False,
|
|
299
307
|
run_state_directory=run_state_directory,
|
|
300
|
-
max_turns=DEFAULT_SPAWN_MAX_TURNS,
|
|
301
308
|
)
|
|
302
309
|
|
|
303
310
|
assert spawn_outcome.is_ok is False
|
|
@@ -313,6 +320,128 @@ def test_grok_usage_limited_on_claude_host_requires_agent(
|
|
|
313
320
|
assert spawn_outcome.all_attempts[1].reason == REASON_CLAUDE_AGENT_REQUIRED
|
|
314
321
|
|
|
315
322
|
|
|
323
|
+
def test_out_of_bounds_timeout_is_reported_as_config_not_worker_failure(
|
|
324
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
|
325
|
+
) -> None:
|
|
326
|
+
"""A refused timeout prints a structured outcome and exits 3, never a traceback.
|
|
327
|
+
|
|
328
|
+
::
|
|
329
|
+
|
|
330
|
+
--timeout-seconds 0 ok: reason timeout_out_of_bounds, exit 3
|
|
331
|
+
--timeout-seconds 5401 ok: reason timeout_out_of_bounds, exit 3
|
|
332
|
+
"""
|
|
333
|
+
prompt_file, working_directory, run_state_directory = _paths(tmp_path)
|
|
334
|
+
_install_seams(monkeypatch, grok_outcome=_grok_ok())
|
|
335
|
+
monkeypatch.setattr(
|
|
336
|
+
dispatcher, "spawn_grok_runner", grok_headless_runner.run_headless_worker
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
for each_refused_timeout in (
|
|
340
|
+
NON_POSITIVE_TIMEOUT_SECONDS,
|
|
341
|
+
MAXIMUM_WORKER_TIMEOUT_SECONDS + 1,
|
|
342
|
+
):
|
|
343
|
+
exit_code = dispatcher.main(
|
|
344
|
+
[
|
|
345
|
+
CLI_ROLE_FLAG,
|
|
346
|
+
FIXTURE_ROLE,
|
|
347
|
+
PROMPT_FILE_FLAG,
|
|
348
|
+
str(prompt_file),
|
|
349
|
+
CWD_FLAG,
|
|
350
|
+
str(working_directory),
|
|
351
|
+
CLI_TIMEOUT_FLAG,
|
|
352
|
+
str(each_refused_timeout),
|
|
353
|
+
CLI_RUN_STATE_DIR_FLAG,
|
|
354
|
+
str(run_state_directory),
|
|
355
|
+
]
|
|
356
|
+
)
|
|
357
|
+
parsed_payload = json.loads(capsys.readouterr().out)
|
|
358
|
+
all_attempt_reasons = [
|
|
359
|
+
each_attempt[ATTEMPT_KEY_REASON]
|
|
360
|
+
for each_attempt in parsed_payload[RESULT_KEY_ATTEMPTS]
|
|
361
|
+
]
|
|
362
|
+
|
|
363
|
+
assert exit_code == SPAWN_CONFIG_ERROR_EXIT_CODE, each_refused_timeout
|
|
364
|
+
assert parsed_payload[RESULT_KEY_OK] is False, each_refused_timeout
|
|
365
|
+
assert parsed_payload[RESULT_KEY_TIER_USED] is None, each_refused_timeout
|
|
366
|
+
assert REASON_TIMEOUT_OUT_OF_BOUNDS in all_attempt_reasons, each_refused_timeout
|
|
367
|
+
|
|
368
|
+
|
|
369
|
+
def test_out_of_bounds_timeout_is_refused_when_the_grok_tier_is_unreachable(
|
|
370
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
|
371
|
+
) -> None:
|
|
372
|
+
"""The bounds hold on a host whose preflight never reaches the grok runner.
|
|
373
|
+
|
|
374
|
+
::
|
|
375
|
+
|
|
376
|
+
preflight unusable, --timeout-seconds 5401
|
|
377
|
+
ok: reason timeout_out_of_bounds, exit 3, no tier invoked
|
|
378
|
+
"""
|
|
379
|
+
prompt_file, working_directory, run_state_directory = _paths(tmp_path)
|
|
380
|
+
call_log = _install_seams(
|
|
381
|
+
monkeypatch,
|
|
382
|
+
preflight_outcome=_fallthrough_preflight(REASON_GROK_AUTH_FAILED),
|
|
383
|
+
claude_outcome=_claude_served(),
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
exit_code = dispatcher.main(
|
|
387
|
+
[
|
|
388
|
+
CLI_ROLE_FLAG,
|
|
389
|
+
FIXTURE_ROLE,
|
|
390
|
+
PROMPT_FILE_FLAG,
|
|
391
|
+
str(prompt_file),
|
|
392
|
+
CWD_FLAG,
|
|
393
|
+
str(working_directory),
|
|
394
|
+
CLI_TIMEOUT_FLAG,
|
|
395
|
+
str(MAXIMUM_WORKER_TIMEOUT_SECONDS + 1),
|
|
396
|
+
CLI_RUN_STATE_DIR_FLAG,
|
|
397
|
+
str(run_state_directory),
|
|
398
|
+
]
|
|
399
|
+
)
|
|
400
|
+
parsed_payload = json.loads(capsys.readouterr().out)
|
|
401
|
+
all_attempt_reasons = [
|
|
402
|
+
each_attempt[ATTEMPT_KEY_REASON]
|
|
403
|
+
for each_attempt in parsed_payload[RESULT_KEY_ATTEMPTS]
|
|
404
|
+
]
|
|
405
|
+
|
|
406
|
+
assert exit_code == SPAWN_CONFIG_ERROR_EXIT_CODE
|
|
407
|
+
assert REASON_TIMEOUT_OUT_OF_BOUNDS in all_attempt_reasons
|
|
408
|
+
assert call_log.preflight_calls == 0
|
|
409
|
+
assert call_log.claude_calls == 0
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def test_timed_out_grok_worker_is_recorded_as_timeout_not_served(
|
|
413
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
414
|
+
) -> None:
|
|
415
|
+
"""A killed worker reaches the dispatcher's trail as timeout, never as served.
|
|
416
|
+
|
|
417
|
+
::
|
|
418
|
+
|
|
419
|
+
grok tier times out ok: attempt reason timeout, tier_used moves on
|
|
420
|
+
"""
|
|
421
|
+
prompt_file, working_directory, run_state_directory = _paths(tmp_path)
|
|
422
|
+
call_log = _install_seams(
|
|
423
|
+
monkeypatch,
|
|
424
|
+
grok_outcome=_grok_failure(CLASSIFICATION_TIMEOUT),
|
|
425
|
+
claude_outcome=_claude_served(),
|
|
426
|
+
host_profile=HOST_PROFILE_THIRD_PARTY,
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
spawn_outcome = dispatcher.resolve_worker_spawn(
|
|
430
|
+
role=FIXTURE_ROLE,
|
|
431
|
+
prompt_file=prompt_file,
|
|
432
|
+
working_directory=working_directory,
|
|
433
|
+
timeout_seconds=DEFAULT_WORKER_TIMEOUT_SECONDS,
|
|
434
|
+
is_claude_tier_enabled=False,
|
|
435
|
+
run_state_directory=run_state_directory,
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
assert call_log.grok_calls == 1
|
|
439
|
+
assert spawn_outcome.all_attempts[0].tier == TIER_GROK
|
|
440
|
+
assert spawn_outcome.all_attempts[0].is_ok is False
|
|
441
|
+
assert spawn_outcome.all_attempts[0].reason == CLASSIFICATION_TIMEOUT
|
|
442
|
+
assert spawn_outcome.tier_used != TIER_GROK
|
|
443
|
+
|
|
444
|
+
|
|
316
445
|
def test_grok_auth_failed_on_third_party_runs_tier_three(
|
|
317
446
|
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
318
447
|
) -> None:
|
|
@@ -331,7 +460,6 @@ def test_grok_auth_failed_on_third_party_runs_tier_three(
|
|
|
331
460
|
timeout_seconds=DEFAULT_WORKER_TIMEOUT_SECONDS,
|
|
332
461
|
is_claude_tier_enabled=False,
|
|
333
462
|
run_state_directory=run_state_directory,
|
|
334
|
-
max_turns=DEFAULT_SPAWN_MAX_TURNS,
|
|
335
463
|
)
|
|
336
464
|
|
|
337
465
|
assert spawn_outcome.is_ok is True
|
|
@@ -423,6 +551,40 @@ def test_config_error_returns_exit_three(
|
|
|
423
551
|
assert parsed_payload[RESULT_KEY_RETURNCODE] == SPAWN_CONFIG_ERROR_EXIT_CODE
|
|
424
552
|
|
|
425
553
|
|
|
554
|
+
def test_below_floor_timeout_returns_json_config_exit(
|
|
555
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
|
556
|
+
) -> None:
|
|
557
|
+
"""A below-floor timeout reads as a config error, not an escaping traceback."""
|
|
558
|
+
prompt_file, working_directory, run_state_directory = _paths(tmp_path)
|
|
559
|
+
_install_seams(monkeypatch, grok_outcome=_grok_ok())
|
|
560
|
+
monkeypatch.setattr(dispatcher, "spawn_grok_runner", run_headless_worker)
|
|
561
|
+
below_floor_timeout_seconds = MIN_WORKER_TIMEOUT_SECONDS - 1
|
|
562
|
+
|
|
563
|
+
exit_code = dispatcher.main(
|
|
564
|
+
[
|
|
565
|
+
CLI_ROLE_FLAG,
|
|
566
|
+
FIXTURE_ROLE,
|
|
567
|
+
PROMPT_FILE_FLAG,
|
|
568
|
+
str(prompt_file),
|
|
569
|
+
CWD_FLAG,
|
|
570
|
+
str(working_directory),
|
|
571
|
+
CLI_TIMEOUT_FLAG,
|
|
572
|
+
str(below_floor_timeout_seconds),
|
|
573
|
+
CLI_RUN_STATE_DIR_FLAG,
|
|
574
|
+
str(run_state_directory),
|
|
575
|
+
]
|
|
576
|
+
)
|
|
577
|
+
|
|
578
|
+
assert exit_code == SPAWN_CONFIG_ERROR_EXIT_CODE
|
|
579
|
+
captured = capsys.readouterr()
|
|
580
|
+
assert captured.err == ""
|
|
581
|
+
parsed_payload = json.loads(captured.out)
|
|
582
|
+
assert parsed_payload[RESULT_KEY_OK] is False
|
|
583
|
+
assert parsed_payload[RESULT_KEY_TIER_USED] is None
|
|
584
|
+
assert parsed_payload[RESULT_KEY_RETURNCODE] == SPAWN_CONFIG_ERROR_EXIT_CODE
|
|
585
|
+
assert MIN_WORKER_TIMEOUT_SECONDS_CONSTANT_NAME in parsed_payload[RESULT_KEY_OUTPUT]
|
|
586
|
+
|
|
587
|
+
|
|
426
588
|
def test_attempts_array_ordering_across_tiers(
|
|
427
589
|
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
428
590
|
) -> None:
|
|
@@ -441,7 +603,6 @@ def test_attempts_array_ordering_across_tiers(
|
|
|
441
603
|
timeout_seconds=DEFAULT_WORKER_TIMEOUT_SECONDS,
|
|
442
604
|
is_claude_tier_enabled=True,
|
|
443
605
|
run_state_directory=run_state_directory,
|
|
444
|
-
max_turns=DEFAULT_SPAWN_MAX_TURNS,
|
|
445
606
|
)
|
|
446
607
|
|
|
447
608
|
all_tiers = [each_attempt.tier for each_attempt in spawn_outcome.all_attempts]
|
|
@@ -515,7 +676,6 @@ def test_preflight_fallthrough_skips_grok_runner(
|
|
|
515
676
|
timeout_seconds=DEFAULT_WORKER_TIMEOUT_SECONDS,
|
|
516
677
|
is_claude_tier_enabled=False,
|
|
517
678
|
run_state_directory=run_state_directory,
|
|
518
|
-
max_turns=DEFAULT_SPAWN_MAX_TURNS,
|
|
519
679
|
)
|
|
520
680
|
|
|
521
681
|
assert call_log.grok_calls == 0
|
|
@@ -533,9 +693,15 @@ def test_detect_host_profile_is_consumed_not_reimplemented() -> None:
|
|
|
533
693
|
assert "THIRD_PARTY" not in source_text
|
|
534
694
|
|
|
535
695
|
|
|
536
|
-
def
|
|
696
|
+
def test_dispatcher_grok_invocation_carries_no_turn_cap(
|
|
537
697
|
monkeypatch: pytest.MonkeyPatch, tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
|
538
698
|
) -> None:
|
|
699
|
+
"""The dispatcher hands the runner no turn cap; the timeout is the only bound.
|
|
700
|
+
|
|
701
|
+
::
|
|
702
|
+
|
|
703
|
+
grok tier kwargs ok: timeout_seconds present, max_turns absent
|
|
704
|
+
"""
|
|
539
705
|
prompt_file, working_directory, run_state_directory = _paths(tmp_path)
|
|
540
706
|
call_log = _install_seams(
|
|
541
707
|
monkeypatch,
|
|
@@ -559,7 +725,10 @@ def test_default_max_turns_reaches_grok_kwargs(
|
|
|
559
725
|
|
|
560
726
|
assert exit_code == SPAWN_SERVED_EXIT_CODE
|
|
561
727
|
assert call_log.grok_keyword_arguments is not None
|
|
562
|
-
assert
|
|
728
|
+
assert "max_turns" not in call_log.grok_keyword_arguments
|
|
729
|
+
assert call_log.grok_keyword_arguments["timeout_seconds"] == (
|
|
730
|
+
DEFAULT_WORKER_TIMEOUT_SECONDS
|
|
731
|
+
)
|
|
563
732
|
parsed_payload = json.loads(capsys.readouterr().out)
|
|
564
733
|
assert parsed_payload[RESULT_KEY_OK] is True
|
|
565
734
|
|
|
@@ -645,7 +814,6 @@ def test_default_role_maps_to_primary_agent_stem(
|
|
|
645
814
|
timeout_seconds=DEFAULT_WORKER_TIMEOUT_SECONDS,
|
|
646
815
|
is_claude_tier_enabled=False,
|
|
647
816
|
run_state_directory=run_state_directory,
|
|
648
|
-
max_turns=DEFAULT_SPAWN_MAX_TURNS,
|
|
649
817
|
)
|
|
650
818
|
|
|
651
819
|
assert spawn_outcome.is_ok is True
|
|
@@ -674,7 +842,6 @@ def test_tier_three_argv_includes_agent_for_default_role(
|
|
|
674
842
|
timeout_seconds=DEFAULT_WORKER_TIMEOUT_SECONDS,
|
|
675
843
|
is_claude_tier_enabled=False,
|
|
676
844
|
run_state_directory=run_state_directory,
|
|
677
|
-
max_turns=DEFAULT_SPAWN_MAX_TURNS,
|
|
678
845
|
)
|
|
679
846
|
|
|
680
847
|
assert spawn_outcome.tier_used == TIER_CLAUDE_HEADLESS
|
|
@@ -708,7 +875,6 @@ def test_large_prompt_stays_out_of_claude_argv(
|
|
|
708
875
|
timeout_seconds=DEFAULT_WORKER_TIMEOUT_SECONDS,
|
|
709
876
|
is_claude_tier_enabled=False,
|
|
710
877
|
run_state_directory=run_state_directory,
|
|
711
|
-
max_turns=DEFAULT_SPAWN_MAX_TURNS,
|
|
712
878
|
)
|
|
713
879
|
|
|
714
880
|
assert spawn_outcome.tier_used == TIER_CLAUDE_HEADLESS
|
|
@@ -926,8 +1092,7 @@ def test_headless_chain_runner_lock_serializes_distinct_cwds(
|
|
|
926
1092
|
timeout_seconds=DEFAULT_WORKER_TIMEOUT_SECONDS,
|
|
927
1093
|
is_claude_tier_enabled=False,
|
|
928
1094
|
run_state_directory=run_state_directory,
|
|
929
|
-
|
|
930
|
-
)
|
|
1095
|
+
)
|
|
931
1096
|
except (OSError, RuntimeError, ValueError, AssertionError) as raised_error:
|
|
932
1097
|
all_errors.append(raised_error)
|
|
933
1098
|
|
|
@@ -1005,7 +1170,6 @@ def test_usage_limit_fallover_delivers_full_prompt_to_each_binary(
|
|
|
1005
1170
|
timeout_seconds=DEFAULT_WORKER_TIMEOUT_SECONDS,
|
|
1006
1171
|
is_claude_tier_enabled=False,
|
|
1007
1172
|
run_state_directory=run_state_directory,
|
|
1008
|
-
max_turns=DEFAULT_SPAWN_MAX_TURNS,
|
|
1009
1173
|
)
|
|
1010
1174
|
|
|
1011
1175
|
assert prompt_text_by_command["claude"] == FIXTURE_PROMPT_TEXT
|