claude-dev-env 2.7.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.
@@ -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
- from grok_headless_runner import GrokRunnerOutcome # noqa: E402
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 test_default_max_turns_reaches_grok_kwargs(
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 call_log.grok_keyword_arguments["max_turns"] == DEFAULT_SPAWN_MAX_TURNS
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
- max_turns=DEFAULT_SPAWN_MAX_TURNS,
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
@@ -22,15 +22,17 @@ from dev_env_scripts_constants.grok_worker_constants import ( # noqa: E402
22
22
  BUILD_PROFILE_PROMPT_HEADER,
23
23
  CLASSIFICATION_ERROR,
24
24
  CLASSIFICATION_OK,
25
+ CLASSIFICATION_TIMEOUT,
25
26
  CLASSIFICATION_USAGE_LIMIT,
26
27
  DEBUG_FILE_FLAG,
27
28
  DEFAULT_ROLE,
28
- DEFAULT_WORKER_MAX_TURNS,
29
29
  DEFAULT_WORKER_TIMEOUT_SECONDS,
30
30
  DISABLE_WEB_SEARCH_FLAG,
31
31
  DISALLOWED_TOOLS_FLAG,
32
32
  LEADER_SOCKET_FILENAME_PREFIX,
33
33
  LEADER_SOCKET_FILENAME_SUFFIX,
34
+ MAX_TURNS_FLAG,
35
+ MAXIMUM_WORKER_TIMEOUT_SECONDS,
34
36
  OUTPUT_FILENAME_PREFIX,
35
37
  PROMPT_FILENAME_PREFIX,
36
38
  READONLY_DISALLOWED_TOOLS_VALUE,
@@ -53,7 +55,6 @@ from dev_env_scripts_constants.grok_worker_constants import ( # noqa: E402
53
55
  TOOL_PROFILE_READONLY,
54
56
  UTF8_ENCODING,
55
57
  WORKER_SPEC_AGENT_NAME_KEY,
56
- WORKER_SPEC_MAX_TURNS_KEY,
57
58
  WORKER_SPEC_PROMPT_PARTS_KEY,
58
59
  WORKER_SPEC_TIMEOUT_KEY,
59
60
  )
@@ -61,8 +62,10 @@ from dev_env_scripts_constants.timing import WORKER_STAGGER_SECONDS # noqa: E40
61
62
  from grok_headless_runner import GrokRunnerOutcome # noqa: E402
62
63
  from grok_worker_preflight import PreflightOutcome # noqa: E402
63
64
 
65
+ RETIRED_MAX_TURNS_KEYWORD = "max_turns"
64
66
  FIXTURE_REPORT_TEXT = '{"status":"done","role":"investigator"}'
65
67
  FIXTURE_USAGE_LIMIT_TEXT = "rate limit exceeded (HTTP 429): quota exceeded"
68
+ FIXTURE_TIMEOUT_KILL_TEXT = "worker exceeded its timeout and was killed"
66
69
 
67
70
 
68
71
  def _write_prompt_parts(
@@ -871,7 +874,7 @@ def test_load_batch_spec_missing_worker_keys_raise_value_error(
871
874
  ).lower() or "must be" in str(raised_error.value).lower()
872
875
 
873
876
 
874
- def test_load_batch_spec_rejects_non_positive_timeout_and_max_turns(
877
+ def test_load_batch_spec_rejects_non_positive_timeout(
875
878
  tmp_path: Path,
876
879
  ) -> None:
877
880
  header_part, body_part = _write_prompt_parts(tmp_path)
@@ -891,27 +894,11 @@ def test_load_batch_spec_rejects_non_positive_timeout_and_max_turns(
891
894
  zero_timeout_dir,
892
895
  all_worker_payloads=[zero_timeout_payload],
893
896
  )
894
- with pytest.raises(ValueError, match=WORKER_SPEC_TIMEOUT_KEY):
897
+ with pytest.raises(ValueError, match="MIN_WORKER_TIMEOUT_SECONDS"):
895
898
  batch.load_batch_spec(zero_timeout_path)
896
899
 
897
- negative_turns_dir = tmp_path / "negative-turns"
898
- negative_turns_dir.mkdir()
899
- negative_turns_payload = _worker_payload(
900
- role_name="negative-turns",
901
- all_prompt_parts=[str(header_part), str(body_part)],
902
- working_directory=working_directory,
903
- tool_profile=TOOL_PROFILE_BUILD,
904
- )
905
- negative_turns_payload[WORKER_SPEC_MAX_TURNS_KEY] = -1
906
- negative_turns_path = _write_batch_spec(
907
- negative_turns_dir,
908
- all_worker_payloads=[negative_turns_payload],
909
- )
910
- with pytest.raises(ValueError, match=WORKER_SPEC_MAX_TURNS_KEY):
911
- batch.load_batch_spec(negative_turns_path)
912
-
913
900
 
914
- def test_load_batch_spec_accepts_default_timeout_and_max_turns(
901
+ def test_load_batch_spec_accepts_the_default_timeout(
915
902
  tmp_path: Path,
916
903
  ) -> None:
917
904
  header_part, body_part = _write_prompt_parts(tmp_path)
@@ -931,7 +918,223 @@ def test_load_batch_spec_accepts_default_timeout_and_max_turns(
931
918
 
932
919
  assert len(batch_spec.all_workers) == 1
933
920
  assert batch_spec.all_workers[0].timeout_seconds == DEFAULT_WORKER_TIMEOUT_SECONDS
934
- assert batch_spec.all_workers[0].max_turns == DEFAULT_WORKER_MAX_TURNS
921
+
922
+
923
+ def test_timeout_over_the_ceiling_is_refused_and_at_the_ceiling_passes(
924
+ tmp_path: Path,
925
+ ) -> None:
926
+ """The launcher refuses a spec past the 90-minute ceiling; it never clamps.
927
+
928
+ ::
929
+
930
+ timeout_seconds 5401 flag: ValueError naming MAXIMUM_WORKER_TIMEOUT_SECONDS
931
+ timeout_seconds 5400 ok: loads, value untouched
932
+ timeout_seconds 30 ok: loads, value untouched
933
+ """
934
+ header_part, body_part = _write_prompt_parts(tmp_path)
935
+ working_directory = tmp_path / "project"
936
+ working_directory.mkdir()
937
+
938
+ over_ceiling_directory = tmp_path / "over-ceiling"
939
+ over_ceiling_directory.mkdir()
940
+ over_ceiling_path = _write_batch_spec(
941
+ over_ceiling_directory,
942
+ all_worker_payloads=[
943
+ _worker_payload(
944
+ role_name="over-ceiling",
945
+ all_prompt_parts=[str(header_part), str(body_part)],
946
+ working_directory=working_directory,
947
+ tool_profile=TOOL_PROFILE_BUILD,
948
+ timeout_seconds=MAXIMUM_WORKER_TIMEOUT_SECONDS + 1,
949
+ )
950
+ ],
951
+ )
952
+ with pytest.raises(ValueError, match="MAXIMUM_WORKER_TIMEOUT_SECONDS"):
953
+ batch.load_batch_spec(over_ceiling_path)
954
+
955
+ at_ceiling_directory = tmp_path / "at-ceiling"
956
+ at_ceiling_directory.mkdir()
957
+ at_ceiling_path = _write_batch_spec(
958
+ at_ceiling_directory,
959
+ all_worker_payloads=[
960
+ _worker_payload(
961
+ role_name="at-ceiling",
962
+ all_prompt_parts=[str(header_part), str(body_part)],
963
+ working_directory=working_directory,
964
+ tool_profile=TOOL_PROFILE_BUILD,
965
+ timeout_seconds=MAXIMUM_WORKER_TIMEOUT_SECONDS,
966
+ ),
967
+ _worker_payload(
968
+ role_name="well-under-ceiling",
969
+ all_prompt_parts=[str(header_part), str(body_part)],
970
+ working_directory=working_directory,
971
+ tool_profile=TOOL_PROFILE_BUILD,
972
+ timeout_seconds=30,
973
+ ),
974
+ ],
975
+ )
976
+ at_ceiling_spec = batch.load_batch_spec(at_ceiling_path)
977
+
978
+ assert at_ceiling_spec.all_workers[0].timeout_seconds == (
979
+ MAXIMUM_WORKER_TIMEOUT_SECONDS
980
+ )
981
+ assert at_ceiling_spec.all_workers[1].timeout_seconds == 30
982
+
983
+
984
+ def test_ceiling_timeout_reaches_the_runner_untouched(
985
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
986
+ ) -> None:
987
+ header_part, body_part = _write_prompt_parts(tmp_path, role_marker="long-worker")
988
+ working_directory = tmp_path / "project"
989
+ working_directory.mkdir()
990
+ run_state_directory = tmp_path / "run-state"
991
+ batch_spec = batch.load_batch_spec(
992
+ _write_batch_spec(
993
+ tmp_path,
994
+ all_worker_payloads=[
995
+ _worker_payload(
996
+ role_name="long-worker",
997
+ all_prompt_parts=[str(header_part), str(body_part)],
998
+ working_directory=working_directory,
999
+ tool_profile=TOOL_PROFILE_READONLY,
1000
+ timeout_seconds=MAXIMUM_WORKER_TIMEOUT_SECONDS,
1001
+ )
1002
+ ],
1003
+ )
1004
+ )
1005
+ recorder = _RunnerRecorder({"long-worker": _ok_outcome()})
1006
+ monkeypatch.setattr(
1007
+ batch, "batch_preflight", lambda **_kwargs: PreflightOutcome(True, None)
1008
+ )
1009
+ monkeypatch.setattr(batch, "batch_headless_runner", recorder)
1010
+ monkeypatch.setattr(batch, "batch_sleep", lambda _seconds: None)
1011
+
1012
+ batch.run_grok_batch(
1013
+ batch_spec=batch_spec,
1014
+ run_state_directory=run_state_directory,
1015
+ )
1016
+
1017
+ assert recorder.all_keyword_arguments[0]["timeout_seconds"] == (
1018
+ MAXIMUM_WORKER_TIMEOUT_SECONDS
1019
+ )
1020
+
1021
+
1022
+ def test_worker_invocations_carry_no_turn_cap(
1023
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
1024
+ ) -> None:
1025
+ header_part, body_part = _write_prompt_parts(tmp_path, role_marker="uncapped")
1026
+ working_directory = tmp_path / "project"
1027
+ working_directory.mkdir()
1028
+ run_state_directory = tmp_path / "run-state"
1029
+ batch_spec = batch.load_batch_spec(
1030
+ _write_batch_spec(
1031
+ tmp_path,
1032
+ all_worker_payloads=[
1033
+ _worker_payload(
1034
+ role_name="uncapped",
1035
+ all_prompt_parts=[str(header_part), str(body_part)],
1036
+ working_directory=working_directory,
1037
+ tool_profile=TOOL_PROFILE_READONLY,
1038
+ )
1039
+ ],
1040
+ )
1041
+ )
1042
+ recorder = _RunnerRecorder({"uncapped": _ok_outcome()})
1043
+ monkeypatch.setattr(
1044
+ batch, "batch_preflight", lambda **_kwargs: PreflightOutcome(True, None)
1045
+ )
1046
+ monkeypatch.setattr(batch, "batch_headless_runner", recorder)
1047
+ monkeypatch.setattr(batch, "batch_sleep", lambda _seconds: None)
1048
+
1049
+ batch.run_grok_batch(
1050
+ batch_spec=batch_spec,
1051
+ run_state_directory=run_state_directory,
1052
+ )
1053
+
1054
+ launched_keyword_arguments = recorder.all_keyword_arguments[0]
1055
+ all_extra_arguments = launched_keyword_arguments["all_extra_arguments"]
1056
+ assert RETIRED_MAX_TURNS_KEYWORD not in launched_keyword_arguments
1057
+ assert isinstance(all_extra_arguments, tuple)
1058
+ assert MAX_TURNS_FLAG not in all_extra_arguments
1059
+
1060
+
1061
+ def test_timed_out_worker_reads_as_timeout_beside_a_completed_worker(
1062
+ monkeypatch: pytest.MonkeyPatch,
1063
+ tmp_path: Path,
1064
+ capsys: pytest.CaptureFixture[str],
1065
+ ) -> None:
1066
+ """A killed worker stays distinguishable from a completed one in the summary.
1067
+
1068
+ ::
1069
+
1070
+ killed worker ok: classification timeout, is_ok False, exit code 1
1071
+ completed worker ok: classification ok, is_ok True
1072
+ """
1073
+ header_done, body_done = _write_prompt_parts(tmp_path, role_marker="done-worker")
1074
+ header_killed, body_killed = _write_prompt_parts(
1075
+ tmp_path, role_marker="killed-worker"
1076
+ )
1077
+ working_directory = tmp_path / "project"
1078
+ working_directory.mkdir()
1079
+ run_state_directory = tmp_path / "run-state"
1080
+ specification_path = _write_batch_spec(
1081
+ tmp_path,
1082
+ all_worker_payloads=[
1083
+ _worker_payload(
1084
+ role_name="done-worker",
1085
+ all_prompt_parts=[str(header_done), str(body_done)],
1086
+ working_directory=working_directory,
1087
+ tool_profile=TOOL_PROFILE_BUILD,
1088
+ ),
1089
+ _worker_payload(
1090
+ role_name="killed-worker",
1091
+ all_prompt_parts=[str(header_killed), str(body_killed)],
1092
+ working_directory=working_directory,
1093
+ tool_profile=TOOL_PROFILE_BUILD,
1094
+ ),
1095
+ ],
1096
+ )
1097
+ recorder = _RunnerRecorder(
1098
+ {
1099
+ "done-worker": _ok_outcome(),
1100
+ "killed-worker": GrokRunnerOutcome(
1101
+ is_ok=False,
1102
+ returncode=-9,
1103
+ classification=CLASSIFICATION_TIMEOUT,
1104
+ stdout="",
1105
+ stderr=FIXTURE_TIMEOUT_KILL_TEXT,
1106
+ ),
1107
+ }
1108
+ )
1109
+ monkeypatch.setattr(
1110
+ batch, "batch_preflight", lambda **_kwargs: PreflightOutcome(True, None)
1111
+ )
1112
+ monkeypatch.setattr(batch, "batch_headless_runner", recorder)
1113
+ monkeypatch.setattr(batch, "batch_sleep", lambda _seconds: None)
1114
+
1115
+ exit_code = batch.main(
1116
+ [
1117
+ "--spec",
1118
+ str(specification_path),
1119
+ "--run-temp-dir",
1120
+ str(run_state_directory),
1121
+ ]
1122
+ )
1123
+
1124
+ summary_payload = json.loads(capsys.readouterr().out)
1125
+ payload_by_role_name = {
1126
+ each_payload[SUMMARY_ROLE_NAME_KEY]: each_payload
1127
+ for each_payload in summary_payload[SUMMARY_WORKERS_KEY]
1128
+ }
1129
+ killed_payload = payload_by_role_name["killed-worker"]
1130
+ done_payload = payload_by_role_name["done-worker"]
1131
+
1132
+ assert exit_code == 1
1133
+ assert killed_payload[SUMMARY_CLASSIFICATION_KEY] == CLASSIFICATION_TIMEOUT
1134
+ assert killed_payload[SUMMARY_IS_OK_KEY] is False
1135
+ assert killed_payload[SUMMARY_CLASSIFICATION_KEY] != CLASSIFICATION_OK
1136
+ assert done_payload[SUMMARY_CLASSIFICATION_KEY] == CLASSIFICATION_OK
1137
+ assert done_payload[SUMMARY_IS_OK_KEY] is True
935
1138
 
936
1139
 
937
1140
  def test_unwritable_report_file_keeps_the_worker_outcome(
@@ -434,7 +434,7 @@ test('the verdict-fence binding does not self-resolve a cwd via git rev-parse fo
434
434
  );
435
435
  });
436
436
 
437
- test('every verify step calls buildVerdictFenceSteps, uses code-verifier, and forbids edits', () => {
437
+ test('every verify step calls buildVerdictFenceSteps, uses code-verifier, and forbids editing the tree under verification', () => {
438
438
  for (const verifyFunctionName of ['runVerifierTask']) {
439
439
  const verifyBody = lensPromptBody(verifyFunctionName);
440
440
  assert.match(
@@ -454,8 +454,8 @@ test('every verify step calls buildVerdictFenceSteps, uses code-verifier, and fo
454
454
  );
455
455
  assert.match(
456
456
  verifyBody,
457
- /do no edits|make no edits|not edit|no file edits/i,
458
- `expected ${verifyFunctionName} to be told to make no edits`,
457
+ /(?:no|not|never)[^.\n]{0,30}edit[^.\n]{0,40}tree[^.\n]{0,30}(?:under|being)\s+verif/i,
458
+ `expected ${verifyFunctionName} to forbid editing the tree under verification (a deliberate break off that tree stays allowed)`,
459
459
  );
460
460
  }
461
461
  });
@@ -467,7 +467,7 @@ test('runFixerTask never verifies — verification belongs to the separate verif
467
467
  assert.match(fixerBody, /agentType:\s*'clean-coder'/, 'expected the fixer to use clean-coder for its commit and recovery edits');
468
468
  });
469
469
 
470
- test('runVerifierTask uses --manifest-hash-for-branch with the hardening branch and forbids edits', () => {
470
+ test('runVerifierTask uses --manifest-hash-for-branch with the hardening branch and forbids editing the tree under verification', () => {
471
471
  const verifyBody = lensPromptBody('runVerifierTask');
472
472
  assert.match(
473
473
  verifyBody,
@@ -476,8 +476,8 @@ test('runVerifierTask uses --manifest-hash-for-branch with the hardening branch
476
476
  );
477
477
  assert.match(
478
478
  verifyBody,
479
- /do no edits|make no edits|not edit|no file edits/i,
480
- 'expected the verifier to be told to make no edits',
479
+ /(?:no|not|never)[^.\n]{0,30}edit[^.\n]{0,40}tree[^.\n]{0,30}(?:under|being)\s+verif/i,
480
+ 'expected the verifier to forbid editing the tree under verification (a deliberate break off that tree stays allowed)',
481
481
  );
482
482
  });
483
483
 
@@ -901,6 +901,28 @@ test('convergeReadOnlyAgent prepends HEADLESS_READONLY_PREAMBLE and the worktree
901
901
  );
902
902
  });
903
903
 
904
+ test('the read-only destructive pointer scopes its no-edit clause to the tree it reads', () => {
905
+ const destructivePointer = convergeSource
906
+ .split('\n')
907
+ .find((eachLine) => eachLine.includes('Never run a destructive command'));
908
+ assert.ok(destructivePointer, 'expected the read-only destructive pointer to be declared');
909
+ assert.match(
910
+ destructivePointer,
911
+ /Never run a destructive command/,
912
+ 'expected the destructive-command prohibition to stay absolute',
913
+ );
914
+ assert.match(
915
+ destructivePointer,
916
+ /no edit to the tree it reads/,
917
+ 'expected the no-edit clause to be scoped to the tree under verification',
918
+ );
919
+ assert.doesNotMatch(
920
+ destructivePointer,
921
+ /edits nothing/,
922
+ 'expected no blanket edits-nothing wording, which forbids the deliberate break off that tree',
923
+ );
924
+ });
925
+
904
926
  const taskDispatchers = [
905
927
  { name: 'runGitTask', isAsync: false },
906
928
  { name: 'runFixerTask', isAsync: false },