claude-dev-env 2.7.1 → 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 (85) 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/process-tree/CLAUDE.md +41 -0
  6. package/_shared/process-tree/scripts/config/process_tree_scripts_constants/__init__.py +1 -0
  7. package/_shared/process-tree/scripts/config/process_tree_scripts_constants/process_tree_kill_constants.py +27 -0
  8. package/_shared/process-tree/scripts/process_tree_kill.py +141 -0
  9. package/_shared/process-tree/scripts/pyproject.toml +16 -0
  10. package/_shared/process-tree/scripts/test_process_tree_kill.py +278 -0
  11. package/agents/code-quality-agent.md +6 -5
  12. package/agents/deep-research.md +7 -24
  13. package/agents/docs-agent.md +1 -27
  14. package/agents/issue-tracker.md +1 -7
  15. package/agents/skill-writer-agent.md +1 -2
  16. package/agents/test_agent_frontmatter.py +309 -12
  17. package/hooks/blocking/CLAUDE.md +2 -0
  18. package/hooks/blocking/fable_spawn_gate.py +187 -0
  19. package/hooks/blocking/piped_pytest_blocker.py +1223 -0
  20. package/hooks/blocking/plain_language_blocker.py +287 -15
  21. package/hooks/blocking/test_fable_spawn_gate.py +374 -0
  22. package/hooks/blocking/test_piped_pytest_blocker.py +587 -0
  23. package/hooks/blocking/test_plain_language_blocker.py +277 -2
  24. package/hooks/blocking/test_pre_tool_use_dispatcher.py +34 -2
  25. package/hooks/git-hooks/CLAUDE.md +2 -2
  26. package/hooks/git-hooks/git_hooks_constants/__init__.py +28 -0
  27. package/hooks/git-hooks/pre_push.py +343 -54
  28. package/hooks/git-hooks/test_pre_push.py +852 -6
  29. package/hooks/hooks.json +9 -19
  30. package/hooks/hooks_constants/CLAUDE.md +2 -0
  31. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +1 -0
  32. package/hooks/hooks_constants/fable_spawn_gate_constants.py +62 -0
  33. package/hooks/hooks_constants/piped_pytest_blocker_constants.py +360 -0
  34. package/hooks/hooks_constants/plain_language_blocker_constants.py +64 -1
  35. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +10 -0
  36. package/hooks/hooks_constants/shell_command_segments.py +1 -1
  37. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +1 -0
  38. package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +27 -0
  39. package/hooks/hooks_constants/test_prose_metrics_parity.py +124 -0
  40. package/package.json +1 -1
  41. package/rules/CLAUDE.md +1 -0
  42. package/rules/ask-user-question-required.md +26 -0
  43. package/rules/claims-as-quotes.md +65 -0
  44. package/scripts/CLAUDE.md +1 -1
  45. package/scripts/_code_review_test_support.py +6 -0
  46. package/scripts/check.ps1 +18 -5
  47. package/scripts/claude_chain_runner.py +203 -31
  48. package/scripts/codec_forwarding_test_support.py +2 -0
  49. package/scripts/dev_env_scripts_constants/CLAUDE.md +4 -4
  50. package/scripts/dev_env_scripts_constants/claude_chain_constants.py +38 -0
  51. package/scripts/dev_env_scripts_constants/code_review_constants.py +403 -2
  52. package/scripts/dev_env_scripts_constants/grok_worker_constants.py +28 -24
  53. package/scripts/grok_headless_runner.py +16 -83
  54. package/scripts/spawn_grok_batch.py +38 -1
  55. package/scripts/test_claude_chain_runner.py +358 -0
  56. package/scripts/test_grok_headless_runner.py +18 -63
  57. package/scripts/test_invoke_code_review.py +298 -0
  58. package/scripts/test_resolve_worker_spawn.py +6 -0
  59. package/scripts/test_spawn_grok_batch.py +101 -0
  60. package/scripts/tests/CLAUDE.md +1 -0
  61. package/scripts/tests/test_grok_worker_constants.py +59 -0
  62. package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +46 -0
  63. package/skills/autoconverge/workflow/converge.contract.test.mjs +105 -2
  64. package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +34 -1
  65. package/skills/autoconverge/workflow/converge.mjs +66 -33
  66. package/skills/codex-review/scripts/codex_review_scripts_constants/codex_usage_probe_constants.py +0 -4
  67. package/skills/codex-review/scripts/codex_usage_probe.py +20 -33
  68. package/skills/codex-review/scripts/run_codex_review.py +16 -64
  69. package/skills/codex-review/scripts/test_codex_usage_probe.py +46 -41
  70. package/skills/codex-review/scripts/test_run_codex_review.py +1 -33
  71. package/skills/e-code-review/SKILL.md +9 -8
  72. package/skills/e-code-review/reference/fix.md +29 -7
  73. package/skills/e-code-review/reference/loop.md +230 -14
  74. package/skills/e-code-review/reference/low.md +33 -15
  75. package/skills/e-code-review/reference/medium.md +55 -21
  76. package/skills/e-code-review/reference/xhigh.md +30 -12
  77. package/skills/fresh-branch/CLAUDE.md +5 -5
  78. package/skills/fresh-branch/SKILL.md +14 -6
  79. package/skills/fresh-branch/scripts/create_fresh_branch.py +122 -39
  80. package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +10 -3
  81. package/skills/fresh-branch/scripts/test_create_fresh_branch.py +251 -0
  82. package/skills/grok-spawn/SKILL.md +5 -0
  83. package/skills/orchestrator/SKILL.md +4 -1
  84. package/skills/orchestrator-refresh/SKILL.md +5 -1
  85. 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,
@@ -133,6 +137,7 @@ def _claude_served(
133
137
  stdout=stdout,
134
138
  stderr="",
135
139
  attempts=(ChainAttempt(command="claude", status="served"),),
140
+ terminal_status=TERMINAL_STATUS_SERVED,
136
141
  )
137
142
 
138
143
 
@@ -143,6 +148,7 @@ def _claude_exhausted() -> ChainInvocationOutcome:
143
148
  stdout="",
144
149
  stderr="usage limit reached",
145
150
  attempts=(ChainAttempt(command="claude", status="usage_limited"),),
151
+ terminal_status=TERMINAL_STATUS_CHAIN_EXHAUSTED,
146
152
  )
147
153
 
148
154
 
@@ -874,6 +874,107 @@ def test_load_batch_spec_missing_worker_keys_raise_value_error(
874
874
  ).lower() or "must be" in str(raised_error.value).lower()
875
875
 
876
876
 
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
+
877
978
  def test_load_batch_spec_rejects_non_positive_timeout(
878
979
  tmp_path: Path,
879
980
  ) -> None:
@@ -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