claude-dev-env 2.7.0 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/_shared/CLAUDE.md +1 -0
  2. package/_shared/advisor/advisor-protocol.md +19 -9
  3. package/_shared/pr-loop/audit-contract.md +4 -4
  4. package/_shared/pr-loop/precatch-rubric.md +2 -2
  5. package/_shared/pr-loop/worker-spawn.md +3 -1
  6. package/_shared/process-tree/CLAUDE.md +41 -0
  7. package/_shared/process-tree/scripts/config/process_tree_scripts_constants/__init__.py +1 -0
  8. package/_shared/process-tree/scripts/config/process_tree_scripts_constants/process_tree_kill_constants.py +27 -0
  9. package/_shared/process-tree/scripts/process_tree_kill.py +141 -0
  10. package/_shared/process-tree/scripts/pyproject.toml +16 -0
  11. package/_shared/process-tree/scripts/test_process_tree_kill.py +278 -0
  12. package/agents/code-quality-agent.md +6 -5
  13. package/agents/deep-research.md +7 -24
  14. package/agents/docs-agent.md +1 -27
  15. package/agents/issue-tracker.md +1 -7
  16. package/agents/skill-writer-agent.md +1 -2
  17. package/agents/test_agent_frontmatter.py +309 -12
  18. package/hooks/blocking/CLAUDE.md +2 -0
  19. package/hooks/blocking/fable_spawn_gate.py +187 -0
  20. package/hooks/blocking/piped_pytest_blocker.py +1223 -0
  21. package/hooks/blocking/plain_language_blocker.py +287 -15
  22. package/hooks/blocking/test_fable_spawn_gate.py +374 -0
  23. package/hooks/blocking/test_piped_pytest_blocker.py +587 -0
  24. package/hooks/blocking/test_plain_language_blocker.py +277 -2
  25. package/hooks/blocking/test_pre_tool_use_dispatcher.py +34 -2
  26. package/hooks/git-hooks/CLAUDE.md +2 -2
  27. package/hooks/git-hooks/git_hooks_constants/__init__.py +28 -0
  28. package/hooks/git-hooks/pre_push.py +343 -54
  29. package/hooks/git-hooks/test_pre_push.py +852 -6
  30. package/hooks/hooks.json +9 -19
  31. package/hooks/hooks_constants/CLAUDE.md +2 -0
  32. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +1 -0
  33. package/hooks/hooks_constants/fable_spawn_gate_constants.py +62 -0
  34. package/hooks/hooks_constants/piped_pytest_blocker_constants.py +360 -0
  35. package/hooks/hooks_constants/plain_language_blocker_constants.py +64 -1
  36. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +10 -0
  37. package/hooks/hooks_constants/shell_command_segments.py +1 -1
  38. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +1 -0
  39. package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +27 -0
  40. package/hooks/hooks_constants/test_prose_metrics_parity.py +124 -0
  41. package/package.json +1 -1
  42. package/rules/CLAUDE.md +1 -0
  43. package/rules/ask-user-question-required.md +26 -0
  44. package/rules/claims-as-quotes.md +65 -0
  45. package/scripts/CLAUDE.md +4 -4
  46. package/scripts/_code_review_test_support.py +6 -0
  47. package/scripts/check.ps1 +18 -5
  48. package/scripts/claude_chain_runner.py +203 -31
  49. package/scripts/codec_forwarding_test_support.py +2 -0
  50. package/scripts/dev_env_scripts_constants/CLAUDE.md +4 -4
  51. package/scripts/dev_env_scripts_constants/claude_chain_constants.py +38 -0
  52. package/scripts/dev_env_scripts_constants/code_review_constants.py +403 -2
  53. package/scripts/dev_env_scripts_constants/grok_worker_constants.py +83 -13
  54. package/scripts/grok_headless_runner.py +148 -18
  55. package/scripts/resolve_worker_spawn.py +56 -10
  56. package/scripts/spawn_grok_batch.py +81 -23
  57. package/scripts/test_claude_chain_runner.py +358 -0
  58. package/scripts/test_grok_headless_runner.py +547 -10
  59. package/scripts/test_invoke_code_review.py +298 -0
  60. package/scripts/test_resolve_worker_spawn.py +185 -15
  61. package/scripts/test_spawn_grok_batch.py +326 -22
  62. package/scripts/tests/CLAUDE.md +1 -0
  63. package/scripts/tests/test_grok_worker_constants.py +59 -0
  64. package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +46 -0
  65. package/skills/autoconverge/workflow/converge.contract.test.mjs +133 -8
  66. package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +107 -1
  67. package/skills/autoconverge/workflow/converge.mjs +113 -31
  68. package/skills/codex-review/scripts/codex_review_scripts_constants/codex_usage_probe_constants.py +0 -4
  69. package/skills/codex-review/scripts/codex_usage_probe.py +20 -33
  70. package/skills/codex-review/scripts/run_codex_review.py +16 -64
  71. package/skills/codex-review/scripts/test_codex_usage_probe.py +46 -41
  72. package/skills/codex-review/scripts/test_run_codex_review.py +1 -33
  73. package/skills/e-code-review/SKILL.md +9 -8
  74. package/skills/e-code-review/reference/fix.md +29 -7
  75. package/skills/e-code-review/reference/loop.md +230 -14
  76. package/skills/e-code-review/reference/low.md +33 -15
  77. package/skills/e-code-review/reference/medium.md +55 -21
  78. package/skills/e-code-review/reference/xhigh.md +30 -12
  79. package/skills/fresh-branch/CLAUDE.md +5 -5
  80. package/skills/fresh-branch/SKILL.md +14 -6
  81. package/skills/fresh-branch/scripts/create_fresh_branch.py +122 -39
  82. package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +10 -3
  83. package/skills/fresh-branch/scripts/test_create_fresh_branch.py +251 -0
  84. package/skills/grok-spawn/SKILL.md +10 -3
  85. package/skills/grok-spawn/reference/flag-profiles.md +3 -1
  86. package/skills/orchestrator/SKILL.md +4 -1
  87. package/skills/orchestrator-refresh/SKILL.md +5 -1
  88. package/skills/team-advisor/SKILL.md +4 -1
@@ -15,11 +15,37 @@ import pytest
15
15
  import invoke_code_review as invoker
16
16
  from claude_chain_runner import ChainConfigurationError, ChainInvocationOutcome
17
17
  from _code_review_test_support import FIXTURE_SESSION_OPUS
18
+ from dev_env_scripts_constants.claude_chain_constants import TERMINAL_STATUS_SERVED
18
19
  from dev_env_scripts_constants.code_review_constants import (
20
+ ALL_FINDING_SEVERITIES,
21
+ ALL_LOOP_TERMINALS,
22
+ ALL_RETAINED_VERIFICATION_VERDICTS,
19
23
  DEFAULT_CODE_REVIEW_EFFORT,
24
+ FINDING_FIELD_SEVERITY,
25
+ FINDING_FIELD_VERDICT,
20
26
  PERMISSION_MODE_ACCEPT_EDITS,
21
27
  PERMISSION_MODE_BYPASS,
28
+ RESULT_KEY_DRAFT_PRESERVED,
29
+ RESULT_KEY_REVIEWED_HEAD_COUNT,
30
+ RESULT_KEY_SURVIVING_FINDINGS,
31
+ RESULT_KEY_TERMINAL,
22
32
  REVIEW_PERMISSION_MODE,
33
+ SEVERITY_BLOCKER,
34
+ SEVERITY_HIGH,
35
+ SEVERITY_LOW,
36
+ SEVERITY_MEDIUM,
37
+ SEVERITY_NIT,
38
+ TERMINAL_ADVISOR_BLOCKED,
39
+ TERMINAL_CLEAN,
40
+ TERMINAL_NITS_FIXED,
41
+ VERDICT_CONFIRMED,
42
+ VERDICT_PLAUSIBLE,
43
+ VERDICT_REFUTED,
44
+ all_findings_carry_severity_and_verdict,
45
+ encode_review_loop_terminal_result,
46
+ is_nits_only_findings,
47
+ record_reviewed_head,
48
+ resolve_review_loop_terminal,
23
49
  )
24
50
 
25
51
 
@@ -66,6 +92,7 @@ def _serve_a_refusing_binary(
66
92
  stdout=EMPTY_REVIEW_STDOUT,
67
93
  stderr=REVIEW_BINARY_REFUSAL_TEXT,
68
94
  attempts=(),
95
+ terminal_status=TERMINAL_STATUS_SERVED,
69
96
  )
70
97
 
71
98
 
@@ -141,3 +168,274 @@ def test_host_profile_failure_reports_its_reason_on_stderr(
141
168
  captured_streams = capsys.readouterr()
142
169
  assert outcome.is_stamp_minted is False
143
170
  assert HOST_PROFILE_FAILURE_TEXT in captured_streams.err
171
+
172
+
173
+ HEAD_SHA_ONE: str = "aaa111"
174
+ HEAD_SHA_TWO: str = "bbb222"
175
+ HEAD_SHA_THREE: str = "ccc333"
176
+ HEAD_SHA_FOUR: str = "ddd444"
177
+ FINDING_FILE_PATH: str = "pkg/module.py"
178
+ FINDING_LINE_NUMBER: int = 12
179
+ FINDING_SUMMARY_TEXT: str = "example finding"
180
+
181
+
182
+ def _retained_finding(
183
+ *,
184
+ severity: str,
185
+ verdict: str = VERDICT_CONFIRMED,
186
+ ) -> dict[str, object]:
187
+ return {
188
+ "file": FINDING_FILE_PATH,
189
+ "line": FINDING_LINE_NUMBER,
190
+ "summary": FINDING_SUMMARY_TEXT,
191
+ FINDING_FIELD_SEVERITY: severity,
192
+ FINDING_FIELD_VERDICT: verdict,
193
+ }
194
+
195
+
196
+ def test_severity_vocabulary_is_the_frozen_five_token_set() -> None:
197
+ assert ALL_FINDING_SEVERITIES == (
198
+ SEVERITY_BLOCKER,
199
+ SEVERITY_HIGH,
200
+ SEVERITY_MEDIUM,
201
+ SEVERITY_LOW,
202
+ SEVERITY_NIT,
203
+ )
204
+ assert ALL_FINDING_SEVERITIES == (
205
+ "blocker",
206
+ "high",
207
+ "medium",
208
+ "low",
209
+ "nit",
210
+ )
211
+
212
+
213
+ def test_loop_terminals_are_the_frozen_three_token_set() -> None:
214
+ assert ALL_LOOP_TERMINALS == (
215
+ TERMINAL_CLEAN,
216
+ TERMINAL_NITS_FIXED,
217
+ TERMINAL_ADVISOR_BLOCKED,
218
+ )
219
+
220
+
221
+ def test_retained_finding_requires_severity_and_verification_verdict() -> None:
222
+ complete_finding = _retained_finding(severity=SEVERITY_HIGH)
223
+ missing_severity = {
224
+ "file": FINDING_FILE_PATH,
225
+ "line": FINDING_LINE_NUMBER,
226
+ FINDING_FIELD_VERDICT: VERDICT_CONFIRMED,
227
+ }
228
+ missing_verdict = {
229
+ "file": FINDING_FILE_PATH,
230
+ "line": FINDING_LINE_NUMBER,
231
+ FINDING_FIELD_SEVERITY: SEVERITY_HIGH,
232
+ }
233
+ refuted_finding = _retained_finding(
234
+ severity=SEVERITY_LOW,
235
+ verdict=VERDICT_REFUTED,
236
+ )
237
+
238
+ assert all_findings_carry_severity_and_verdict([complete_finding]) is True
239
+ assert all_findings_carry_severity_and_verdict([missing_severity]) is False
240
+ assert all_findings_carry_severity_and_verdict([missing_verdict]) is False
241
+ assert all_findings_carry_severity_and_verdict([refuted_finding]) is False
242
+ assert VERDICT_CONFIRMED in ALL_RETAINED_VERIFICATION_VERDICTS
243
+ assert VERDICT_PLAUSIBLE in ALL_RETAINED_VERIFICATION_VERDICTS
244
+ assert VERDICT_REFUTED not in ALL_RETAINED_VERIFICATION_VERDICTS
245
+
246
+
247
+ def test_review_of_new_head_increments_reviewed_head_count_once() -> None:
248
+ after_first = record_reviewed_head((), HEAD_SHA_ONE)
249
+ after_same_head = record_reviewed_head(after_first, HEAD_SHA_ONE)
250
+ after_second = record_reviewed_head(after_same_head, HEAD_SHA_TWO)
251
+
252
+ assert after_first == (HEAD_SHA_ONE,)
253
+ assert after_same_head == (HEAD_SHA_ONE,)
254
+ assert after_second == (HEAD_SHA_ONE, HEAD_SHA_TWO)
255
+ assert len(after_second) == 2
256
+
257
+
258
+ def test_empty_findings_return_clean() -> None:
259
+ terminal_status = resolve_review_loop_terminal(
260
+ all_findings=(),
261
+ reviewed_head_count=1,
262
+ is_gates_passed=True,
263
+ is_nits_applied=False,
264
+ )
265
+
266
+ assert terminal_status == TERMINAL_CLEAN
267
+
268
+
269
+ def test_empty_findings_continue_when_gates_have_not_passed() -> None:
270
+ terminal_status = resolve_review_loop_terminal(
271
+ all_findings=(),
272
+ reviewed_head_count=1,
273
+ is_gates_passed=False,
274
+ is_nits_applied=False,
275
+ )
276
+
277
+ assert terminal_status is None
278
+
279
+
280
+ def test_nits_only_round_returns_nits_fixed_after_gates() -> None:
281
+ all_nits = (
282
+ _retained_finding(severity=SEVERITY_NIT),
283
+ _retained_finding(severity=SEVERITY_NIT, verdict=VERDICT_PLAUSIBLE),
284
+ )
285
+
286
+ assert is_nits_only_findings(all_nits) is True
287
+ terminal_status = resolve_review_loop_terminal(
288
+ all_findings=all_nits,
289
+ reviewed_head_count=1,
290
+ is_gates_passed=True,
291
+ is_nits_applied=True,
292
+ )
293
+
294
+ assert terminal_status == TERMINAL_NITS_FIXED
295
+
296
+
297
+ def test_nits_without_retained_verdict_do_not_return_nits_fixed() -> None:
298
+ severity_only_nit = {
299
+ "file": FINDING_FILE_PATH,
300
+ "line": FINDING_LINE_NUMBER,
301
+ "summary": FINDING_SUMMARY_TEXT,
302
+ FINDING_FIELD_SEVERITY: SEVERITY_NIT,
303
+ }
304
+
305
+ assert is_nits_only_findings((severity_only_nit,)) is True
306
+ assert all_findings_carry_severity_and_verdict((severity_only_nit,)) is False
307
+ terminal_status = resolve_review_loop_terminal(
308
+ all_findings=(severity_only_nit,),
309
+ reviewed_head_count=1,
310
+ is_gates_passed=True,
311
+ is_nits_applied=True,
312
+ )
313
+
314
+ assert terminal_status is None
315
+
316
+
317
+ def test_open_non_nit_findings_continue_at_any_head_count() -> None:
318
+ all_findings = (
319
+ _retained_finding(severity=SEVERITY_HIGH),
320
+ _retained_finding(severity=SEVERITY_NIT),
321
+ )
322
+ all_heads = record_reviewed_head((), HEAD_SHA_ONE)
323
+ all_heads = record_reviewed_head(all_heads, HEAD_SHA_TWO)
324
+ all_heads = record_reviewed_head(all_heads, HEAD_SHA_THREE)
325
+ all_heads = record_reviewed_head(all_heads, HEAD_SHA_FOUR)
326
+
327
+ assert len(all_heads) == 4
328
+ terminal_status = resolve_review_loop_terminal(
329
+ all_findings=all_findings,
330
+ reviewed_head_count=len(all_heads),
331
+ is_gates_passed=True,
332
+ is_nits_applied=False,
333
+ )
334
+
335
+ assert terminal_status is None
336
+
337
+
338
+ def test_unclassified_findings_continue_when_advisor_is_reachable() -> None:
339
+ unclassified_finding = {
340
+ "file": FINDING_FILE_PATH,
341
+ "line": FINDING_LINE_NUMBER,
342
+ "summary": FINDING_SUMMARY_TEXT,
343
+ FINDING_FIELD_VERDICT: VERDICT_CONFIRMED,
344
+ }
345
+
346
+ terminal_status = resolve_review_loop_terminal(
347
+ all_findings=(unclassified_finding,),
348
+ reviewed_head_count=5,
349
+ is_gates_passed=True,
350
+ is_nits_applied=False,
351
+ )
352
+
353
+ assert terminal_status is None
354
+
355
+
356
+ def test_nits_only_returns_nits_fixed_at_any_head_count() -> None:
357
+ all_nits = (_retained_finding(severity=SEVERITY_NIT),)
358
+
359
+ terminal_status = resolve_review_loop_terminal(
360
+ all_findings=all_nits,
361
+ reviewed_head_count=5,
362
+ is_gates_passed=True,
363
+ is_nits_applied=True,
364
+ )
365
+
366
+ assert terminal_status == TERMINAL_NITS_FIXED
367
+
368
+
369
+ def test_advisor_unreachable_with_unclassified_returns_advisor_blocked() -> None:
370
+ unclassified_finding = {
371
+ "file": FINDING_FILE_PATH,
372
+ "line": FINDING_LINE_NUMBER,
373
+ FINDING_FIELD_VERDICT: VERDICT_CONFIRMED,
374
+ }
375
+
376
+ terminal_status = resolve_review_loop_terminal(
377
+ all_findings=(unclassified_finding,),
378
+ reviewed_head_count=1,
379
+ is_gates_passed=False,
380
+ is_nits_applied=False,
381
+ is_advisor_unreachable=True,
382
+ )
383
+
384
+ assert terminal_status == TERMINAL_ADVISOR_BLOCKED
385
+
386
+
387
+ def test_terminal_serialization_preserves_draft_and_findings() -> None:
388
+ surviving_finding = _retained_finding(severity=SEVERITY_MEDIUM)
389
+ encoded_payload = encode_review_loop_terminal_result(
390
+ terminal=TERMINAL_ADVISOR_BLOCKED,
391
+ all_surviving_findings=(surviving_finding,),
392
+ reviewed_head_count=4,
393
+ is_draft_preserved=True,
394
+ )
395
+
396
+ assert encoded_payload[RESULT_KEY_TERMINAL] == TERMINAL_ADVISOR_BLOCKED
397
+ assert encoded_payload[RESULT_KEY_DRAFT_PRESERVED] is True
398
+ assert encoded_payload[RESULT_KEY_REVIEWED_HEAD_COUNT] == 4
399
+ assert encoded_payload[RESULT_KEY_SURVIVING_FINDINGS] == [surviving_finding]
400
+
401
+
402
+ def test_advisor_blocked_forces_draft_preserved_when_caller_passes_false() -> None:
403
+ surviving_finding = _retained_finding(severity=SEVERITY_HIGH)
404
+ advisor_blocked_payload = encode_review_loop_terminal_result(
405
+ terminal=TERMINAL_ADVISOR_BLOCKED,
406
+ all_surviving_findings=(surviving_finding,),
407
+ reviewed_head_count=1,
408
+ is_draft_preserved=False,
409
+ )
410
+ clean_payload = encode_review_loop_terminal_result(
411
+ terminal=TERMINAL_CLEAN,
412
+ all_surviving_findings=(),
413
+ reviewed_head_count=1,
414
+ is_draft_preserved=False,
415
+ )
416
+
417
+ assert advisor_blocked_payload[RESULT_KEY_DRAFT_PRESERVED] is True
418
+ assert clean_payload[RESULT_KEY_DRAFT_PRESERVED] is False
419
+
420
+
421
+ def test_fourth_and_later_heads_are_recorded() -> None:
422
+ all_heads = (HEAD_SHA_ONE, HEAD_SHA_TWO, HEAD_SHA_THREE)
423
+ after_fourth = record_reviewed_head(all_heads, HEAD_SHA_FOUR)
424
+
425
+ assert after_fourth == (
426
+ HEAD_SHA_ONE,
427
+ HEAD_SHA_TWO,
428
+ HEAD_SHA_THREE,
429
+ HEAD_SHA_FOUR,
430
+ )
431
+ assert len(after_fourth) == 4
432
+
433
+
434
+ def test_mixed_severities_are_not_nits_only() -> None:
435
+ all_findings = (
436
+ _retained_finding(severity=SEVERITY_NIT),
437
+ _retained_finding(severity=SEVERITY_BLOCKER),
438
+ )
439
+
440
+ assert is_nits_only_findings(all_findings) is False
441
+ assert is_nits_only_findings(()) is False
@@ -23,6 +23,10 @@ from claude_chain_runner import ( # noqa: E402
23
23
  ChainConfigurationError,
24
24
  ChainInvocationOutcome,
25
25
  )
26
+ from dev_env_scripts_constants.claude_chain_constants import ( # noqa: E402
27
+ TERMINAL_STATUS_CHAIN_EXHAUSTED,
28
+ TERMINAL_STATUS_SERVED,
29
+ )
26
30
  from dev_env_scripts_constants.grok_worker_constants import ( # noqa: E402
27
31
  AGENT_FLAG,
28
32
  ALL_AGENT_FILENAMES_BY_ROLE,
@@ -35,17 +39,20 @@ from dev_env_scripts_constants.grok_worker_constants import ( # noqa: E402
35
39
  CLI_TIMEOUT_FLAG,
36
40
  CLASSIFICATION_AUTH_FAILURE,
37
41
  CLASSIFICATION_OK,
42
+ CLASSIFICATION_TIMEOUT,
38
43
  CLASSIFICATION_USAGE_LIMIT,
39
44
  CWD_FLAG,
40
45
  DEFAULT_ROLE,
41
- DEFAULT_SPAWN_MAX_TURNS,
42
46
  DEFAULT_WORKER_TIMEOUT_SECONDS,
47
+ MAXIMUM_WORKER_TIMEOUT_SECONDS,
48
+ MIN_WORKER_TIMEOUT_SECONDS,
43
49
  OUTPUT_FORMAT_FLAG,
44
50
  OUTPUT_FORMAT_JSON,
45
51
  PROMPT_FILE_FLAG,
46
52
  REASON_CLAUDE_AGENT_REQUIRED,
47
53
  REASON_GROK_AUTH_FAILED,
48
54
  REASON_PROMPT_FILE_MISSING,
55
+ REASON_TIMEOUT_OUT_OF_BOUNDS,
49
56
  RESULT_KEY_ATTEMPTS,
50
57
  RESULT_KEY_OK,
51
58
  RESULT_KEY_OUTPUT,
@@ -59,11 +66,16 @@ from dev_env_scripts_constants.grok_worker_constants import ( # noqa: E402
59
66
  TIER_CLAUDE_HEADLESS,
60
67
  TIER_GROK,
61
68
  )
62
- from grok_headless_runner import GrokRunnerOutcome # noqa: E402
69
+ import grok_headless_runner # noqa: E402
70
+ from grok_headless_runner import ( # noqa: E402
71
+ GrokRunnerOutcome,
72
+ run_headless_worker,
73
+ )
63
74
  from grok_worker_preflight import PreflightOutcome # noqa: E402
64
75
 
65
76
  HOST_PROFILE_CLAUDE = "Claude"
66
77
  HOST_PROFILE_THIRD_PARTY = "ThirdParty"
78
+ NON_POSITIVE_TIMEOUT_SECONDS = 0
67
79
 
68
80
  FIXTURE_GROK_STDOUT = '{"tier":"grok","status":"done"}'
69
81
  FIXTURE_CLAUDE_STDOUT = '{"tier":"claude","status":"done"}'
@@ -72,6 +84,7 @@ FIXTURE_GROK_RETURNCODE = 0
72
84
  FIXTURE_CLAUDE_RETURNCODE = 0
73
85
  FIXTURE_FAILED_RETURNCODE = 1
74
86
  FIXTURE_ROLE = "code-quality-agent"
87
+ MIN_WORKER_TIMEOUT_SECONDS_CONSTANT_NAME = "MIN_WORKER_TIMEOUT_SECONDS"
75
88
  LARGE_PROMPT_CHARACTER_COUNT = 40000
76
89
  WINDOWS_SAFE_ARGV_ELEMENT_CEILING = 8192
77
90
  EXPECTED_PRIMARY_AGENT_FOR_DEFAULT_ROLE = Path(
@@ -124,6 +137,7 @@ def _claude_served(
124
137
  stdout=stdout,
125
138
  stderr="",
126
139
  attempts=(ChainAttempt(command="claude", status="served"),),
140
+ terminal_status=TERMINAL_STATUS_SERVED,
127
141
  )
128
142
 
129
143
 
@@ -134,6 +148,7 @@ def _claude_exhausted() -> ChainInvocationOutcome:
134
148
  stdout="",
135
149
  stderr="usage limit reached",
136
150
  attempts=(ChainAttempt(command="claude", status="usage_limited"),),
151
+ terminal_status=TERMINAL_STATUS_CHAIN_EXHAUSTED,
137
152
  )
138
153
 
139
154
 
@@ -261,7 +276,6 @@ def test_grok_ok_serves_tier_one(
261
276
  timeout_seconds=DEFAULT_WORKER_TIMEOUT_SECONDS,
262
277
  is_claude_tier_enabled=False,
263
278
  run_state_directory=run_state_directory,
264
- max_turns=DEFAULT_SPAWN_MAX_TURNS,
265
279
  )
266
280
 
267
281
  assert spawn_outcome.is_ok is True
@@ -297,7 +311,6 @@ def test_grok_usage_limited_on_claude_host_requires_agent(
297
311
  timeout_seconds=DEFAULT_WORKER_TIMEOUT_SECONDS,
298
312
  is_claude_tier_enabled=False,
299
313
  run_state_directory=run_state_directory,
300
- max_turns=DEFAULT_SPAWN_MAX_TURNS,
301
314
  )
302
315
 
303
316
  assert spawn_outcome.is_ok is False
@@ -313,6 +326,128 @@ def test_grok_usage_limited_on_claude_host_requires_agent(
313
326
  assert spawn_outcome.all_attempts[1].reason == REASON_CLAUDE_AGENT_REQUIRED
314
327
 
315
328
 
329
+ def test_out_of_bounds_timeout_is_reported_as_config_not_worker_failure(
330
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path, capsys: pytest.CaptureFixture[str]
331
+ ) -> None:
332
+ """A refused timeout prints a structured outcome and exits 3, never a traceback.
333
+
334
+ ::
335
+
336
+ --timeout-seconds 0 ok: reason timeout_out_of_bounds, exit 3
337
+ --timeout-seconds 5401 ok: reason timeout_out_of_bounds, exit 3
338
+ """
339
+ prompt_file, working_directory, run_state_directory = _paths(tmp_path)
340
+ _install_seams(monkeypatch, grok_outcome=_grok_ok())
341
+ monkeypatch.setattr(
342
+ dispatcher, "spawn_grok_runner", grok_headless_runner.run_headless_worker
343
+ )
344
+
345
+ for each_refused_timeout in (
346
+ NON_POSITIVE_TIMEOUT_SECONDS,
347
+ MAXIMUM_WORKER_TIMEOUT_SECONDS + 1,
348
+ ):
349
+ exit_code = dispatcher.main(
350
+ [
351
+ CLI_ROLE_FLAG,
352
+ FIXTURE_ROLE,
353
+ PROMPT_FILE_FLAG,
354
+ str(prompt_file),
355
+ CWD_FLAG,
356
+ str(working_directory),
357
+ CLI_TIMEOUT_FLAG,
358
+ str(each_refused_timeout),
359
+ CLI_RUN_STATE_DIR_FLAG,
360
+ str(run_state_directory),
361
+ ]
362
+ )
363
+ parsed_payload = json.loads(capsys.readouterr().out)
364
+ all_attempt_reasons = [
365
+ each_attempt[ATTEMPT_KEY_REASON]
366
+ for each_attempt in parsed_payload[RESULT_KEY_ATTEMPTS]
367
+ ]
368
+
369
+ assert exit_code == SPAWN_CONFIG_ERROR_EXIT_CODE, each_refused_timeout
370
+ assert parsed_payload[RESULT_KEY_OK] is False, each_refused_timeout
371
+ assert parsed_payload[RESULT_KEY_TIER_USED] is None, each_refused_timeout
372
+ assert REASON_TIMEOUT_OUT_OF_BOUNDS in all_attempt_reasons, each_refused_timeout
373
+
374
+
375
+ def test_out_of_bounds_timeout_is_refused_when_the_grok_tier_is_unreachable(
376
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path, capsys: pytest.CaptureFixture[str]
377
+ ) -> None:
378
+ """The bounds hold on a host whose preflight never reaches the grok runner.
379
+
380
+ ::
381
+
382
+ preflight unusable, --timeout-seconds 5401
383
+ ok: reason timeout_out_of_bounds, exit 3, no tier invoked
384
+ """
385
+ prompt_file, working_directory, run_state_directory = _paths(tmp_path)
386
+ call_log = _install_seams(
387
+ monkeypatch,
388
+ preflight_outcome=_fallthrough_preflight(REASON_GROK_AUTH_FAILED),
389
+ claude_outcome=_claude_served(),
390
+ )
391
+
392
+ exit_code = dispatcher.main(
393
+ [
394
+ CLI_ROLE_FLAG,
395
+ FIXTURE_ROLE,
396
+ PROMPT_FILE_FLAG,
397
+ str(prompt_file),
398
+ CWD_FLAG,
399
+ str(working_directory),
400
+ CLI_TIMEOUT_FLAG,
401
+ str(MAXIMUM_WORKER_TIMEOUT_SECONDS + 1),
402
+ CLI_RUN_STATE_DIR_FLAG,
403
+ str(run_state_directory),
404
+ ]
405
+ )
406
+ parsed_payload = json.loads(capsys.readouterr().out)
407
+ all_attempt_reasons = [
408
+ each_attempt[ATTEMPT_KEY_REASON]
409
+ for each_attempt in parsed_payload[RESULT_KEY_ATTEMPTS]
410
+ ]
411
+
412
+ assert exit_code == SPAWN_CONFIG_ERROR_EXIT_CODE
413
+ assert REASON_TIMEOUT_OUT_OF_BOUNDS in all_attempt_reasons
414
+ assert call_log.preflight_calls == 0
415
+ assert call_log.claude_calls == 0
416
+
417
+
418
+ def test_timed_out_grok_worker_is_recorded_as_timeout_not_served(
419
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
420
+ ) -> None:
421
+ """A killed worker reaches the dispatcher's trail as timeout, never as served.
422
+
423
+ ::
424
+
425
+ grok tier times out ok: attempt reason timeout, tier_used moves on
426
+ """
427
+ prompt_file, working_directory, run_state_directory = _paths(tmp_path)
428
+ call_log = _install_seams(
429
+ monkeypatch,
430
+ grok_outcome=_grok_failure(CLASSIFICATION_TIMEOUT),
431
+ claude_outcome=_claude_served(),
432
+ host_profile=HOST_PROFILE_THIRD_PARTY,
433
+ )
434
+
435
+ spawn_outcome = dispatcher.resolve_worker_spawn(
436
+ role=FIXTURE_ROLE,
437
+ prompt_file=prompt_file,
438
+ working_directory=working_directory,
439
+ timeout_seconds=DEFAULT_WORKER_TIMEOUT_SECONDS,
440
+ is_claude_tier_enabled=False,
441
+ run_state_directory=run_state_directory,
442
+ )
443
+
444
+ assert call_log.grok_calls == 1
445
+ assert spawn_outcome.all_attempts[0].tier == TIER_GROK
446
+ assert spawn_outcome.all_attempts[0].is_ok is False
447
+ assert spawn_outcome.all_attempts[0].reason == CLASSIFICATION_TIMEOUT
448
+ assert spawn_outcome.tier_used != TIER_GROK
449
+
450
+
316
451
  def test_grok_auth_failed_on_third_party_runs_tier_three(
317
452
  monkeypatch: pytest.MonkeyPatch, tmp_path: Path
318
453
  ) -> None:
@@ -331,7 +466,6 @@ def test_grok_auth_failed_on_third_party_runs_tier_three(
331
466
  timeout_seconds=DEFAULT_WORKER_TIMEOUT_SECONDS,
332
467
  is_claude_tier_enabled=False,
333
468
  run_state_directory=run_state_directory,
334
- max_turns=DEFAULT_SPAWN_MAX_TURNS,
335
469
  )
336
470
 
337
471
  assert spawn_outcome.is_ok is True
@@ -423,6 +557,40 @@ def test_config_error_returns_exit_three(
423
557
  assert parsed_payload[RESULT_KEY_RETURNCODE] == SPAWN_CONFIG_ERROR_EXIT_CODE
424
558
 
425
559
 
560
+ def test_below_floor_timeout_returns_json_config_exit(
561
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path, capsys: pytest.CaptureFixture[str]
562
+ ) -> None:
563
+ """A below-floor timeout reads as a config error, not an escaping traceback."""
564
+ prompt_file, working_directory, run_state_directory = _paths(tmp_path)
565
+ _install_seams(monkeypatch, grok_outcome=_grok_ok())
566
+ monkeypatch.setattr(dispatcher, "spawn_grok_runner", run_headless_worker)
567
+ below_floor_timeout_seconds = MIN_WORKER_TIMEOUT_SECONDS - 1
568
+
569
+ exit_code = dispatcher.main(
570
+ [
571
+ CLI_ROLE_FLAG,
572
+ FIXTURE_ROLE,
573
+ PROMPT_FILE_FLAG,
574
+ str(prompt_file),
575
+ CWD_FLAG,
576
+ str(working_directory),
577
+ CLI_TIMEOUT_FLAG,
578
+ str(below_floor_timeout_seconds),
579
+ CLI_RUN_STATE_DIR_FLAG,
580
+ str(run_state_directory),
581
+ ]
582
+ )
583
+
584
+ assert exit_code == SPAWN_CONFIG_ERROR_EXIT_CODE
585
+ captured = capsys.readouterr()
586
+ assert captured.err == ""
587
+ parsed_payload = json.loads(captured.out)
588
+ assert parsed_payload[RESULT_KEY_OK] is False
589
+ assert parsed_payload[RESULT_KEY_TIER_USED] is None
590
+ assert parsed_payload[RESULT_KEY_RETURNCODE] == SPAWN_CONFIG_ERROR_EXIT_CODE
591
+ assert MIN_WORKER_TIMEOUT_SECONDS_CONSTANT_NAME in parsed_payload[RESULT_KEY_OUTPUT]
592
+
593
+
426
594
  def test_attempts_array_ordering_across_tiers(
427
595
  monkeypatch: pytest.MonkeyPatch, tmp_path: Path
428
596
  ) -> None:
@@ -441,7 +609,6 @@ def test_attempts_array_ordering_across_tiers(
441
609
  timeout_seconds=DEFAULT_WORKER_TIMEOUT_SECONDS,
442
610
  is_claude_tier_enabled=True,
443
611
  run_state_directory=run_state_directory,
444
- max_turns=DEFAULT_SPAWN_MAX_TURNS,
445
612
  )
446
613
 
447
614
  all_tiers = [each_attempt.tier for each_attempt in spawn_outcome.all_attempts]
@@ -515,7 +682,6 @@ def test_preflight_fallthrough_skips_grok_runner(
515
682
  timeout_seconds=DEFAULT_WORKER_TIMEOUT_SECONDS,
516
683
  is_claude_tier_enabled=False,
517
684
  run_state_directory=run_state_directory,
518
- max_turns=DEFAULT_SPAWN_MAX_TURNS,
519
685
  )
520
686
 
521
687
  assert call_log.grok_calls == 0
@@ -533,9 +699,15 @@ def test_detect_host_profile_is_consumed_not_reimplemented() -> None:
533
699
  assert "THIRD_PARTY" not in source_text
534
700
 
535
701
 
536
- def test_default_max_turns_reaches_grok_kwargs(
702
+ def test_dispatcher_grok_invocation_carries_no_turn_cap(
537
703
  monkeypatch: pytest.MonkeyPatch, tmp_path: Path, capsys: pytest.CaptureFixture[str]
538
704
  ) -> None:
705
+ """The dispatcher hands the runner no turn cap; the timeout is the only bound.
706
+
707
+ ::
708
+
709
+ grok tier kwargs ok: timeout_seconds present, max_turns absent
710
+ """
539
711
  prompt_file, working_directory, run_state_directory = _paths(tmp_path)
540
712
  call_log = _install_seams(
541
713
  monkeypatch,
@@ -559,7 +731,10 @@ def test_default_max_turns_reaches_grok_kwargs(
559
731
 
560
732
  assert exit_code == SPAWN_SERVED_EXIT_CODE
561
733
  assert call_log.grok_keyword_arguments is not None
562
- assert call_log.grok_keyword_arguments["max_turns"] == DEFAULT_SPAWN_MAX_TURNS
734
+ assert "max_turns" not in call_log.grok_keyword_arguments
735
+ assert call_log.grok_keyword_arguments["timeout_seconds"] == (
736
+ DEFAULT_WORKER_TIMEOUT_SECONDS
737
+ )
563
738
  parsed_payload = json.loads(capsys.readouterr().out)
564
739
  assert parsed_payload[RESULT_KEY_OK] is True
565
740
 
@@ -645,7 +820,6 @@ def test_default_role_maps_to_primary_agent_stem(
645
820
  timeout_seconds=DEFAULT_WORKER_TIMEOUT_SECONDS,
646
821
  is_claude_tier_enabled=False,
647
822
  run_state_directory=run_state_directory,
648
- max_turns=DEFAULT_SPAWN_MAX_TURNS,
649
823
  )
650
824
 
651
825
  assert spawn_outcome.is_ok is True
@@ -674,7 +848,6 @@ def test_tier_three_argv_includes_agent_for_default_role(
674
848
  timeout_seconds=DEFAULT_WORKER_TIMEOUT_SECONDS,
675
849
  is_claude_tier_enabled=False,
676
850
  run_state_directory=run_state_directory,
677
- max_turns=DEFAULT_SPAWN_MAX_TURNS,
678
851
  )
679
852
 
680
853
  assert spawn_outcome.tier_used == TIER_CLAUDE_HEADLESS
@@ -708,7 +881,6 @@ def test_large_prompt_stays_out_of_claude_argv(
708
881
  timeout_seconds=DEFAULT_WORKER_TIMEOUT_SECONDS,
709
882
  is_claude_tier_enabled=False,
710
883
  run_state_directory=run_state_directory,
711
- max_turns=DEFAULT_SPAWN_MAX_TURNS,
712
884
  )
713
885
 
714
886
  assert spawn_outcome.tier_used == TIER_CLAUDE_HEADLESS
@@ -926,8 +1098,7 @@ def test_headless_chain_runner_lock_serializes_distinct_cwds(
926
1098
  timeout_seconds=DEFAULT_WORKER_TIMEOUT_SECONDS,
927
1099
  is_claude_tier_enabled=False,
928
1100
  run_state_directory=run_state_directory,
929
- max_turns=DEFAULT_SPAWN_MAX_TURNS,
930
- )
1101
+ )
931
1102
  except (OSError, RuntimeError, ValueError, AssertionError) as raised_error:
932
1103
  all_errors.append(raised_error)
933
1104
 
@@ -1005,7 +1176,6 @@ def test_usage_limit_fallover_delivers_full_prompt_to_each_binary(
1005
1176
  timeout_seconds=DEFAULT_WORKER_TIMEOUT_SECONDS,
1006
1177
  is_claude_tier_enabled=False,
1007
1178
  run_state_directory=run_state_directory,
1008
- max_turns=DEFAULT_SPAWN_MAX_TURNS,
1009
1179
  )
1010
1180
 
1011
1181
  assert prompt_text_by_command["claude"] == FIXTURE_PROMPT_TEXT