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
|
@@ -40,9 +40,21 @@ import sys
|
|
|
40
40
|
import threading
|
|
41
41
|
from collections.abc import Mapping, Sequence
|
|
42
42
|
from datetime import datetime, timezone
|
|
43
|
+
from pathlib import Path
|
|
43
44
|
from typing import IO, Protocol
|
|
44
45
|
|
|
45
|
-
|
|
46
|
+
_shared_process_tree_scripts_directory = (
|
|
47
|
+
Path(__file__).resolve().parents[3] / "_shared" / "process-tree" / "scripts"
|
|
48
|
+
)
|
|
49
|
+
if str(_shared_process_tree_scripts_directory) not in sys.path:
|
|
50
|
+
sys.path.insert(0, str(_shared_process_tree_scripts_directory))
|
|
51
|
+
|
|
52
|
+
from process_tree_kill import ( # noqa: E402
|
|
53
|
+
should_start_new_session,
|
|
54
|
+
terminate_process_tree,
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
from codex_review_scripts_constants.codex_usage_probe_constants import ( # noqa: E402
|
|
46
58
|
ALL_APP_SERVER_COMMAND_PARTS,
|
|
47
59
|
ALL_WINDOWS_SCRIPT_SUFFIXES,
|
|
48
60
|
APP_SERVER_TIMEOUT_SECONDS,
|
|
@@ -94,10 +106,6 @@ from codex_review_scripts_constants.codex_usage_probe_constants import (
|
|
|
94
106
|
WINDOWS_COMMAND_SHELL,
|
|
95
107
|
WINDOWS_COMMAND_SHELL_RUN_FLAG,
|
|
96
108
|
WINDOWS_OS_NAME,
|
|
97
|
-
WINDOWS_TASKKILL_COMMAND,
|
|
98
|
-
WINDOWS_TASKKILL_FORCE_FLAG,
|
|
99
|
-
WINDOWS_TASKKILL_PID_FLAG,
|
|
100
|
-
WINDOWS_TASKKILL_TREE_FLAG,
|
|
101
109
|
)
|
|
102
110
|
|
|
103
111
|
|
|
@@ -387,41 +395,19 @@ def _collect_server_lines(
|
|
|
387
395
|
|
|
388
396
|
|
|
389
397
|
def _terminate_process_tree(server_process: subprocess.Popen[str]) -> None:
|
|
390
|
-
"""Stop the app-server process and any children it launched.
|
|
398
|
+
"""Stop the app-server process and any children it launched, then reap it.
|
|
391
399
|
|
|
392
400
|
On Windows the probe may wrap a ``.cmd``/``.bat`` shim as ``cmd /c``, so
|
|
393
401
|
``Popen.pid`` is ``cmd.exe``. Killing only that process leaves the real
|
|
394
|
-
Codex app-server grandchild alive and holding stdout.
|
|
395
|
-
|
|
402
|
+
Codex app-server grandchild alive and holding stdout. The shared tree kill
|
|
403
|
+
ends the whole tree on either platform; the timed wait that follows reaps
|
|
404
|
+
the direct child so the enclosing ``with`` block exits without a long wait.
|
|
396
405
|
"""
|
|
397
|
-
|
|
398
|
-
return
|
|
399
|
-
process_id = server_process.pid
|
|
400
|
-
if process_id is None:
|
|
401
|
-
return
|
|
402
|
-
if os.name == WINDOWS_OS_NAME:
|
|
403
|
-
subprocess.run(
|
|
404
|
-
[
|
|
405
|
-
WINDOWS_TASKKILL_COMMAND,
|
|
406
|
-
WINDOWS_TASKKILL_FORCE_FLAG,
|
|
407
|
-
WINDOWS_TASKKILL_TREE_FLAG,
|
|
408
|
-
WINDOWS_TASKKILL_PID_FLAG,
|
|
409
|
-
str(process_id),
|
|
410
|
-
],
|
|
411
|
-
check=False,
|
|
412
|
-
stdout=subprocess.DEVNULL,
|
|
413
|
-
stderr=subprocess.DEVNULL,
|
|
414
|
-
)
|
|
415
|
-
try:
|
|
416
|
-
server_process.wait(timeout=PROCESS_TREE_WAIT_TIMEOUT_SECONDS)
|
|
417
|
-
except subprocess.TimeoutExpired:
|
|
418
|
-
server_process.kill()
|
|
419
|
-
return
|
|
420
|
-
server_process.kill()
|
|
406
|
+
terminate_process_tree(server_process)
|
|
421
407
|
try:
|
|
422
408
|
server_process.wait(timeout=PROCESS_TREE_WAIT_TIMEOUT_SECONDS)
|
|
423
409
|
except subprocess.TimeoutExpired:
|
|
424
|
-
|
|
410
|
+
return
|
|
425
411
|
|
|
426
412
|
|
|
427
413
|
def _teardown_app_server_exchange(
|
|
@@ -467,6 +453,7 @@ def _exchange_app_server_messages_via_subprocess(
|
|
|
467
453
|
text=True,
|
|
468
454
|
encoding=UTF8_ENCODING,
|
|
469
455
|
shell=False,
|
|
456
|
+
start_new_session=should_start_new_session(),
|
|
470
457
|
) as server_process:
|
|
471
458
|
server_stdin = server_process.stdin
|
|
472
459
|
server_stdout = server_process.stdout
|
|
@@ -18,22 +18,29 @@ import json
|
|
|
18
18
|
import os
|
|
19
19
|
import re
|
|
20
20
|
import shutil
|
|
21
|
-
import signal
|
|
22
21
|
import subprocess
|
|
22
|
+
import sys
|
|
23
23
|
from dataclasses import dataclass
|
|
24
24
|
from pathlib import Path
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
_shared_process_tree_scripts_directory = (
|
|
27
|
+
Path(__file__).resolve().parents[3] / "_shared" / "process-tree" / "scripts"
|
|
28
|
+
)
|
|
29
|
+
if str(_shared_process_tree_scripts_directory) not in sys.path:
|
|
30
|
+
sys.path.insert(0, str(_shared_process_tree_scripts_directory))
|
|
31
|
+
|
|
32
|
+
from process_tree_kill import ( # noqa: E402
|
|
33
|
+
should_start_new_session,
|
|
34
|
+
terminate_process_tree,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
from codex_review_scripts_constants.codex_usage_probe_constants import ( # noqa: E402
|
|
27
38
|
ALL_WINDOWS_SCRIPT_SUFFIXES,
|
|
28
39
|
WINDOWS_COMMAND_SHELL,
|
|
29
40
|
WINDOWS_COMMAND_SHELL_RUN_FLAG,
|
|
30
41
|
WINDOWS_OS_NAME,
|
|
31
|
-
WINDOWS_TASKKILL_COMMAND,
|
|
32
|
-
WINDOWS_TASKKILL_FORCE_FLAG,
|
|
33
|
-
WINDOWS_TASKKILL_PID_FLAG,
|
|
34
|
-
WINDOWS_TASKKILL_TREE_FLAG,
|
|
35
42
|
)
|
|
36
|
-
from codex_review_scripts_constants.run_constants import (
|
|
43
|
+
from codex_review_scripts_constants.run_constants import ( # noqa: E402
|
|
37
44
|
ALL_SHAPE_PROBE_REQUIRED_FLAGS,
|
|
38
45
|
BASE_TARGET_FLAG,
|
|
39
46
|
CAPTURE_STREAMS_KEYWORD,
|
|
@@ -74,61 +81,6 @@ from codex_review_scripts_constants.run_constants import (
|
|
|
74
81
|
)
|
|
75
82
|
|
|
76
83
|
|
|
77
|
-
def _kill_windows_process_tree(process_identifier: int) -> None:
|
|
78
|
-
"""Kill a Windows process and every descendant it started, by PID.
|
|
79
|
-
|
|
80
|
-
Swallows taskkill failures so the caller can fall back to ``Popen.kill()``
|
|
81
|
-
and a timed drain. A raised ``TimeoutExpired`` here would replace the
|
|
82
|
-
original review-timeout exception and skip that drain path.
|
|
83
|
-
"""
|
|
84
|
-
try:
|
|
85
|
-
subprocess.run(
|
|
86
|
-
[
|
|
87
|
-
WINDOWS_TASKKILL_COMMAND,
|
|
88
|
-
WINDOWS_TASKKILL_TREE_FLAG,
|
|
89
|
-
WINDOWS_TASKKILL_FORCE_FLAG,
|
|
90
|
-
WINDOWS_TASKKILL_PID_FLAG,
|
|
91
|
-
str(process_identifier),
|
|
92
|
-
],
|
|
93
|
-
stdout=subprocess.DEVNULL,
|
|
94
|
-
stderr=subprocess.DEVNULL,
|
|
95
|
-
check=False,
|
|
96
|
-
timeout=PROCESS_TREE_KILL_TIMEOUT_SECONDS,
|
|
97
|
-
)
|
|
98
|
-
except (subprocess.TimeoutExpired, OSError):
|
|
99
|
-
return
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
def _kill_posix_process_group(process_identifier: int) -> None:
|
|
103
|
-
"""Kill a POSIX process group so no grandchild keeps the capture pipe open."""
|
|
104
|
-
try:
|
|
105
|
-
process_group_identifier = os.getpgid(process_identifier)
|
|
106
|
-
os.killpg(process_group_identifier, signal.SIGKILL)
|
|
107
|
-
except (ProcessLookupError, PermissionError):
|
|
108
|
-
return
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
def _terminate_process_tree(review_process: subprocess.Popen[str]) -> None:
|
|
112
|
-
"""Kill the review process and every descendant it spawned.
|
|
113
|
-
|
|
114
|
-
Tree kill first (taskkill /T or killpg). When the direct child is still
|
|
115
|
-
alive after that, fall back to ``Popen.kill()`` so ``Popen.__exit__`` never
|
|
116
|
-
hits an unbounded wait on a surviving process.
|
|
117
|
-
"""
|
|
118
|
-
if review_process.poll() is not None:
|
|
119
|
-
return
|
|
120
|
-
if os.name == WINDOWS_OS_NAME:
|
|
121
|
-
_kill_windows_process_tree(review_process.pid)
|
|
122
|
-
else:
|
|
123
|
-
_kill_posix_process_group(review_process.pid)
|
|
124
|
-
if review_process.poll() is not None:
|
|
125
|
-
return
|
|
126
|
-
try:
|
|
127
|
-
review_process.kill()
|
|
128
|
-
except ProcessLookupError:
|
|
129
|
-
return
|
|
130
|
-
|
|
131
|
-
|
|
132
84
|
def _open_codex_popen(
|
|
133
85
|
all_arguments: list[str],
|
|
134
86
|
all_keyword_arguments: dict[str, object],
|
|
@@ -147,7 +99,7 @@ def _open_codex_popen(
|
|
|
147
99
|
text=bool(all_keyword_arguments.get(TEXT_MODE_KEYWORD, False)),
|
|
148
100
|
encoding=stream_encoding if isinstance(stream_encoding, str) else None,
|
|
149
101
|
env=process_environment if isinstance(process_environment, dict) else None,
|
|
150
|
-
start_new_session=
|
|
102
|
+
start_new_session=should_start_new_session(),
|
|
151
103
|
)
|
|
152
104
|
|
|
153
105
|
|
|
@@ -198,7 +150,7 @@ def _communicate_with_tree_kill_on_timeout(
|
|
|
198
150
|
try:
|
|
199
151
|
return review_process.communicate(timeout=timeout_seconds)
|
|
200
152
|
except subprocess.TimeoutExpired:
|
|
201
|
-
|
|
153
|
+
terminate_process_tree(review_process)
|
|
202
154
|
_drain_process_after_tree_kill(review_process)
|
|
203
155
|
raise
|
|
204
156
|
|
|
@@ -34,9 +34,6 @@ from codex_review_scripts_constants.codex_usage_probe_constants import (
|
|
|
34
34
|
USAGE_REPORT_KEY_WINDOW_RESET,
|
|
35
35
|
WEEKLY_USAGE_GATE_THRESHOLD_PERCENT,
|
|
36
36
|
WEEKLY_WINDOW_DURATION_MINUTES,
|
|
37
|
-
WINDOWS_OS_NAME,
|
|
38
|
-
WINDOWS_TASKKILL_COMMAND,
|
|
39
|
-
WINDOWS_TASKKILL_TREE_FLAG,
|
|
40
37
|
)
|
|
41
38
|
|
|
42
39
|
PROBE_PATH = SCRIPTS_DIRECTORY / "codex_usage_probe.py"
|
|
@@ -300,58 +297,66 @@ class TestParseTextStatus:
|
|
|
300
297
|
|
|
301
298
|
|
|
302
299
|
class TestProcessTreeTeardown:
|
|
303
|
-
def
|
|
304
|
-
|
|
305
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
306
|
-
) -> None:
|
|
300
|
+
def should_end_a_running_app_server_and_reap_it(self) -> None:
|
|
301
|
+
"""A real sleeping child is gone once teardown returns, on any platform."""
|
|
307
302
|
probe = load_probe_module()
|
|
308
|
-
|
|
309
|
-
fake_process = subprocess.Popen(
|
|
303
|
+
sleeping_process = subprocess.Popen(
|
|
310
304
|
[sys.executable, "-c", "import time; time.sleep(30)"],
|
|
311
305
|
stdout=subprocess.DEVNULL,
|
|
312
306
|
stderr=subprocess.DEVNULL,
|
|
307
|
+
start_new_session=probe.should_start_new_session(),
|
|
313
308
|
)
|
|
314
309
|
try:
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
def capture_run(
|
|
318
|
-
command: list[str],
|
|
319
|
-
**_kwargs: object,
|
|
320
|
-
) -> subprocess.CompletedProcess[str]:
|
|
321
|
-
all_taskkill_commands.append(list(command))
|
|
322
|
-
fake_process.kill()
|
|
323
|
-
fake_process.wait(timeout=PROCESS_TREE_WAIT_SECONDS)
|
|
324
|
-
return subprocess.CompletedProcess(command, 0)
|
|
325
|
-
|
|
326
|
-
monkeypatch.setattr(probe.subprocess, "run", capture_run)
|
|
327
|
-
probe._terminate_process_tree(fake_process)
|
|
310
|
+
probe._terminate_process_tree(sleeping_process)
|
|
311
|
+
assert sleeping_process.poll() is not None
|
|
328
312
|
finally:
|
|
329
|
-
if
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
assert WINDOWS_TASKKILL_TREE_FLAG in all_taskkill_commands[0]
|
|
335
|
-
assert str(fake_process.pid) in all_taskkill_commands[0]
|
|
336
|
-
|
|
337
|
-
def should_kill_process_directly_when_os_name_is_not_windows(
|
|
313
|
+
if sleeping_process.poll() is None:
|
|
314
|
+
sleeping_process.kill()
|
|
315
|
+
sleeping_process.wait(timeout=PROCESS_TREE_WAIT_SECONDS)
|
|
316
|
+
|
|
317
|
+
def should_hand_the_whole_tree_to_the_shared_kill(
|
|
338
318
|
self,
|
|
339
319
|
monkeypatch: pytest.MonkeyPatch,
|
|
340
320
|
) -> None:
|
|
321
|
+
"""Teardown routes the server process through the shared tree kill.
|
|
322
|
+
|
|
323
|
+
::
|
|
324
|
+
|
|
325
|
+
shared tree kill ok: the cmd.exe shim's app-server child dies too
|
|
326
|
+
Popen.kill() flag: the app-server grandchild holds stdout open
|
|
327
|
+
"""
|
|
341
328
|
probe = load_probe_module()
|
|
342
|
-
|
|
343
|
-
|
|
329
|
+
all_killed_processes: list[object] = []
|
|
330
|
+
finished_process = subprocess.Popen(
|
|
331
|
+
[sys.executable, "-c", ""],
|
|
344
332
|
stdout=subprocess.DEVNULL,
|
|
345
333
|
stderr=subprocess.DEVNULL,
|
|
346
334
|
)
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
probe
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
335
|
+
finished_process.wait(timeout=PROCESS_TREE_WAIT_SECONDS)
|
|
336
|
+
monkeypatch.setattr(
|
|
337
|
+
probe,
|
|
338
|
+
"terminate_process_tree",
|
|
339
|
+
lambda target_process: all_killed_processes.append(target_process),
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
probe._terminate_process_tree(finished_process)
|
|
343
|
+
|
|
344
|
+
assert all_killed_processes == [finished_process]
|
|
345
|
+
|
|
346
|
+
def should_return_when_a_killed_child_never_reaps(
|
|
347
|
+
self,
|
|
348
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
349
|
+
) -> None:
|
|
350
|
+
"""A child that outlives its grace window ends teardown, never hangs it."""
|
|
351
|
+
probe = load_probe_module()
|
|
352
|
+
|
|
353
|
+
class _UnreapableProcess:
|
|
354
|
+
def wait(self, timeout: float | None = None) -> int:
|
|
355
|
+
raise subprocess.TimeoutExpired(cmd="codex", timeout=timeout or 0)
|
|
356
|
+
|
|
357
|
+
monkeypatch.setattr(probe, "terminate_process_tree", lambda _process: None)
|
|
358
|
+
|
|
359
|
+
probe._terminate_process_tree(_UnreapableProcess())
|
|
355
360
|
|
|
356
361
|
|
|
357
362
|
class TestReaderThreadLifecycle:
|
|
@@ -890,38 +890,6 @@ def test_run_command_kills_grandchild_tree_on_timeout_without_hanging(
|
|
|
890
890
|
assert _wait_until_process_stops(grandchild_identifier, wall_clock_ceiling_seconds)
|
|
891
891
|
|
|
892
892
|
|
|
893
|
-
def test_windows_process_tree_kill_builds_taskkill_argv(
|
|
894
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
895
|
-
) -> None:
|
|
896
|
-
"""The Windows kill path issues taskkill /T /F /PID for the given process id."""
|
|
897
|
-
target_process_identifier = 4242
|
|
898
|
-
recorded_argv: list[list[str]] = []
|
|
899
|
-
|
|
900
|
-
def record_argv(all_arguments: list[str], **_keywords: object) -> None:
|
|
901
|
-
recorded_argv.append(all_arguments)
|
|
902
|
-
|
|
903
|
-
monkeypatch.setattr(wrapper.subprocess, "run", record_argv)
|
|
904
|
-
wrapper._kill_windows_process_tree(target_process_identifier)
|
|
905
|
-
|
|
906
|
-
assert recorded_argv == [
|
|
907
|
-
["taskkill", "/T", "/F", "/PID", str(target_process_identifier)]
|
|
908
|
-
]
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
def test_windows_process_tree_kill_swallows_taskkill_timeout(
|
|
912
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
913
|
-
) -> None:
|
|
914
|
-
"""A hung taskkill must not raise — caller falls back to Popen.kill + drain."""
|
|
915
|
-
|
|
916
|
-
def raise_taskkill_timeout(
|
|
917
|
-
_all_arguments: list[str], **_keywords: object
|
|
918
|
-
) -> None:
|
|
919
|
-
raise subprocess.TimeoutExpired(cmd="taskkill", timeout=1)
|
|
920
|
-
|
|
921
|
-
monkeypatch.setattr(wrapper.subprocess, "run", raise_taskkill_timeout)
|
|
922
|
-
wrapper._kill_windows_process_tree(4242)
|
|
923
|
-
|
|
924
|
-
|
|
925
893
|
def test_drain_joins_pipes_when_direct_kill_raises_process_lookup_error() -> None:
|
|
926
894
|
"""Even when the process is already gone, drain still joins pipe readers."""
|
|
927
895
|
all_communicate_timeouts: list[float | None] = []
|
|
@@ -990,7 +958,7 @@ def test_run_command_surfaces_timeout_when_tree_kill_is_noop(
|
|
|
990
958
|
def leave_process_alive(_review_process: object) -> None:
|
|
991
959
|
return None
|
|
992
960
|
|
|
993
|
-
monkeypatch.setattr(wrapper, "
|
|
961
|
+
monkeypatch.setattr(wrapper, "terminate_process_tree", leave_process_alive)
|
|
994
962
|
start_time = time.monotonic()
|
|
995
963
|
with pytest.raises(subprocess.TimeoutExpired):
|
|
996
964
|
wrapper._run_command(
|
|
@@ -13,11 +13,12 @@ description: >-
|
|
|
13
13
|
|
|
14
14
|
## Gotchas
|
|
15
15
|
|
|
16
|
-
- **`low` stays single-pass.**
|
|
16
|
+
- **`low` stays single-pass.** No subagents, no full-file reads: one read pass per target item, one findings pass.
|
|
17
17
|
- **`medium` favors precision, `xhigh` favors recall.** At `medium` (8 angles) surface only findings a maintainer would act on. At `xhigh` (10 angles plus a gap sweep) a single non-REFUTED vote carries the finding; do not drop on uncertainty.
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
18
|
+
- **Every retained finding carries `severity` and `verdict`.** Severity is one of `blocker`, `high`, `medium`, `low`, `nit`. Verdict is `CONFIRMED` or `PLAUSIBLE`. Drop REFUTED candidates; never emit an unclassified retained finding.
|
|
19
|
+
- **`--fix` applies findings once.** Load `reference/fix.md` and follow it — it owns the fix agent, the code-rules gate, skip logging, and outcome reporting. Commits are lead-owned; fix agents never commit or push.
|
|
20
|
+
- **`loop` never asks.** A round with bug findings validates them with an advisor, fixes, and re-reviews. Terminals are exactly `clean`, `nits_fixed`, and `advisor_blocked`. There is no reviewed-head count limit; a new head increments the count once, a re-review of the same head does not. Load `reference/loop.md` and follow it.
|
|
21
|
+
- **`--fix` and `loop` combine.** With both, each loop round runs the level file, and the round's fixing happens inside `reference/loop.md`'s gate sequence, which loads `reference/fix.md` for the mechanics. There is no separate fix pass around the round.
|
|
21
22
|
|
|
22
23
|
## When this skill applies
|
|
23
24
|
|
|
@@ -31,8 +32,8 @@ Triggers: `/e-code-review <level> [--fix] [loop]`. `<level>` is `low`, `medium`,
|
|
|
31
32
|
|
|
32
33
|
1. Read `<level>` and the optional `--fix` and `loop` flags. Apply the refusal first.
|
|
33
34
|
2. Load `reference/low.md`, `reference/medium.md`, or `reference/xhigh.md`. Run that file as one review cycle, ending in its structured findings report.
|
|
34
|
-
3. If `--fix` is set, load `reference/fix.md` and apply it to that cycle's findings.
|
|
35
|
-
4. If `loop` is set, load `reference/loop.md` and follow it.
|
|
35
|
+
3. If `--fix` is set and `loop` is not set, load `reference/fix.md` and apply it to that cycle's findings. This path has no commit step: the fixes stay uncommitted in the working tree for the user to review and commit.
|
|
36
|
+
4. If `loop` is set, load `reference/loop.md` and follow it with that cycle's findings still unfixed. Round 1's findings are the ones step 2 already produced; from the second round on, the round re-runs that same level file end to end. End to end means the level file's review phases, up to and including its findings report — not its *Looping* section, which hands control to `loop.md` and would re-enter the loop the round is already inside. `loop.md`'s gate sequence owns when the round fixes.
|
|
36
37
|
5. Without `--fix` or `loop`, return the cycle findings and stop.
|
|
37
38
|
|
|
38
39
|
## File index
|
|
@@ -40,10 +41,10 @@ Triggers: `/e-code-review <level> [--fix] [loop]`. `<level>` is `low`, `medium`,
|
|
|
40
41
|
| File | Purpose |
|
|
41
42
|
|---|---|
|
|
42
43
|
| `SKILL.md` | Route by level; dispatch `--fix` and `loop` |
|
|
43
|
-
| `reference/low.md` | low review procedure — 1 diff pass, no verify |
|
|
44
|
+
| `reference/low.md` | low review procedure — 1 diff pass per target item, no verify |
|
|
44
45
|
| `reference/medium.md` | medium review procedure — 8 angles, 1-vote verify |
|
|
45
46
|
| `reference/xhigh.md` | xhigh review procedure — 10 angles, 1-vote verify, gap sweep |
|
|
46
|
-
| `reference/fix.md` | Fix application,
|
|
47
|
+
| `reference/fix.md` | Fix application, code-rules gate, skip logging, outcome reporting |
|
|
47
48
|
| `reference/loop.md` | Repeat review/fix rounds until clean |
|
|
48
49
|
|
|
49
50
|
## Folder map
|
|
@@ -12,16 +12,38 @@ At `low` — and any time the Agent tool is unavailable — no finder agent exis
|
|
|
12
12
|
to resume. Apply the fixes yourself, sequentially, in this context, holding to
|
|
13
13
|
every rule below.
|
|
14
14
|
|
|
15
|
-
##
|
|
15
|
+
## Code-rules gate
|
|
16
16
|
|
|
17
|
-
Before returning, the resumed agent
|
|
17
|
+
Before returning, the resumed agent runs the code-rules gate with the same bare
|
|
18
|
+
call every other surface in this skill uses:
|
|
18
19
|
|
|
19
20
|
1. Run `~/.claude/_shared/pr-loop/scripts/code_rules_gate.py --repo-root <repo
|
|
20
|
-
root
|
|
21
|
-
2. If the gate reports violations
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
root>` with no file paths and no `--only-under` prefix.
|
|
22
|
+
2. If the gate reports violations **on lines this fix already owns** (the files
|
|
23
|
+
and added lines the fix changed), fix them and re-run the exact same command.
|
|
24
|
+
3. A violation on a path or added line outside this fix's own work is reported
|
|
25
|
+
or skipped — it is not force-fixed into unrelated files. Log each skip with
|
|
26
|
+
the path and reason, then continue.
|
|
27
|
+
4. Repeat until every violation on this fix's own work is clean (or skipped with
|
|
28
|
+
a logged reason for out-of-scope hits).
|
|
29
|
+
5. Only after that result does the agent return control and report its outcome.
|
|
30
|
+
|
|
31
|
+
A file path named on the command line puts the gate in whole-file scope and can
|
|
32
|
+
churn on untouched lines; keep the call bare.
|
|
33
|
+
|
|
34
|
+
This gate commits nothing. The fix lands in the working tree and stays
|
|
35
|
+
uncommitted. **Commit ownership is lead-only:** a fix agent, patch worker, or
|
|
36
|
+
resumed finding agent never runs `git commit` or `git push`. The lead that owns
|
|
37
|
+
the branch creates the commit after gates pass. The bare code-rules call covers
|
|
38
|
+
the merge-base surface and staged added lines only — working-tree-only lines
|
|
39
|
+
enter that scope after the lead stages or commits them.
|
|
40
|
+
|
|
41
|
+
Under a bare `--fix` — this document invoked without `loop` — nothing
|
|
42
|
+
downstream commits either, and that is the intended outcome: a one-shot fix
|
|
43
|
+
pass leaves its fixes uncommitted in the working tree for the lead to review
|
|
44
|
+
and commit (stage first if you re-run the gate on those lines). Under `loop`,
|
|
45
|
+
the round tail in `reference\loop.md` stages and runs required checks; the lead
|
|
46
|
+
still owns the commit and push.
|
|
25
47
|
|
|
26
48
|
## Skip candidates
|
|
27
49
|
|