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.
- package/_shared/CLAUDE.md +1 -0
- package/_shared/advisor/advisor-protocol.md +19 -9
- package/_shared/pr-loop/audit-contract.md +4 -4
- package/_shared/pr-loop/precatch-rubric.md +2 -2
- package/_shared/pr-loop/worker-spawn.md +3 -1
- package/_shared/process-tree/CLAUDE.md +41 -0
- package/_shared/process-tree/scripts/config/process_tree_scripts_constants/__init__.py +1 -0
- package/_shared/process-tree/scripts/config/process_tree_scripts_constants/process_tree_kill_constants.py +27 -0
- package/_shared/process-tree/scripts/process_tree_kill.py +141 -0
- package/_shared/process-tree/scripts/pyproject.toml +16 -0
- package/_shared/process-tree/scripts/test_process_tree_kill.py +278 -0
- package/agents/code-quality-agent.md +6 -5
- package/agents/deep-research.md +7 -24
- package/agents/docs-agent.md +1 -27
- package/agents/issue-tracker.md +1 -7
- package/agents/skill-writer-agent.md +1 -2
- package/agents/test_agent_frontmatter.py +309 -12
- package/hooks/blocking/CLAUDE.md +2 -0
- package/hooks/blocking/fable_spawn_gate.py +187 -0
- package/hooks/blocking/piped_pytest_blocker.py +1223 -0
- package/hooks/blocking/plain_language_blocker.py +287 -15
- package/hooks/blocking/test_fable_spawn_gate.py +374 -0
- package/hooks/blocking/test_piped_pytest_blocker.py +587 -0
- package/hooks/blocking/test_plain_language_blocker.py +277 -2
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +34 -2
- package/hooks/git-hooks/CLAUDE.md +2 -2
- package/hooks/git-hooks/git_hooks_constants/__init__.py +28 -0
- package/hooks/git-hooks/pre_push.py +343 -54
- package/hooks/git-hooks/test_pre_push.py +852 -6
- package/hooks/hooks.json +9 -19
- package/hooks/hooks_constants/CLAUDE.md +2 -0
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +1 -0
- package/hooks/hooks_constants/fable_spawn_gate_constants.py +62 -0
- package/hooks/hooks_constants/piped_pytest_blocker_constants.py +360 -0
- package/hooks/hooks_constants/plain_language_blocker_constants.py +64 -1
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +10 -0
- package/hooks/hooks_constants/shell_command_segments.py +1 -1
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +1 -0
- package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +27 -0
- package/hooks/hooks_constants/test_prose_metrics_parity.py +124 -0
- package/package.json +1 -1
- package/rules/CLAUDE.md +1 -0
- package/rules/ask-user-question-required.md +26 -0
- package/rules/claims-as-quotes.md +65 -0
- package/scripts/CLAUDE.md +4 -4
- package/scripts/_code_review_test_support.py +6 -0
- package/scripts/check.ps1 +18 -5
- package/scripts/claude_chain_runner.py +203 -31
- package/scripts/codec_forwarding_test_support.py +2 -0
- package/scripts/dev_env_scripts_constants/CLAUDE.md +4 -4
- package/scripts/dev_env_scripts_constants/claude_chain_constants.py +38 -0
- package/scripts/dev_env_scripts_constants/code_review_constants.py +403 -2
- package/scripts/dev_env_scripts_constants/grok_worker_constants.py +83 -13
- package/scripts/grok_headless_runner.py +148 -18
- package/scripts/resolve_worker_spawn.py +56 -10
- package/scripts/spawn_grok_batch.py +81 -23
- package/scripts/test_claude_chain_runner.py +358 -0
- package/scripts/test_grok_headless_runner.py +547 -10
- package/scripts/test_invoke_code_review.py +298 -0
- package/scripts/test_resolve_worker_spawn.py +185 -15
- package/scripts/test_spawn_grok_batch.py +326 -22
- package/scripts/tests/CLAUDE.md +1 -0
- package/scripts/tests/test_grok_worker_constants.py +59 -0
- package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +46 -0
- package/skills/autoconverge/workflow/converge.contract.test.mjs +133 -8
- package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +107 -1
- package/skills/autoconverge/workflow/converge.mjs +113 -31
- package/skills/codex-review/scripts/codex_review_scripts_constants/codex_usage_probe_constants.py +0 -4
- package/skills/codex-review/scripts/codex_usage_probe.py +20 -33
- package/skills/codex-review/scripts/run_codex_review.py +16 -64
- package/skills/codex-review/scripts/test_codex_usage_probe.py +46 -41
- package/skills/codex-review/scripts/test_run_codex_review.py +1 -33
- package/skills/e-code-review/SKILL.md +9 -8
- package/skills/e-code-review/reference/fix.md +29 -7
- package/skills/e-code-review/reference/loop.md +230 -14
- package/skills/e-code-review/reference/low.md +33 -15
- package/skills/e-code-review/reference/medium.md +55 -21
- package/skills/e-code-review/reference/xhigh.md +30 -12
- package/skills/fresh-branch/CLAUDE.md +5 -5
- package/skills/fresh-branch/SKILL.md +14 -6
- package/skills/fresh-branch/scripts/create_fresh_branch.py +122 -39
- package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +10 -3
- package/skills/fresh-branch/scripts/test_create_fresh_branch.py +251 -0
- package/skills/grok-spawn/SKILL.md +10 -3
- package/skills/grok-spawn/reference/flag-profiles.md +3 -1
- package/skills/orchestrator/SKILL.md +4 -1
- package/skills/orchestrator-refresh/SKILL.md +5 -1
- package/skills/team-advisor/SKILL.md +4 -1
|
@@ -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,108 @@ 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
|
|
877
|
+
def test_load_batch_spec_rejects_an_unknown_worker_key(
|
|
878
|
+
tmp_path: Path,
|
|
879
|
+
) -> None:
|
|
880
|
+
header_part, body_part = _write_prompt_parts(tmp_path)
|
|
881
|
+
working_directory = tmp_path / "project"
|
|
882
|
+
working_directory.mkdir()
|
|
883
|
+
worker_payload = _worker_payload(
|
|
884
|
+
role_name="stray-key-worker",
|
|
885
|
+
all_prompt_parts=[str(header_part), str(body_part)],
|
|
886
|
+
working_directory=working_directory,
|
|
887
|
+
tool_profile=TOOL_PROFILE_BUILD,
|
|
888
|
+
)
|
|
889
|
+
worker_payload["timeout_second"] = 30
|
|
890
|
+
specification_path = _write_batch_spec(
|
|
891
|
+
tmp_path, all_worker_payloads=[worker_payload]
|
|
892
|
+
)
|
|
893
|
+
|
|
894
|
+
with pytest.raises(ValueError) as raised_error:
|
|
895
|
+
batch.load_batch_spec(specification_path)
|
|
896
|
+
|
|
897
|
+
error_text = str(raised_error.value)
|
|
898
|
+
assert "timeout_second" in error_text
|
|
899
|
+
assert WORKER_SPEC_TIMEOUT_KEY in error_text
|
|
900
|
+
|
|
901
|
+
|
|
902
|
+
def test_load_batch_spec_names_every_unknown_worker_key(
|
|
903
|
+
tmp_path: Path,
|
|
904
|
+
) -> None:
|
|
905
|
+
header_part, body_part = _write_prompt_parts(tmp_path)
|
|
906
|
+
working_directory = tmp_path / "project"
|
|
907
|
+
working_directory.mkdir()
|
|
908
|
+
worker_payload = _worker_payload(
|
|
909
|
+
role_name="two-stray-keys",
|
|
910
|
+
all_prompt_parts=[str(header_part), str(body_part)],
|
|
911
|
+
working_directory=working_directory,
|
|
912
|
+
tool_profile=TOOL_PROFILE_BUILD,
|
|
913
|
+
)
|
|
914
|
+
worker_payload["stray_cap"] = 5
|
|
915
|
+
worker_payload["notes"] = "operator scratch"
|
|
916
|
+
specification_path = _write_batch_spec(
|
|
917
|
+
tmp_path, all_worker_payloads=[worker_payload]
|
|
918
|
+
)
|
|
919
|
+
|
|
920
|
+
with pytest.raises(ValueError) as raised_error:
|
|
921
|
+
batch.load_batch_spec(specification_path)
|
|
922
|
+
|
|
923
|
+
error_text = str(raised_error.value)
|
|
924
|
+
assert "stray_cap" in error_text
|
|
925
|
+
assert "notes" in error_text
|
|
926
|
+
|
|
927
|
+
|
|
928
|
+
def test_load_batch_spec_rejects_the_retired_turn_cap_key(
|
|
929
|
+
tmp_path: Path,
|
|
930
|
+
) -> None:
|
|
931
|
+
header_part, body_part = _write_prompt_parts(tmp_path)
|
|
932
|
+
working_directory = tmp_path / "project"
|
|
933
|
+
working_directory.mkdir()
|
|
934
|
+
worker_payload = _worker_payload(
|
|
935
|
+
role_name="retired-cap-worker",
|
|
936
|
+
all_prompt_parts=[str(header_part), str(body_part)],
|
|
937
|
+
working_directory=working_directory,
|
|
938
|
+
tool_profile=TOOL_PROFILE_BUILD,
|
|
939
|
+
)
|
|
940
|
+
worker_payload[RETIRED_MAX_TURNS_KEYWORD] = 5
|
|
941
|
+
specification_path = _write_batch_spec(
|
|
942
|
+
tmp_path, all_worker_payloads=[worker_payload]
|
|
943
|
+
)
|
|
944
|
+
|
|
945
|
+
with pytest.raises(ValueError) as raised_error:
|
|
946
|
+
batch.load_batch_spec(specification_path)
|
|
947
|
+
|
|
948
|
+
error_text = str(raised_error.value)
|
|
949
|
+
assert RETIRED_MAX_TURNS_KEYWORD in error_text
|
|
950
|
+
assert WORKER_SPEC_TIMEOUT_KEY in error_text
|
|
951
|
+
|
|
952
|
+
|
|
953
|
+
def test_load_batch_spec_accepts_every_documented_worker_key(
|
|
954
|
+
tmp_path: Path,
|
|
955
|
+
) -> None:
|
|
956
|
+
header_part, body_part = _write_prompt_parts(tmp_path)
|
|
957
|
+
working_directory = tmp_path / "project"
|
|
958
|
+
working_directory.mkdir()
|
|
959
|
+
worker_payload: dict[str, object] = {
|
|
960
|
+
"role_name": "every-key-worker",
|
|
961
|
+
"prompt_parts": [str(header_part), str(body_part)],
|
|
962
|
+
"cwd": str(working_directory),
|
|
963
|
+
"tool_profile": TOOL_PROFILE_READONLY,
|
|
964
|
+
"timeout_seconds": 30,
|
|
965
|
+
"is_repo_only": True,
|
|
966
|
+
"agent_name": None,
|
|
967
|
+
}
|
|
968
|
+
specification_path = _write_batch_spec(
|
|
969
|
+
tmp_path, all_worker_payloads=[worker_payload]
|
|
970
|
+
)
|
|
971
|
+
|
|
972
|
+
batch_spec = batch.load_batch_spec(specification_path)
|
|
973
|
+
|
|
974
|
+
assert batch_spec.all_workers[0].role_name == "every-key-worker"
|
|
975
|
+
assert batch_spec.all_workers[0].is_repo_only is True
|
|
976
|
+
|
|
977
|
+
|
|
978
|
+
def test_load_batch_spec_rejects_non_positive_timeout(
|
|
875
979
|
tmp_path: Path,
|
|
876
980
|
) -> None:
|
|
877
981
|
header_part, body_part = _write_prompt_parts(tmp_path)
|
|
@@ -891,27 +995,11 @@ def test_load_batch_spec_rejects_non_positive_timeout_and_max_turns(
|
|
|
891
995
|
zero_timeout_dir,
|
|
892
996
|
all_worker_payloads=[zero_timeout_payload],
|
|
893
997
|
)
|
|
894
|
-
with pytest.raises(ValueError, match=
|
|
998
|
+
with pytest.raises(ValueError, match="MIN_WORKER_TIMEOUT_SECONDS"):
|
|
895
999
|
batch.load_batch_spec(zero_timeout_path)
|
|
896
1000
|
|
|
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
1001
|
|
|
914
|
-
def
|
|
1002
|
+
def test_load_batch_spec_accepts_the_default_timeout(
|
|
915
1003
|
tmp_path: Path,
|
|
916
1004
|
) -> None:
|
|
917
1005
|
header_part, body_part = _write_prompt_parts(tmp_path)
|
|
@@ -931,7 +1019,223 @@ def test_load_batch_spec_accepts_default_timeout_and_max_turns(
|
|
|
931
1019
|
|
|
932
1020
|
assert len(batch_spec.all_workers) == 1
|
|
933
1021
|
assert batch_spec.all_workers[0].timeout_seconds == DEFAULT_WORKER_TIMEOUT_SECONDS
|
|
934
|
-
|
|
1022
|
+
|
|
1023
|
+
|
|
1024
|
+
def test_timeout_over_the_ceiling_is_refused_and_at_the_ceiling_passes(
|
|
1025
|
+
tmp_path: Path,
|
|
1026
|
+
) -> None:
|
|
1027
|
+
"""The launcher refuses a spec past the 90-minute ceiling; it never clamps.
|
|
1028
|
+
|
|
1029
|
+
::
|
|
1030
|
+
|
|
1031
|
+
timeout_seconds 5401 flag: ValueError naming MAXIMUM_WORKER_TIMEOUT_SECONDS
|
|
1032
|
+
timeout_seconds 5400 ok: loads, value untouched
|
|
1033
|
+
timeout_seconds 30 ok: loads, value untouched
|
|
1034
|
+
"""
|
|
1035
|
+
header_part, body_part = _write_prompt_parts(tmp_path)
|
|
1036
|
+
working_directory = tmp_path / "project"
|
|
1037
|
+
working_directory.mkdir()
|
|
1038
|
+
|
|
1039
|
+
over_ceiling_directory = tmp_path / "over-ceiling"
|
|
1040
|
+
over_ceiling_directory.mkdir()
|
|
1041
|
+
over_ceiling_path = _write_batch_spec(
|
|
1042
|
+
over_ceiling_directory,
|
|
1043
|
+
all_worker_payloads=[
|
|
1044
|
+
_worker_payload(
|
|
1045
|
+
role_name="over-ceiling",
|
|
1046
|
+
all_prompt_parts=[str(header_part), str(body_part)],
|
|
1047
|
+
working_directory=working_directory,
|
|
1048
|
+
tool_profile=TOOL_PROFILE_BUILD,
|
|
1049
|
+
timeout_seconds=MAXIMUM_WORKER_TIMEOUT_SECONDS + 1,
|
|
1050
|
+
)
|
|
1051
|
+
],
|
|
1052
|
+
)
|
|
1053
|
+
with pytest.raises(ValueError, match="MAXIMUM_WORKER_TIMEOUT_SECONDS"):
|
|
1054
|
+
batch.load_batch_spec(over_ceiling_path)
|
|
1055
|
+
|
|
1056
|
+
at_ceiling_directory = tmp_path / "at-ceiling"
|
|
1057
|
+
at_ceiling_directory.mkdir()
|
|
1058
|
+
at_ceiling_path = _write_batch_spec(
|
|
1059
|
+
at_ceiling_directory,
|
|
1060
|
+
all_worker_payloads=[
|
|
1061
|
+
_worker_payload(
|
|
1062
|
+
role_name="at-ceiling",
|
|
1063
|
+
all_prompt_parts=[str(header_part), str(body_part)],
|
|
1064
|
+
working_directory=working_directory,
|
|
1065
|
+
tool_profile=TOOL_PROFILE_BUILD,
|
|
1066
|
+
timeout_seconds=MAXIMUM_WORKER_TIMEOUT_SECONDS,
|
|
1067
|
+
),
|
|
1068
|
+
_worker_payload(
|
|
1069
|
+
role_name="well-under-ceiling",
|
|
1070
|
+
all_prompt_parts=[str(header_part), str(body_part)],
|
|
1071
|
+
working_directory=working_directory,
|
|
1072
|
+
tool_profile=TOOL_PROFILE_BUILD,
|
|
1073
|
+
timeout_seconds=30,
|
|
1074
|
+
),
|
|
1075
|
+
],
|
|
1076
|
+
)
|
|
1077
|
+
at_ceiling_spec = batch.load_batch_spec(at_ceiling_path)
|
|
1078
|
+
|
|
1079
|
+
assert at_ceiling_spec.all_workers[0].timeout_seconds == (
|
|
1080
|
+
MAXIMUM_WORKER_TIMEOUT_SECONDS
|
|
1081
|
+
)
|
|
1082
|
+
assert at_ceiling_spec.all_workers[1].timeout_seconds == 30
|
|
1083
|
+
|
|
1084
|
+
|
|
1085
|
+
def test_ceiling_timeout_reaches_the_runner_untouched(
|
|
1086
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
1087
|
+
) -> None:
|
|
1088
|
+
header_part, body_part = _write_prompt_parts(tmp_path, role_marker="long-worker")
|
|
1089
|
+
working_directory = tmp_path / "project"
|
|
1090
|
+
working_directory.mkdir()
|
|
1091
|
+
run_state_directory = tmp_path / "run-state"
|
|
1092
|
+
batch_spec = batch.load_batch_spec(
|
|
1093
|
+
_write_batch_spec(
|
|
1094
|
+
tmp_path,
|
|
1095
|
+
all_worker_payloads=[
|
|
1096
|
+
_worker_payload(
|
|
1097
|
+
role_name="long-worker",
|
|
1098
|
+
all_prompt_parts=[str(header_part), str(body_part)],
|
|
1099
|
+
working_directory=working_directory,
|
|
1100
|
+
tool_profile=TOOL_PROFILE_READONLY,
|
|
1101
|
+
timeout_seconds=MAXIMUM_WORKER_TIMEOUT_SECONDS,
|
|
1102
|
+
)
|
|
1103
|
+
],
|
|
1104
|
+
)
|
|
1105
|
+
)
|
|
1106
|
+
recorder = _RunnerRecorder({"long-worker": _ok_outcome()})
|
|
1107
|
+
monkeypatch.setattr(
|
|
1108
|
+
batch, "batch_preflight", lambda **_kwargs: PreflightOutcome(True, None)
|
|
1109
|
+
)
|
|
1110
|
+
monkeypatch.setattr(batch, "batch_headless_runner", recorder)
|
|
1111
|
+
monkeypatch.setattr(batch, "batch_sleep", lambda _seconds: None)
|
|
1112
|
+
|
|
1113
|
+
batch.run_grok_batch(
|
|
1114
|
+
batch_spec=batch_spec,
|
|
1115
|
+
run_state_directory=run_state_directory,
|
|
1116
|
+
)
|
|
1117
|
+
|
|
1118
|
+
assert recorder.all_keyword_arguments[0]["timeout_seconds"] == (
|
|
1119
|
+
MAXIMUM_WORKER_TIMEOUT_SECONDS
|
|
1120
|
+
)
|
|
1121
|
+
|
|
1122
|
+
|
|
1123
|
+
def test_worker_invocations_carry_no_turn_cap(
|
|
1124
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
1125
|
+
) -> None:
|
|
1126
|
+
header_part, body_part = _write_prompt_parts(tmp_path, role_marker="uncapped")
|
|
1127
|
+
working_directory = tmp_path / "project"
|
|
1128
|
+
working_directory.mkdir()
|
|
1129
|
+
run_state_directory = tmp_path / "run-state"
|
|
1130
|
+
batch_spec = batch.load_batch_spec(
|
|
1131
|
+
_write_batch_spec(
|
|
1132
|
+
tmp_path,
|
|
1133
|
+
all_worker_payloads=[
|
|
1134
|
+
_worker_payload(
|
|
1135
|
+
role_name="uncapped",
|
|
1136
|
+
all_prompt_parts=[str(header_part), str(body_part)],
|
|
1137
|
+
working_directory=working_directory,
|
|
1138
|
+
tool_profile=TOOL_PROFILE_READONLY,
|
|
1139
|
+
)
|
|
1140
|
+
],
|
|
1141
|
+
)
|
|
1142
|
+
)
|
|
1143
|
+
recorder = _RunnerRecorder({"uncapped": _ok_outcome()})
|
|
1144
|
+
monkeypatch.setattr(
|
|
1145
|
+
batch, "batch_preflight", lambda **_kwargs: PreflightOutcome(True, None)
|
|
1146
|
+
)
|
|
1147
|
+
monkeypatch.setattr(batch, "batch_headless_runner", recorder)
|
|
1148
|
+
monkeypatch.setattr(batch, "batch_sleep", lambda _seconds: None)
|
|
1149
|
+
|
|
1150
|
+
batch.run_grok_batch(
|
|
1151
|
+
batch_spec=batch_spec,
|
|
1152
|
+
run_state_directory=run_state_directory,
|
|
1153
|
+
)
|
|
1154
|
+
|
|
1155
|
+
launched_keyword_arguments = recorder.all_keyword_arguments[0]
|
|
1156
|
+
all_extra_arguments = launched_keyword_arguments["all_extra_arguments"]
|
|
1157
|
+
assert RETIRED_MAX_TURNS_KEYWORD not in launched_keyword_arguments
|
|
1158
|
+
assert isinstance(all_extra_arguments, tuple)
|
|
1159
|
+
assert MAX_TURNS_FLAG not in all_extra_arguments
|
|
1160
|
+
|
|
1161
|
+
|
|
1162
|
+
def test_timed_out_worker_reads_as_timeout_beside_a_completed_worker(
|
|
1163
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
1164
|
+
tmp_path: Path,
|
|
1165
|
+
capsys: pytest.CaptureFixture[str],
|
|
1166
|
+
) -> None:
|
|
1167
|
+
"""A killed worker stays distinguishable from a completed one in the summary.
|
|
1168
|
+
|
|
1169
|
+
::
|
|
1170
|
+
|
|
1171
|
+
killed worker ok: classification timeout, is_ok False, exit code 1
|
|
1172
|
+
completed worker ok: classification ok, is_ok True
|
|
1173
|
+
"""
|
|
1174
|
+
header_done, body_done = _write_prompt_parts(tmp_path, role_marker="done-worker")
|
|
1175
|
+
header_killed, body_killed = _write_prompt_parts(
|
|
1176
|
+
tmp_path, role_marker="killed-worker"
|
|
1177
|
+
)
|
|
1178
|
+
working_directory = tmp_path / "project"
|
|
1179
|
+
working_directory.mkdir()
|
|
1180
|
+
run_state_directory = tmp_path / "run-state"
|
|
1181
|
+
specification_path = _write_batch_spec(
|
|
1182
|
+
tmp_path,
|
|
1183
|
+
all_worker_payloads=[
|
|
1184
|
+
_worker_payload(
|
|
1185
|
+
role_name="done-worker",
|
|
1186
|
+
all_prompt_parts=[str(header_done), str(body_done)],
|
|
1187
|
+
working_directory=working_directory,
|
|
1188
|
+
tool_profile=TOOL_PROFILE_BUILD,
|
|
1189
|
+
),
|
|
1190
|
+
_worker_payload(
|
|
1191
|
+
role_name="killed-worker",
|
|
1192
|
+
all_prompt_parts=[str(header_killed), str(body_killed)],
|
|
1193
|
+
working_directory=working_directory,
|
|
1194
|
+
tool_profile=TOOL_PROFILE_BUILD,
|
|
1195
|
+
),
|
|
1196
|
+
],
|
|
1197
|
+
)
|
|
1198
|
+
recorder = _RunnerRecorder(
|
|
1199
|
+
{
|
|
1200
|
+
"done-worker": _ok_outcome(),
|
|
1201
|
+
"killed-worker": GrokRunnerOutcome(
|
|
1202
|
+
is_ok=False,
|
|
1203
|
+
returncode=-9,
|
|
1204
|
+
classification=CLASSIFICATION_TIMEOUT,
|
|
1205
|
+
stdout="",
|
|
1206
|
+
stderr=FIXTURE_TIMEOUT_KILL_TEXT,
|
|
1207
|
+
),
|
|
1208
|
+
}
|
|
1209
|
+
)
|
|
1210
|
+
monkeypatch.setattr(
|
|
1211
|
+
batch, "batch_preflight", lambda **_kwargs: PreflightOutcome(True, None)
|
|
1212
|
+
)
|
|
1213
|
+
monkeypatch.setattr(batch, "batch_headless_runner", recorder)
|
|
1214
|
+
monkeypatch.setattr(batch, "batch_sleep", lambda _seconds: None)
|
|
1215
|
+
|
|
1216
|
+
exit_code = batch.main(
|
|
1217
|
+
[
|
|
1218
|
+
"--spec",
|
|
1219
|
+
str(specification_path),
|
|
1220
|
+
"--run-temp-dir",
|
|
1221
|
+
str(run_state_directory),
|
|
1222
|
+
]
|
|
1223
|
+
)
|
|
1224
|
+
|
|
1225
|
+
summary_payload = json.loads(capsys.readouterr().out)
|
|
1226
|
+
payload_by_role_name = {
|
|
1227
|
+
each_payload[SUMMARY_ROLE_NAME_KEY]: each_payload
|
|
1228
|
+
for each_payload in summary_payload[SUMMARY_WORKERS_KEY]
|
|
1229
|
+
}
|
|
1230
|
+
killed_payload = payload_by_role_name["killed-worker"]
|
|
1231
|
+
done_payload = payload_by_role_name["done-worker"]
|
|
1232
|
+
|
|
1233
|
+
assert exit_code == 1
|
|
1234
|
+
assert killed_payload[SUMMARY_CLASSIFICATION_KEY] == CLASSIFICATION_TIMEOUT
|
|
1235
|
+
assert killed_payload[SUMMARY_IS_OK_KEY] is False
|
|
1236
|
+
assert killed_payload[SUMMARY_CLASSIFICATION_KEY] != CLASSIFICATION_OK
|
|
1237
|
+
assert done_payload[SUMMARY_CLASSIFICATION_KEY] == CLASSIFICATION_OK
|
|
1238
|
+
assert done_payload[SUMMARY_IS_OK_KEY] is True
|
|
935
1239
|
|
|
936
1240
|
|
|
937
1241
|
def test_unwritable_report_file_keeps_the_worker_outcome(
|
package/scripts/tests/CLAUDE.md
CHANGED
|
@@ -10,6 +10,7 @@ pytest suite for the Python scripts and Pester suite for the PowerShell scripts
|
|
|
10
10
|
| `test_setup_project_paths_config.py` | Configuration constants used by `setup_project_paths.py` |
|
|
11
11
|
| `test_sweep_empty_dirs.py` | `sweep_empty_dirs.py` — age check, one-shot mode, and continuous-watch behavior |
|
|
12
12
|
| `test_sync_to_cursor.py` | `sync_to_cursor/` package — mapping, hashing, manifest, and path resolution |
|
|
13
|
+
| `test_grok_worker_constants.py` | `grok_worker_constants.py` — the accepted batch worker-key set stays in step with the worker key constants, and the unknown-key message names both its placeholders |
|
|
13
14
|
|
|
14
15
|
## PowerShell test files
|
|
15
16
|
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Specifications for the accepted-key set on a batch worker entry.
|
|
2
|
+
|
|
3
|
+
The batch launcher rejects a worker entry carrying any key outside
|
|
4
|
+
``ALL_KNOWN_WORKER_SPEC_KEYS``. That makes the set a gate on the published
|
|
5
|
+
JSON contract: a worker key the module defines but the set omits is rejected
|
|
6
|
+
from every spec that uses it, and a member the module declares nowhere keeps
|
|
7
|
+
accepting a key the launcher drops. Both drifts are silent, so they are
|
|
8
|
+
pinned here.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from dev_env_scripts_constants import grok_worker_constants
|
|
14
|
+
|
|
15
|
+
WORKER_SPEC_KEY_NAME_PREFIX: str = "WORKER_SPEC_"
|
|
16
|
+
WORKER_SPEC_KEY_NAME_SUFFIX: str = "_KEY"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _all_declared_worker_key_values() -> set[str]:
|
|
20
|
+
"""Read every ``WORKER_SPEC_*_KEY`` value the constants module declares."""
|
|
21
|
+
return {
|
|
22
|
+
getattr(grok_worker_constants, each_name)
|
|
23
|
+
for each_name in dir(grok_worker_constants)
|
|
24
|
+
if each_name.startswith(WORKER_SPEC_KEY_NAME_PREFIX)
|
|
25
|
+
and each_name.endswith(WORKER_SPEC_KEY_NAME_SUFFIX)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_should_accept_every_declared_worker_spec_key() -> None:
|
|
30
|
+
all_declared_key_values = _all_declared_worker_key_values()
|
|
31
|
+
all_missing_key_values = (
|
|
32
|
+
all_declared_key_values - grok_worker_constants.ALL_KNOWN_WORKER_SPEC_KEYS
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
assert not all_missing_key_values, (
|
|
36
|
+
"worker key constants missing from the accepted set: "
|
|
37
|
+
f"{sorted(all_missing_key_values)}"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_should_accept_only_keys_the_module_declares() -> None:
|
|
42
|
+
all_declared_key_values = _all_declared_worker_key_values()
|
|
43
|
+
all_stale_key_values = (
|
|
44
|
+
grok_worker_constants.ALL_KNOWN_WORKER_SPEC_KEYS - all_declared_key_values
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
assert not all_stale_key_values, (
|
|
48
|
+
f"accepted keys with no worker key constant: {sorted(all_stale_key_values)}"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def test_should_name_both_placeholders_in_the_unknown_key_message() -> None:
|
|
53
|
+
formatted_message = grok_worker_constants.UNKNOWN_WORKER_KEY_ERROR_TEMPLATE.format(
|
|
54
|
+
unknown_keys="timeout_second",
|
|
55
|
+
accepted_keys="timeout_seconds",
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
assert "timeout_second;" in formatted_message
|
|
59
|
+
assert formatted_message.endswith("timeout_seconds")
|
|
@@ -378,3 +378,49 @@ def test_main_headless_flavor_emits_outcome_path_on_stdout(
|
|
|
378
378
|
assert "post_audit_thread.py" in captured.out
|
|
379
379
|
assert "Do not post reviews" in captured.out
|
|
380
380
|
assert "add_comment_to_pending_review" not in captured.out
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
_PACKAGE_ROOT = _SCRIPTS_DIR.parents[3]
|
|
384
|
+
_CODE_QUALITY_AGENT_PATH = _PACKAGE_ROOT / "agents" / "code-quality-agent.md"
|
|
385
|
+
_AUDIT_CONTRACT_PATH = _PACKAGE_ROOT / "_shared" / "pr-loop" / "audit-contract.md"
|
|
386
|
+
_PRECATCH_RUBRIC_PATH = _PACKAGE_ROOT / "_shared" / "pr-loop" / "precatch-rubric.md"
|
|
387
|
+
_CATEGORY_Q_LABEL = (
|
|
388
|
+
"Cross-surface claim consistency "
|
|
389
|
+
"(terminology, PR-description claims, message-vs-guard)"
|
|
390
|
+
)
|
|
391
|
+
_CATEGORY_Q_RUBRIC_REFERENCE = (
|
|
392
|
+
"../audit-rubrics/category_rubrics/category-q-cross-surface-claims.md"
|
|
393
|
+
)
|
|
394
|
+
_A_THROUGH_Q_PATTERN = re.compile(r"A[\u2013-]Q")
|
|
395
|
+
_SEVENTEEN_CATEGORIES_PATTERN = re.compile(r"seventeen categor", re.IGNORECASE)
|
|
396
|
+
_STALE_SIXTEEN_PATTERN = re.compile(r"sixteen categor", re.IGNORECASE)
|
|
397
|
+
_STALE_A_THROUGH_P_PATTERN = re.compile(r"A[\u2013-]P")
|
|
398
|
+
|
|
399
|
+
|
|
400
|
+
def test_code_quality_agent_default_scope_is_a_through_q() -> None:
|
|
401
|
+
agent_text = _CODE_QUALITY_AGENT_PATH.read_text(encoding="utf-8")
|
|
402
|
+
assert _A_THROUGH_Q_PATTERN.search(agent_text) is not None
|
|
403
|
+
assert _SEVENTEEN_CATEGORIES_PATTERN.search(agent_text) is not None
|
|
404
|
+
assert _STALE_SIXTEEN_PATTERN.search(agent_text) is None
|
|
405
|
+
assert "A through Q" in agent_text
|
|
406
|
+
assert "A through P" not in agent_text
|
|
407
|
+
assert _STALE_A_THROUGH_P_PATTERN.search(agent_text) is None
|
|
408
|
+
assert "## Bug Categories A–Q" in agent_text
|
|
409
|
+
assert _CATEGORY_Q_LABEL in agent_text
|
|
410
|
+
assert _CATEGORY_Q_RUBRIC_REFERENCE in agent_text
|
|
411
|
+
assert "| Q |" in agent_text
|
|
412
|
+
|
|
413
|
+
|
|
414
|
+
def test_audit_contract_category_schema_includes_q() -> None:
|
|
415
|
+
contract_text = _AUDIT_CONTRACT_PATH.read_text(encoding="utf-8")
|
|
416
|
+
assert '"category": "A | B | C | D | E | F | G | H | I | J | K | L | M | N | O | P | Q"' in (
|
|
417
|
+
contract_text
|
|
418
|
+
)
|
|
419
|
+
assert _A_THROUGH_Q_PATTERN.search(contract_text) is not None
|
|
420
|
+
assert _STALE_A_THROUGH_P_PATTERN.search(contract_text) is None
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def test_precatch_rubric_adversarial_lane_uses_a_through_q() -> None:
|
|
424
|
+
rubric_text = _PRECATCH_RUBRIC_PATH.read_text(encoding="utf-8")
|
|
425
|
+
assert _A_THROUGH_Q_PATTERN.search(rubric_text) is not None
|
|
426
|
+
assert _STALE_A_THROUGH_P_PATTERN.search(rubric_text) is None
|