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.
- 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/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 +1 -1
- 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 +28 -24
- package/scripts/grok_headless_runner.py +16 -83
- package/scripts/spawn_grok_batch.py +38 -1
- package/scripts/test_claude_chain_runner.py +358 -0
- package/scripts/test_grok_headless_runner.py +18 -63
- package/scripts/test_invoke_code_review.py +298 -0
- package/scripts/test_resolve_worker_spawn.py +6 -0
- package/scripts/test_spawn_grok_batch.py +101 -0
- 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 +105 -2
- package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +34 -1
- package/skills/autoconverge/workflow/converge.mjs +66 -33
- 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 +5 -0
- package/skills/orchestrator/SKILL.md +4 -1
- package/skills/orchestrator-refresh/SKILL.md +5 -1
- 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:
|
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
|