claude-dev-env 2.7.1 → 2.9.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 (127) hide show
  1. package/CLAUDE.md +7 -1
  2. package/_shared/CLAUDE.md +1 -0
  3. package/_shared/advisor/advisor-protocol.md +19 -9
  4. package/_shared/pr-loop/audit-contract.md +4 -4
  5. package/_shared/pr-loop/precatch-rubric.md +2 -2
  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/clean-coder.md +9 -19
  13. package/agents/code-quality-agent.md +6 -5
  14. package/agents/deep-research.md +7 -24
  15. package/agents/docs-agent.md +1 -27
  16. package/agents/issue-tracker.md +1 -7
  17. package/agents/skill-writer-agent.md +1 -2
  18. package/agents/test_agent_frontmatter.py +335 -12
  19. package/docs/CODE_RULES.md +4 -2
  20. package/docs/references/CLAUDE.md +2 -2
  21. package/docs/references/advisor-tool.md +44 -6
  22. package/docs/references/team-advisor-skill.md +14 -8
  23. package/hooks/blocking/CLAUDE.md +2 -0
  24. package/hooks/blocking/fable_spawn_gate.py +187 -0
  25. package/hooks/blocking/piped_pytest_blocker.py +1223 -0
  26. package/hooks/blocking/plain_language_blocker.py +287 -15
  27. package/hooks/blocking/test_fable_spawn_gate.py +374 -0
  28. package/hooks/blocking/test_piped_pytest_blocker.py +587 -0
  29. package/hooks/blocking/test_plain_language_blocker.py +277 -2
  30. package/hooks/blocking/test_pre_tool_use_dispatcher.py +34 -2
  31. package/hooks/git-hooks/CLAUDE.md +2 -2
  32. package/hooks/git-hooks/git_hooks_constants/__init__.py +28 -0
  33. package/hooks/git-hooks/pre_push.py +343 -54
  34. package/hooks/git-hooks/test_pre_push.py +852 -6
  35. package/hooks/hooks.json +9 -19
  36. package/hooks/hooks_constants/CLAUDE.md +2 -0
  37. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +1 -0
  38. package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -0
  39. package/hooks/hooks_constants/fable_spawn_gate_constants.py +62 -0
  40. package/hooks/hooks_constants/piped_pytest_blocker_constants.py +360 -0
  41. package/hooks/hooks_constants/plain_language_blocker_constants.py +64 -1
  42. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +10 -0
  43. package/hooks/hooks_constants/shell_command_segments.py +1 -1
  44. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +1 -0
  45. package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +27 -0
  46. package/hooks/hooks_constants/test_prose_metrics_parity.py +124 -0
  47. package/output-styles/CLAUDE.md +17 -0
  48. package/output-styles/caveman-agent.md +37 -0
  49. package/package.json +2 -1
  50. package/rules/CLAUDE.md +1 -0
  51. package/rules/ask-user-question-required.md +26 -0
  52. package/rules/claims-as-quotes.md +65 -0
  53. package/rules/code-standards.md +33 -7
  54. package/rules/eli11-replies.md +1 -1
  55. package/scripts/CLAUDE.md +3 -3
  56. package/scripts/_code_review_test_support.py +6 -0
  57. package/scripts/check.ps1 +18 -5
  58. package/scripts/claude_chain_runner.py +203 -31
  59. package/scripts/codec_forwarding_test_support.py +2 -0
  60. package/scripts/dev_env_scripts_constants/CLAUDE.md +4 -4
  61. package/scripts/dev_env_scripts_constants/claude_chain_constants.py +38 -0
  62. package/scripts/dev_env_scripts_constants/code_review_constants.py +403 -2
  63. package/scripts/dev_env_scripts_constants/grok_run_ledger_constants.py +50 -0
  64. package/scripts/dev_env_scripts_constants/grok_worker_constants.py +132 -24
  65. package/scripts/grok_headless_runner.py +16 -83
  66. package/scripts/grok_patch_artifacts.py +123 -0
  67. package/scripts/grok_run_ledger.py +318 -0
  68. package/scripts/spawn_grok_batch.py +591 -10
  69. package/scripts/test_claude_chain_runner.py +358 -0
  70. package/scripts/test_grok_headless_runner.py +18 -63
  71. package/scripts/test_grok_patch_artifacts.py +82 -0
  72. package/scripts/test_grok_run_ledger.py +116 -0
  73. package/scripts/test_invoke_code_review.py +298 -0
  74. package/scripts/test_resolve_worker_spawn.py +6 -0
  75. package/scripts/test_spawn_grok_batch.py +396 -0
  76. package/scripts/tests/CLAUDE.md +1 -0
  77. package/scripts/tests/test_grok_worker_constants.py +59 -0
  78. package/skills/CLAUDE.md +4 -2
  79. package/skills/_shared/CLAUDE.md +37 -4
  80. package/skills/_shared/advisor/CLAUDE.md +9 -0
  81. package/skills/_shared/advisor/advisor-protocol.md +5 -0
  82. package/skills/_shared/advisor/scripts/README.md +9 -0
  83. package/skills/_shared/end-of-run-gotcha-recommendations.md +156 -0
  84. package/skills/_shared/pr-loop/CLAUDE.md +18 -1
  85. package/skills/_shared/pr-loop/audit-contract.md +5 -0
  86. package/skills/_shared/pr-loop/audit-reply-template.md +5 -0
  87. package/skills/_shared/pr-loop/code-rules-gate.md +5 -0
  88. package/skills/_shared/pr-loop/fix-protocol.md +5 -0
  89. package/skills/_shared/pr-loop/gh-payloads.md +5 -0
  90. package/skills/_shared/pr-loop/post-audit-thread-contract.md +5 -0
  91. package/skills/_shared/pr-loop/precatch-rubric.md +5 -0
  92. package/skills/_shared/pr-loop/scripts/CLAUDE.md +8 -1
  93. package/skills/_shared/pr-loop/scripts/RUNTIME_SCRIPTS.md +29 -0
  94. package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +46 -0
  95. package/skills/_shared/pr-loop/state-schema.md +5 -0
  96. package/skills/_shared/pr-loop/worker-spawn.md +5 -0
  97. package/skills/autoconverge/workflow/converge.contract.test.mjs +105 -2
  98. package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +34 -1
  99. package/skills/autoconverge/workflow/converge.mjs +66 -33
  100. package/skills/codex-review/scripts/codex_review_scripts_constants/codex_usage_probe_constants.py +0 -4
  101. package/skills/codex-review/scripts/codex_usage_probe.py +20 -33
  102. package/skills/codex-review/scripts/run_codex_review.py +16 -64
  103. package/skills/codex-review/scripts/test_codex_usage_probe.py +46 -41
  104. package/skills/codex-review/scripts/test_run_codex_review.py +1 -33
  105. package/skills/e-code-review/SKILL.md +15 -9
  106. package/skills/e-code-review/reference/fix.md +29 -7
  107. package/skills/e-code-review/reference/loop.md +230 -14
  108. package/skills/e-code-review/reference/low.md +33 -15
  109. package/skills/e-code-review/reference/medium.md +55 -21
  110. package/skills/e-code-review/reference/runner-selection.md +40 -0
  111. package/skills/e-code-review/reference/xhigh.md +30 -12
  112. package/skills/e-code-review/scripts/e_code_review_scripts_constants/__init__.py +1 -0
  113. package/skills/e-code-review/scripts/e_code_review_scripts_constants/grok_code_review_constants.py +55 -0
  114. package/skills/e-code-review/scripts/grok_code_review.py +221 -0
  115. package/skills/e-code-review/scripts/test_grok_code_review.py +212 -0
  116. package/skills/fresh-branch/CLAUDE.md +5 -5
  117. package/skills/fresh-branch/SKILL.md +14 -6
  118. package/skills/fresh-branch/scripts/create_fresh_branch.py +122 -39
  119. package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +10 -3
  120. package/skills/fresh-branch/scripts/test_create_fresh_branch.py +251 -0
  121. package/skills/grok-spawn/SKILL.md +10 -0
  122. package/skills/orchestrator/SKILL.md +9 -1
  123. package/skills/orchestrator-refresh/SKILL.md +5 -1
  124. package/skills/task-build/reference/tool-routing.md +3 -0
  125. package/skills/team-advisor/SKILL.md +23 -41
  126. package/system-prompts/software-engineer.xml +6 -3
  127. package/skills/test_markdown_link_integrity.py +0 -107
@@ -0,0 +1,116 @@
1
+ """Behavioral tests for the host-neutral Grok run ledger."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ import pytest
10
+
11
+ _SCRIPTS_DIR = Path(__file__).resolve().parent
12
+ if str(_SCRIPTS_DIR) not in sys.path:
13
+ sys.path.insert(0, str(_SCRIPTS_DIR))
14
+
15
+ from dev_env_scripts_constants.grok_run_ledger_constants import ( # noqa: E402
16
+ TASK_STATUS_ADVISOR_BLOCKED,
17
+ TASK_STATUS_COMPLETED,
18
+ TASK_STATUS_PENDING,
19
+ TASK_STATUS_PENDING_REVIEW,
20
+ )
21
+ from grok_run_ledger import GrokRunLedger, is_legal_status # noqa: E402
22
+
23
+
24
+ def test_register_task_persists_atomically(tmp_path: Path) -> None:
25
+ ledger = GrokRunLedger(tmp_path)
26
+ ledger.register_task(task_id="O-04", all_dependencies=())
27
+ reloaded = GrokRunLedger(tmp_path)
28
+ record = reloaded.get_task("O-04")
29
+ assert record.status == TASK_STATUS_PENDING
30
+ assert is_legal_status(record.status)
31
+ payload = json.loads((tmp_path / "grok-run-ledger.json").read_text(encoding="utf-8"))
32
+ assert payload["tasks"][0]["task_id"] == "O-04"
33
+
34
+
35
+ def test_dependencies_block_dispatch(tmp_path: Path) -> None:
36
+ ledger = GrokRunLedger(tmp_path)
37
+ ledger.register_task(task_id="dep", all_dependencies=())
38
+ ledger.register_task(task_id="child", all_dependencies=("dep",))
39
+ assert ledger.can_dispatch("child") is False
40
+ with pytest.raises(ValueError, match="dependencies"):
41
+ ledger.mark_in_progress(
42
+ task_id="child",
43
+ owner_id="w1",
44
+ advisor_session_id="s1",
45
+ base_sha="aaa",
46
+ )
47
+ ledger.mark_in_progress(
48
+ task_id="dep", owner_id="w0", advisor_session_id="s0", base_sha="aaa"
49
+ )
50
+ ledger.mark_completed(
51
+ task_id="dep",
52
+ reviewed_head="bbb",
53
+ all_changed_paths=(),
54
+ advisor_verdict="ENDORSE",
55
+ all_acceptance_mapping={},
56
+ all_test_evidence=["ok"],
57
+ )
58
+ assert ledger.can_dispatch("child") is True
59
+
60
+
61
+ def test_one_live_owner_and_unique_advisor_session(tmp_path: Path) -> None:
62
+ ledger = GrokRunLedger(tmp_path)
63
+ ledger.register_task(task_id="a", all_dependencies=())
64
+ ledger.register_task(task_id="b", all_dependencies=())
65
+ ledger.mark_in_progress(
66
+ task_id="a", owner_id="owner", advisor_session_id="sess-a", base_sha="1"
67
+ )
68
+ with pytest.raises(ValueError, match="owner already live"):
69
+ ledger.mark_in_progress(
70
+ task_id="b", owner_id="owner", advisor_session_id="sess-b", base_sha="1"
71
+ )
72
+ with pytest.raises(ValueError, match="advisor session already bound"):
73
+ ledger.mark_in_progress(
74
+ task_id="b", owner_id="other", advisor_session_id="sess-a", base_sha="1"
75
+ )
76
+
77
+
78
+ def test_snapshot_drift_moves_to_pending_review(tmp_path: Path) -> None:
79
+ ledger = GrokRunLedger(tmp_path)
80
+ ledger.register_task(task_id="t", all_dependencies=())
81
+ ledger.mark_in_progress(
82
+ task_id="t", owner_id="w", advisor_session_id="s", base_sha="base"
83
+ )
84
+ record = ledger.invalidate_on_snapshot_drift(task_id="t", current_sha="drifted")
85
+ assert record.status == TASK_STATUS_PENDING_REVIEW
86
+ assert record.owner_id is None
87
+
88
+
89
+ def test_advisor_blocked_terminal(tmp_path: Path) -> None:
90
+ ledger = GrokRunLedger(tmp_path)
91
+ ledger.register_task(task_id="t", all_dependencies=())
92
+ ledger.mark_in_progress(
93
+ task_id="t", owner_id="w", advisor_session_id="s", base_sha="base"
94
+ )
95
+ record = ledger.mark_advisor_blocked(task_id="t", reason="bind failed")
96
+ assert record.status == TASK_STATUS_ADVISOR_BLOCKED
97
+ assert "bind failed" in record.test_evidence[0]
98
+
99
+
100
+ def test_completed_records_acceptance_and_head(tmp_path: Path) -> None:
101
+ ledger = GrokRunLedger(tmp_path)
102
+ ledger.register_task(task_id="t", all_dependencies=())
103
+ ledger.mark_in_progress(
104
+ task_id="t", owner_id="w", advisor_session_id="s", base_sha="base"
105
+ )
106
+ record = ledger.mark_completed(
107
+ task_id="t",
108
+ reviewed_head="head",
109
+ all_changed_paths=("a.py",),
110
+ advisor_verdict="ENDORSE",
111
+ all_acceptance_mapping={"criterion": "evidence"},
112
+ all_test_evidence=["pytest -q"],
113
+ )
114
+ assert record.status == TASK_STATUS_COMPLETED
115
+ assert record.reviewed_head == "head"
116
+ assert record.changed_paths == ("a.py",)
@@ -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