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.
Files changed (88) 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/pr-loop/worker-spawn.md +3 -1
  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/code-quality-agent.md +6 -5
  13. package/agents/deep-research.md +7 -24
  14. package/agents/docs-agent.md +1 -27
  15. package/agents/issue-tracker.md +1 -7
  16. package/agents/skill-writer-agent.md +1 -2
  17. package/agents/test_agent_frontmatter.py +309 -12
  18. package/hooks/blocking/CLAUDE.md +2 -0
  19. package/hooks/blocking/fable_spawn_gate.py +187 -0
  20. package/hooks/blocking/piped_pytest_blocker.py +1223 -0
  21. package/hooks/blocking/plain_language_blocker.py +287 -15
  22. package/hooks/blocking/test_fable_spawn_gate.py +374 -0
  23. package/hooks/blocking/test_piped_pytest_blocker.py +587 -0
  24. package/hooks/blocking/test_plain_language_blocker.py +277 -2
  25. package/hooks/blocking/test_pre_tool_use_dispatcher.py +34 -2
  26. package/hooks/git-hooks/CLAUDE.md +2 -2
  27. package/hooks/git-hooks/git_hooks_constants/__init__.py +28 -0
  28. package/hooks/git-hooks/pre_push.py +343 -54
  29. package/hooks/git-hooks/test_pre_push.py +852 -6
  30. package/hooks/hooks.json +9 -19
  31. package/hooks/hooks_constants/CLAUDE.md +2 -0
  32. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +1 -0
  33. package/hooks/hooks_constants/fable_spawn_gate_constants.py +62 -0
  34. package/hooks/hooks_constants/piped_pytest_blocker_constants.py +360 -0
  35. package/hooks/hooks_constants/plain_language_blocker_constants.py +64 -1
  36. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +10 -0
  37. package/hooks/hooks_constants/shell_command_segments.py +1 -1
  38. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +1 -0
  39. package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +27 -0
  40. package/hooks/hooks_constants/test_prose_metrics_parity.py +124 -0
  41. package/package.json +1 -1
  42. package/rules/CLAUDE.md +1 -0
  43. package/rules/ask-user-question-required.md +26 -0
  44. package/rules/claims-as-quotes.md +65 -0
  45. package/scripts/CLAUDE.md +4 -4
  46. package/scripts/_code_review_test_support.py +6 -0
  47. package/scripts/check.ps1 +18 -5
  48. package/scripts/claude_chain_runner.py +203 -31
  49. package/scripts/codec_forwarding_test_support.py +2 -0
  50. package/scripts/dev_env_scripts_constants/CLAUDE.md +4 -4
  51. package/scripts/dev_env_scripts_constants/claude_chain_constants.py +38 -0
  52. package/scripts/dev_env_scripts_constants/code_review_constants.py +403 -2
  53. package/scripts/dev_env_scripts_constants/grok_worker_constants.py +83 -13
  54. package/scripts/grok_headless_runner.py +148 -18
  55. package/scripts/resolve_worker_spawn.py +56 -10
  56. package/scripts/spawn_grok_batch.py +81 -23
  57. package/scripts/test_claude_chain_runner.py +358 -0
  58. package/scripts/test_grok_headless_runner.py +547 -10
  59. package/scripts/test_invoke_code_review.py +298 -0
  60. package/scripts/test_resolve_worker_spawn.py +185 -15
  61. package/scripts/test_spawn_grok_batch.py +326 -22
  62. package/scripts/tests/CLAUDE.md +1 -0
  63. package/scripts/tests/test_grok_worker_constants.py +59 -0
  64. package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +46 -0
  65. package/skills/autoconverge/workflow/converge.contract.test.mjs +133 -8
  66. package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +107 -1
  67. package/skills/autoconverge/workflow/converge.mjs +113 -31
  68. package/skills/codex-review/scripts/codex_review_scripts_constants/codex_usage_probe_constants.py +0 -4
  69. package/skills/codex-review/scripts/codex_usage_probe.py +20 -33
  70. package/skills/codex-review/scripts/run_codex_review.py +16 -64
  71. package/skills/codex-review/scripts/test_codex_usage_probe.py +46 -41
  72. package/skills/codex-review/scripts/test_run_codex_review.py +1 -33
  73. package/skills/e-code-review/SKILL.md +9 -8
  74. package/skills/e-code-review/reference/fix.md +29 -7
  75. package/skills/e-code-review/reference/loop.md +230 -14
  76. package/skills/e-code-review/reference/low.md +33 -15
  77. package/skills/e-code-review/reference/medium.md +55 -21
  78. package/skills/e-code-review/reference/xhigh.md +30 -12
  79. package/skills/fresh-branch/CLAUDE.md +5 -5
  80. package/skills/fresh-branch/SKILL.md +14 -6
  81. package/skills/fresh-branch/scripts/create_fresh_branch.py +122 -39
  82. package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +10 -3
  83. package/skills/fresh-branch/scripts/test_create_fresh_branch.py +251 -0
  84. package/skills/grok-spawn/SKILL.md +10 -3
  85. package/skills/grok-spawn/reference/flag-profiles.md +3 -1
  86. package/skills/orchestrator/SKILL.md +4 -1
  87. package/skills/orchestrator-refresh/SKILL.md +5 -1
  88. 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
- from codex_review_scripts_constants.codex_usage_probe_constants import (
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. ``taskkill /T``
395
- tears down the whole tree; other platforms use ``kill()``.
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
- if server_process.poll() is not None:
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
- pass
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
- from codex_review_scripts_constants.codex_usage_probe_constants import (
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=os.name != WINDOWS_OS_NAME,
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
- _terminate_process_tree(review_process)
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 should_run_taskkill_tree_when_os_name_is_windows(
304
- self,
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
- all_taskkill_commands: list[list[str]] = []
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
- monkeypatch.setattr(probe.os, "name", WINDOWS_OS_NAME)
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 fake_process.poll() is None:
330
- fake_process.kill()
331
- fake_process.wait(timeout=PROCESS_TREE_WAIT_SECONDS)
332
- assert len(all_taskkill_commands) == 1
333
- assert all_taskkill_commands[0][0] == WINDOWS_TASKKILL_COMMAND
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
- fake_process = subprocess.Popen(
343
- [sys.executable, "-c", "import time; time.sleep(30)"],
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
- try:
348
- monkeypatch.setattr(probe.os, "name", "posix")
349
- probe._terminate_process_tree(fake_process)
350
- assert fake_process.poll() is not None
351
- finally:
352
- if fake_process.poll() is None:
353
- fake_process.kill()
354
- fake_process.wait(timeout=PROCESS_TREE_WAIT_SECONDS)
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, "_terminate_process_tree", leave_process_alive)
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.** Do not spawn subagents. One diff read, one findings 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
- - **`--fix` applies findings once.** Load `reference/fix.md` and follow it it owns the fix agent, the commit gate, skip logging, and outcome reporting.
19
- - **`loop` never asks.** After findings, fix nits or stop on bugs. Load `reference/loop.md` and follow it.
20
- - **`--fix` and `loop` combine.** With both, each loop round runs the level file, then `reference/fix.md`, then re-reviews.
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. Each round re-runs the same level file from step 2, plus step 3 when `--fix` is set.
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, commit gate, skip logging, outcome reporting |
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
- ## Commit gate
15
+ ## Code-rules gate
16
16
 
17
- Before returning, the resumed agent attempts a commit on its own change:
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> <changed/added files>` against every file it changed or added.
21
- 2. If the gate reports violations, fix them and re-run the exact same command.
22
- 3. Repeat until the gate returns clean.
23
- 4. Only after a clean gate result does the agent return control and report
24
- its outcome.
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