claude-dev-env 2.32.0 → 2.33.1

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 (97) hide show
  1. package/.agents/agents/clean-coder.md +8 -7
  2. package/.agents/agents/code-quality-agent.md +4 -2
  3. package/.agents/agents/pr-description-writer.md +8 -3
  4. package/.agents/agents/reference/pr-description-illustrative-voice.md +6 -1
  5. package/.agents/agents/test_agent_frontmatter.py +0 -9
  6. package/.agents/agents/test_pr_description_writer_style.py +24 -1
  7. package/.agents/skills/e-simplify/SKILL.md +1 -15
  8. package/.agents/skills/grok-spawn/reference/worker-briefs.md +4 -0
  9. package/.agents/skills/pr-refinement/SKILL.md +2 -2
  10. package/.agents/skills/pr-shared-extraction/SKILL.md +2 -2
  11. package/.agents/skills/pr-title-description/SKILL.md +6 -2
  12. package/.agents/skills/review-tier/SKILL.md +1 -1
  13. package/.agents/skills/review-tier/reference/tier-policy.json +2 -2
  14. package/.agents/skills/review-tier/scripts/review_tier.py +31 -4
  15. package/.agents/skills/review-tier/scripts/test_generation.py +7 -19
  16. package/.agents/skills/review-tier/scripts/test_review_tier.py +11 -11
  17. package/AGENTS.md +83 -0
  18. package/_shared/pr-loop/code-rules-gate.md +5 -2
  19. package/_shared/pr-loop/scripts/code_rules_gate.py +7 -3
  20. package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_arguments.py +13 -1
  21. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_gate_arguments.py +7 -0
  22. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +47 -1
  23. package/audit-rubrics/audit-categories.json +2 -2
  24. package/audit-rubrics/category_rubrics/category-e-dead-code.md +1 -1
  25. package/audit-rubrics/category_rubrics/category-j-code-rules-compliance.md +5 -3
  26. package/audit-rubrics/category_rubrics/category-l-behavior-equivalence.md +6 -3
  27. package/audit-rubrics/prompts/category-e-dead-code.md +7 -7
  28. package/audit-rubrics/prompts/category-j-code-rules-compliance.md +25 -19
  29. package/audit-rubrics/prompts/category-l-behavior-equivalence.md +28 -25
  30. package/docs/CODE_RULES.md +7 -8
  31. package/docs/REACT_PATTERNS.md +1 -1
  32. package/hooks/blocking/code_rules_comments.py +118 -59
  33. package/hooks/blocking/code_rules_enforcer.py +19 -7
  34. package/hooks/blocking/code_rules_type_escape.py +16 -23
  35. package/hooks/blocking/javascript_comment_scanner.py +99 -0
  36. package/hooks/blocking/test_code_rules_enforcer_any_type_ignore.py +24 -4
  37. package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +0 -1
  38. package/hooks/blocking/test_code_rules_enforcer_comment_string_awareness.py +190 -3
  39. package/hooks/blocking/test_code_rules_enforcer_javascript_comments.py +44 -0
  40. package/hooks/blocking/test_code_rules_enforcer_join_separator_magic.py +0 -1
  41. package/hooks/git-hooks/pull_request_handoff.py +1 -1
  42. package/hooks/git-hooks/test_post_commit.py +8 -0
  43. package/hooks/hooks_constants/stale_comment_reference_blocker_constants.py +3 -4
  44. package/hooks/hooks_constants/test_unused_module_import_constants.py +9 -1
  45. package/hooks/hooks_constants/unused_module_import_constants.py +1 -1
  46. package/hooks/validators/comment_checks.py +2 -2
  47. package/hooks/validators/fast_save_validators.py +0 -9
  48. package/hooks/validators/git_checks.py +0 -84
  49. package/hooks/validators/run_all_validators.py +2 -9
  50. package/hooks/validators/test_fast_save_validators.py +2 -8
  51. package/hooks/validators/test_git_checks.py +0 -249
  52. package/hooks/validators/test_todo_checks.py +1 -0
  53. package/hooks/validators/todo_checks.py +1 -1
  54. package/package.json +1 -1
  55. package/rules/git-workflow.md +0 -8
  56. package/scripts/check.ps1 +35 -1
  57. package/scripts/codex_compat_materializer.py +1 -0
  58. package/scripts/tests/test_rules.py +128 -0
  59. package/system-prompts/software-engineer.xml +6 -19
  60. package/.agents/skills-archived/split-pr/SKILL.md +0 -85
  61. package/.agents/skills-archived/split-pr/reference/path-layers.md +0 -16
  62. package/.agents/skills-archived/split-pr/reference/proposal-format.md +0 -15
  63. package/.agents/skills-archived/split-pr/reference/split-further-loop.md +0 -10
  64. package/.agents/skills-archived/split-pr/reference/splitting-principles.md +0 -26
  65. package/.agents/skills-archived/split-pr/scripts/analyze_pr.py +0 -279
  66. package/.agents/skills-archived/split-pr/scripts/categorize_files.py +0 -106
  67. package/.agents/skills-archived/split-pr/scripts/config/__init__.py +0 -1
  68. package/.agents/skills-archived/split-pr/scripts/config/dependency_constants.py +0 -14
  69. package/.agents/skills-archived/split-pr/scripts/config/git_operations_constants.py +0 -36
  70. package/.agents/skills-archived/split-pr/scripts/config/packing_constants.py +0 -61
  71. package/.agents/skills-archived/split-pr/scripts/config/plan_constants.py +0 -49
  72. package/.agents/skills-archived/split-pr/scripts/config/split_pr_constants.py +0 -110
  73. package/.agents/skills-archived/split-pr/scripts/execute_split_slices.py +0 -82
  74. package/.agents/skills-archived/split-pr/scripts/pack_files_into_slices.py +0 -212
  75. package/.agents/skills-archived/split-pr/scripts/split_pr_dependency_graph.py +0 -70
  76. package/.agents/skills-archived/split-pr/scripts/split_pr_git_operations.py +0 -184
  77. package/.agents/skills-archived/split-pr/scripts/split_pr_layer_order.py +0 -58
  78. package/.agents/skills-archived/split-pr/scripts/split_pr_paginate.py +0 -119
  79. package/.agents/skills-archived/split-pr/scripts/split_pr_process_runner.py +0 -52
  80. package/.agents/skills-archived/split-pr/scripts/split_pr_script_types.py +0 -126
  81. package/.agents/skills-archived/split-pr/scripts/split_pr_title.py +0 -41
  82. package/.agents/skills-archived/split-pr/scripts/test_analyze_pr.py +0 -228
  83. package/.agents/skills-archived/split-pr/scripts/test_categorize_files.py +0 -55
  84. package/.agents/skills-archived/split-pr/scripts/test_categorize_files_packing.py +0 -59
  85. package/.agents/skills-archived/split-pr/scripts/test_execute_split_slices.py +0 -99
  86. package/.agents/skills-archived/split-pr/scripts/test_split_pr_dependency_graph.py +0 -47
  87. package/.agents/skills-archived/split-pr/scripts/test_split_pr_git_operations.py +0 -125
  88. package/.agents/skills-archived/split-pr/scripts/test_split_pr_layer_order.py +0 -36
  89. package/.agents/skills-archived/split-pr/scripts/test_split_pr_paginate.py +0 -65
  90. package/.agents/skills-archived/split-pr/scripts/test_split_pr_script_types.py +0 -73
  91. package/.agents/skills-archived/split-pr/scripts/test_split_pr_title.py +0 -28
  92. package/.agents/skills-archived/split-pr/scripts/test_verify_dependency_graph.py +0 -46
  93. package/.agents/skills-archived/split-pr/scripts/test_verify_plan.py +0 -56
  94. package/.agents/skills-archived/split-pr/scripts/test_verify_plan_contract.py +0 -50
  95. package/.agents/skills-archived/split-pr/scripts/test_verify_plan_path_normalization.py +0 -45
  96. package/.agents/skills-archived/split-pr/scripts/verify_dependency_graph.py +0 -111
  97. package/.agents/skills-archived/split-pr/scripts/verify_plan.py +0 -139
@@ -1,215 +1,17 @@
1
1
  """Tests for git and GitHub validation checks."""
2
2
 
3
- import json
4
3
  import subprocess
5
- from pathlib import Path
6
- from typing import List
7
4
  from unittest.mock import MagicMock, patch
8
5
 
9
6
  import pytest
10
7
 
11
8
  from .git_checks import (
12
9
  Violation,
13
- check_single_commit_when_pr_exists,
14
10
  check_draft_pr_state,
15
11
  main,
16
12
  )
17
13
 
18
14
 
19
- class TestSingleCommitWhenPrExists:
20
- """Test that PR branches have exactly 1 commit ahead of base."""
21
-
22
- @patch("validators.git_checks.subprocess.run")
23
- def test_no_pr_returns_empty(self, mock_run: MagicMock) -> None:
24
- """When no PR exists, check should return empty list."""
25
- mock_run.side_effect = [
26
- MagicMock(returncode=0, stdout="feature/my-branch\n", stderr=""),
27
- MagicMock(returncode=0, stdout="[]", stderr=""),
28
- ]
29
-
30
- violations = check_single_commit_when_pr_exists()
31
-
32
- assert violations == []
33
-
34
- @patch("validators.git_checks.subprocess.run")
35
- def test_single_commit_ahead_passes(self, mock_run: MagicMock) -> None:
36
- """Exactly 1 commit ahead should pass."""
37
- mock_run.side_effect = [
38
- MagicMock(returncode=0, stdout="feature/my-branch\n", stderr=""),
39
- MagicMock(returncode=0, stdout='[{"baseRefName": "main", "number": 123}]', stderr=""),
40
- MagicMock(returncode=0, stdout="1", stderr=""),
41
- ]
42
-
43
- violations = check_single_commit_when_pr_exists()
44
-
45
- assert violations == []
46
-
47
- @patch("validators.git_checks.subprocess.run")
48
- def test_zero_commits_ahead_fails(self, mock_run: MagicMock) -> None:
49
- """Zero commits ahead should fail."""
50
- mock_run.side_effect = [
51
- MagicMock(returncode=0, stdout="feature/my-branch\n", stderr=""),
52
- MagicMock(returncode=0, stdout='[{"baseRefName": "main", "number": 123}]', stderr=""),
53
- MagicMock(returncode=0, stdout="0", stderr=""),
54
- ]
55
-
56
- violations = check_single_commit_when_pr_exists()
57
-
58
- assert len(violations) == 1
59
- assert violations[0].file == ""
60
- assert violations[0].line == 0
61
- assert "exactly 1 commit" in violations[0].message
62
- assert "0 commits" in violations[0].message
63
-
64
- @patch("validators.git_checks.subprocess.run")
65
- def test_multiple_commits_ahead_fails(self, mock_run: MagicMock) -> None:
66
- """More than 1 commit ahead should fail."""
67
- mock_run.side_effect = [
68
- MagicMock(returncode=0, stdout="feature/my-branch\n", stderr=""),
69
- MagicMock(returncode=0, stdout='[{"baseRefName": "main", "number": 123}]', stderr=""),
70
- MagicMock(returncode=0, stdout="3", stderr=""),
71
- ]
72
-
73
- violations = check_single_commit_when_pr_exists()
74
-
75
- assert len(violations) == 1
76
- assert "exactly 1 commit" in violations[0].message
77
- assert "3 commits" in violations[0].message
78
-
79
- @patch("validators.git_checks.subprocess.run")
80
- def test_gh_cli_not_available_returns_empty(self, mock_run: MagicMock) -> None:
81
- """When gh CLI not available, should return empty (warning, not failure)."""
82
- mock_run.side_effect = [
83
- MagicMock(returncode=0, stdout="feature/my-branch\n", stderr=""),
84
- FileNotFoundError("gh not found"),
85
- ]
86
-
87
- violations = check_single_commit_when_pr_exists()
88
-
89
- assert violations == []
90
-
91
- @patch("validators.git_checks.subprocess.run")
92
- def test_git_not_available_returns_empty(self, mock_run: MagicMock) -> None:
93
- """When git not available, should return empty."""
94
- mock_run.side_effect = [
95
- MagicMock(returncode=0, stdout="feature/my-branch\n", stderr=""),
96
- MagicMock(returncode=0, stdout='[{"baseRefName": "main", "number": 123}]', stderr=""),
97
- FileNotFoundError("git not found"),
98
- ]
99
-
100
- violations = check_single_commit_when_pr_exists()
101
-
102
- assert violations == []
103
-
104
- @patch("validators.git_checks.subprocess.run")
105
- def test_extracts_base_branch_from_pr_info(self, mock_run: MagicMock) -> None:
106
- """Should extract base branch name from gh pr list JSON output, falling back to main when absent."""
107
- mock_run.side_effect = [
108
- MagicMock(returncode=0, stdout="feature/my-branch\n", stderr=""),
109
- MagicMock(returncode=0, stdout='[{"baseRefName": "develop", "number": 123}]', stderr=""),
110
- MagicMock(returncode=0, stdout="2", stderr=""),
111
- ]
112
-
113
- violations = check_single_commit_when_pr_exists()
114
-
115
- assert len(violations) == 1
116
- mock_run.assert_any_call(
117
- ["git", "rev-list", "--count", "develop..HEAD"],
118
- capture_output=True,
119
- text=True,
120
- check=True,
121
- timeout=30,
122
- )
123
-
124
- mock_run.reset_mock()
125
- mock_run.side_effect = [
126
- MagicMock(returncode=0, stdout="feature/my-branch\n", stderr=""),
127
- MagicMock(returncode=0, stdout='[{"number": 123}]', stderr=""),
128
- MagicMock(returncode=0, stdout="2", stderr=""),
129
- ]
130
-
131
- fallback_violations = check_single_commit_when_pr_exists()
132
-
133
- assert len(fallback_violations) == 1
134
- assert "main" in fallback_violations[0].message
135
- mock_run.assert_any_call(
136
- ["git", "rev-list", "--count", "main..HEAD"],
137
- capture_output=True,
138
- text=True,
139
- check=True,
140
- timeout=30,
141
- )
142
-
143
- @patch("validators.git_checks.subprocess.run")
144
- def test_non_numeric_commit_count_returns_empty(self, mock_run: MagicMock) -> None:
145
- """When git rev-list returns non-numeric output, should return empty."""
146
- mock_run.side_effect = [
147
- MagicMock(returncode=0, stdout="feature/my-branch\n", stderr=""),
148
- MagicMock(returncode=0, stdout='[{"baseRefName": "main", "number": 123}]', stderr=""),
149
- MagicMock(returncode=0, stdout="not a number\n", stderr=""),
150
- ]
151
-
152
- violations = check_single_commit_when_pr_exists()
153
-
154
- assert violations == []
155
-
156
- @patch("validators.git_checks.subprocess.run")
157
- def test_gh_timeout_returns_empty(self, mock_run: MagicMock) -> None:
158
- """When gh CLI times out, should return empty (warning, not failure)."""
159
- mock_run.side_effect = [
160
- MagicMock(returncode=0, stdout="feature/my-branch\n", stderr=""),
161
- subprocess.TimeoutExpired(cmd=["gh", "pr", "list"], timeout=30),
162
- ]
163
-
164
- violations = check_single_commit_when_pr_exists()
165
-
166
- assert violations == []
167
-
168
- @patch("validators.git_checks.subprocess.run")
169
- def test_git_timeout_returns_empty(self, mock_run: MagicMock) -> None:
170
- """When git times out, should return empty (warning, not failure)."""
171
- mock_run.side_effect = [
172
- MagicMock(returncode=0, stdout="feature/my-branch\n", stderr=""),
173
- MagicMock(returncode=0, stdout='[{"baseRefName": "main", "number": 123}]', stderr=""),
174
- subprocess.TimeoutExpired(cmd=["git", "rev-list"], timeout=30),
175
- ]
176
-
177
- violations = check_single_commit_when_pr_exists()
178
-
179
- assert violations == []
180
-
181
- @patch("validators.git_checks.subprocess.run")
182
- def test_passes_resolved_branch_name_to_gh(self, mock_run: MagicMock) -> None:
183
- """gh pr list must receive the resolved branch name, never the literal 'HEAD'."""
184
- mock_run.side_effect = [
185
- MagicMock(returncode=0, stdout="feature/my-branch\n", stderr=""),
186
- MagicMock(returncode=0, stdout='[{"baseRefName": "main", "number": 123}]', stderr=""),
187
- MagicMock(returncode=0, stdout="1", stderr=""),
188
- ]
189
-
190
- check_single_commit_when_pr_exists()
191
-
192
- mock_run.assert_any_call(
193
- ["gh", "pr", "list", "--head", "feature/my-branch", "--json", "baseRefName,number"],
194
- capture_output=True,
195
- text=True,
196
- check=True,
197
- timeout=30,
198
- )
199
-
200
- @patch("validators.git_checks.subprocess.run")
201
- def test_unresolved_branch_returns_empty(self, mock_run: MagicMock) -> None:
202
- """When current branch cannot be resolved, should return empty."""
203
- mock_run.side_effect = [
204
- MagicMock(returncode=0, stdout="\n", stderr=""),
205
- ]
206
-
207
- violations = check_single_commit_when_pr_exists()
208
-
209
- assert violations == []
210
- mock_run.assert_called_once()
211
-
212
-
213
15
  class TestDraftPrState:
214
16
  """Test that PR is in draft state when pushing review fixes."""
215
17
 
@@ -274,16 +76,13 @@ class TestDraftPrState:
274
76
  class TestMain:
275
77
  """Test main function integration."""
276
78
 
277
- @patch("validators.git_checks.check_single_commit_when_pr_exists")
278
79
  @patch("validators.git_checks.check_draft_pr_state")
279
80
  def test_main_no_violations_exits_zero(
280
81
  self,
281
82
  mock_draft: MagicMock,
282
- mock_commit: MagicMock,
283
83
  capsys,
284
84
  ) -> None:
285
85
  """main() should exit 0 when no violations found."""
286
- mock_commit.return_value = []
287
86
  mock_draft.return_value = []
288
87
 
289
88
  with pytest.raises(SystemExit) as exc_info:
@@ -293,37 +92,13 @@ class TestMain:
293
92
  captured = capsys.readouterr()
294
93
  assert captured.out == ""
295
94
 
296
- @patch("validators.git_checks.check_single_commit_when_pr_exists")
297
- @patch("validators.git_checks.check_draft_pr_state")
298
- def test_main_with_violations_exits_one(
299
- self,
300
- mock_draft: MagicMock,
301
- mock_commit: MagicMock,
302
- capsys,
303
- ) -> None:
304
- """main() should exit 1 and print violations when found."""
305
- mock_commit.return_value = [
306
- Violation(file="", line=0, message="Branch has 3 commits ahead")
307
- ]
308
- mock_draft.return_value = []
309
-
310
- with pytest.raises(SystemExit) as exc_info:
311
- main()
312
-
313
- assert exc_info.value.code == 1
314
- captured = capsys.readouterr()
315
- assert "Branch has 3 commits ahead" in captured.out
316
-
317
- @patch("validators.git_checks.check_single_commit_when_pr_exists")
318
95
  @patch("validators.git_checks.check_draft_pr_state")
319
96
  def test_main_prints_violations_without_file_line(
320
97
  self,
321
98
  mock_draft: MagicMock,
322
- mock_commit: MagicMock,
323
99
  capsys,
324
100
  ) -> None:
325
101
  """main() should print git violations without file:line: prefix."""
326
- mock_commit.return_value = []
327
102
  mock_draft.return_value = [
328
103
  Violation(file="", line=0, message="PR must be in draft state")
329
104
  ]
@@ -335,27 +110,3 @@ class TestMain:
335
110
  captured = capsys.readouterr()
336
111
  assert captured.out == "PR must be in draft state\n"
337
112
  assert ":0:" not in captured.out
338
-
339
- @patch("validators.git_checks.check_single_commit_when_pr_exists")
340
- @patch("validators.git_checks.check_draft_pr_state")
341
- def test_main_prints_all_violations(
342
- self,
343
- mock_draft: MagicMock,
344
- mock_commit: MagicMock,
345
- capsys,
346
- ) -> None:
347
- """main() should print all violations."""
348
- mock_commit.return_value = [
349
- Violation(file="", line=0, message="Branch has 2 commits")
350
- ]
351
- mock_draft.return_value = [
352
- Violation(file="", line=0, message="PR not in draft")
353
- ]
354
-
355
- with pytest.raises(SystemExit) as exc_info:
356
- main()
357
-
358
- assert exc_info.value.code == 1
359
- captured = capsys.readouterr()
360
- assert "Branch has 2 commits" in captured.out
361
- assert "PR not in draft" in captured.out
@@ -27,6 +27,7 @@ class TestUntrackedTodos:
27
27
  violations = check_untracked_todos(code, "test.py")
28
28
  assert len(violations) == 1
29
29
  assert "TODO" in violations[0].message
30
+ assert "remove it from source code and track the work outside the file" in violations[0].message
30
31
 
31
32
  def test_todo_with_issue_passes(self) -> None:
32
33
  code = "# TODO #123: fix this later\ndef foo(): pass"
@@ -24,7 +24,7 @@ def check_untracked_todos(source: str, filename: str) -> List[Violation]:
24
24
  Violation(
25
25
  filename,
26
26
  line_num,
27
- "TODO/FIXME without issue reference - add #<issue_number>",
27
+ "TODO/FIXME marker found - remove it from source code and track the work outside the file",
28
28
  )
29
29
  )
30
30
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "2.32.0",
3
+ "version": "2.33.1",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {
@@ -25,13 +25,6 @@ User-level rule: applies to **every** git repo that uses GitHub with `gh` (no ex
25
25
  - Before push: `gh pr ready --undo`
26
26
  - After review approved: `gh pr ready`
27
27
 
28
- 2. **ONE COMMIT PER REVIEW STAGE**: Each review round gets exactly ONE commit
29
- - Initial feature: 1 commit
30
- - After review #1: 2 commits (initial + review #1 fixes)
31
- - After review #2: 3 commits (initial + review #1 fixes + review #2 fixes)
32
- - NEVER squash multiple review stages into one commit
33
- - NEVER have multiple commits for the same review stage
34
-
35
28
  ## Never Commit Working Documents or Images
36
29
 
37
30
  **NEVER commit these files to the repo:**
@@ -53,7 +46,6 @@ An image a PR needs as visual evidence is not an exception to that row. Upload i
53
46
  2. Create a TodoWrite checklist with one item per comment.
54
47
  3. Fix systematically, marking each todo complete.
55
48
  4. Reply to each comment inline.
56
- 5. Create one review-fix commit. Do not squash it with the original.
57
49
 
58
50
  Repair only reported findings, then re-verify after every repair.
59
51
 
package/scripts/check.ps1 CHANGED
@@ -25,6 +25,9 @@
25
25
  .PARAMETER SkipRuff
26
26
  Skip the ruff run.
27
27
 
28
+ .PARAMETER CommentPolicyBase
29
+ Optional git reference used as the comment-policy baseline.
30
+
28
31
  .OUTPUTS
29
32
  Per-tool status lines on stdout. Final summary line:
30
33
  CHECK: OK
@@ -34,12 +37,14 @@
34
37
  param(
35
38
  [switch]$SkipTests,
36
39
  [switch]$SkipMypy,
37
- [switch]$SkipRuff
40
+ [switch]$SkipRuff,
41
+ [string]$CommentPolicyBase
38
42
  )
39
43
 
40
44
  $ErrorActionPreference = 'Stop'
41
45
 
42
46
  $hooksRoot = Resolve-Path (Join-Path $PSScriptRoot '..' 'hooks')
47
+ $repositoryRoot = Resolve-Path (Join-Path $PSScriptRoot '..' '..' '..')
43
48
  $blockingRoot = Join-Path $hooksRoot 'blocking'
44
49
  $prLoopScriptsRoot = Resolve-Path (Join-Path $PSScriptRoot '..' '_shared' 'pr-loop' 'scripts')
45
50
  $processTreeScriptsRoot = Resolve-Path (Join-Path $PSScriptRoot '..' '_shared' 'process-tree' 'scripts')
@@ -47,6 +52,26 @@ $processTreeScriptsRoot = Resolve-Path (Join-Path $PSScriptRoot '..' '_shared' '
47
52
  $failedTools = @()
48
53
  $firstNonZeroExitCode = 0
49
54
 
55
+ function Resolve-CommentPolicyBase {
56
+ param([string]$ExplicitBase)
57
+ if ($ExplicitBase) {
58
+ $resolvedExplicitBase = & git -C $repositoryRoot rev-parse --verify --quiet "${ExplicitBase}^{commit}" 2>$null
59
+ if ($LASTEXITCODE -eq 0 -and $resolvedExplicitBase) {
60
+ return $ExplicitBase
61
+ }
62
+ throw "Comment-policy baseline '$ExplicitBase' does not resolve in the repository."
63
+ }
64
+ foreach ($candidateBase in @('origin/main', 'HEAD^1')) {
65
+ $resolvedCandidateBase = & git -C $repositoryRoot rev-parse --verify --quiet "${candidateBase}^{commit}" 2>$null
66
+ if ($LASTEXITCODE -eq 0 -and $resolvedCandidateBase) {
67
+ return $candidateBase
68
+ }
69
+ }
70
+ throw 'Comment-policy baseline does not resolve to origin/main or HEAD^1.'
71
+ }
72
+
73
+ $commentPolicyBase = Resolve-CommentPolicyBase -ExplicitBase $CommentPolicyBase
74
+
50
75
  function Invoke-Tool {
51
76
  param(
52
77
  [string]$Label,
@@ -78,6 +103,15 @@ if (-not $SkipRuff) {
78
103
  }
79
104
  }
80
105
 
106
+ Invoke-Tool -Label 'comment-policy' -Action {
107
+ Push-Location $prLoopScriptsRoot
108
+ try {
109
+ python code_rules_gate.py --base $commentPolicyBase --comment-policy --repo-root $repositoryRoot
110
+ } finally {
111
+ Pop-Location
112
+ }
113
+ }
114
+
81
115
  if (-not $SkipMypy) {
82
116
  Invoke-Tool -Label 'mypy' -Action {
83
117
  Push-Location $hooksRoot
@@ -62,6 +62,7 @@ codex_hook_dependency_manifest = (
62
62
  "hooks/blocking/code_rules_boolean_mustcheck.py",
63
63
  "hooks/blocking/code_rules_command_dispatch.py",
64
64
  "hooks/blocking/code_rules_comments.py",
65
+ "hooks/blocking/javascript_comment_scanner.py",
65
66
  "hooks/blocking/code_rules_constants_config.py",
66
67
  "hooks/blocking/codex_apply_patch.py",
67
68
  "hooks/blocking/config/__init__.py",
@@ -76,3 +76,131 @@ def test_every_shipped_claude_rule_maps_to_an_mdc() -> None:
76
76
  if each_rule_file.name in _SKIPPED_RULE_FILE_NAMES:
77
77
  continue
78
78
  assert each_rule_file.name in output_name_by_rule_file, each_rule_file.name
79
+
80
+
81
+ def _comment_policy_phrases() -> tuple[str, ...]:
82
+ return (
83
+ "when a change touches code that an existing comment describes or is attached to",
84
+ "leave comments tied to untouched code unchanged",
85
+ "keep comment cleanup inside the requested task",
86
+ "production and tests follow one rule",
87
+ "changed directive, todo, fixme, hack, xxx, and type-ignore comments are removed rather than added or justified",
88
+ )
89
+
90
+
91
+ def _comment_policy_surfaces(package_root: Path) -> tuple[Path, ...]:
92
+ return (
93
+ package_root.parent.parent / "AGENTS.md",
94
+ package_root / "AGENTS.md",
95
+ package_root / "docs" / "CODE_RULES.md",
96
+ package_root / "system-prompts" / "software-engineer.xml",
97
+ package_root / ".agents" / "agents" / "clean-coder.md",
98
+ package_root / ".agents" / "agents" / "code-quality-agent.md",
99
+ package_root / "_shared" / "pr-loop" / "code-rules-gate.md",
100
+ package_root / "audit-rubrics" / "category_rubrics" / "category-j-code-rules-compliance.md",
101
+ package_root / "audit-rubrics" / "prompts" / "category-j-code-rules-compliance.md",
102
+ package_root / "audit-rubrics" / "category_rubrics" / "category-l-behavior-equivalence.md",
103
+ package_root / "audit-rubrics" / "prompts" / "category-l-behavior-equivalence.md",
104
+ package_root / ".agents" / "skills" / "grok-spawn" / "reference" / "worker-briefs.md",
105
+ package_root.parent.parent / ".github" / "copilot-instructions.md",
106
+ )
107
+
108
+
109
+ def _worker_policy_phrases() -> tuple[str, ...]:
110
+ return (
111
+ "do not add code comments.",
112
+ "preserve existing comments.",
113
+ "docstrings remain allowed.",
114
+ )
115
+
116
+
117
+ def _worker_policy_surfaces(package_root: Path) -> tuple[Path, ...]:
118
+ return (
119
+ package_root.parent.parent / "AGENTS.md",
120
+ package_root / "AGENTS.md",
121
+ package_root / "docs" / "CODE_RULES.md",
122
+ package_root / "system-prompts" / "software-engineer.xml",
123
+ package_root / ".agents" / "agents" / "clean-coder.md",
124
+ package_root / ".agents" / "skills" / "grok-spawn" / "reference" / "worker-briefs.md",
125
+ package_root.parent.parent / ".github" / "copilot-instructions.md",
126
+ )
127
+
128
+
129
+ def test_comment_guidance_reaches_installed_instruction_surfaces() -> None:
130
+ expected_phrases = _comment_policy_phrases()
131
+ all_surface_paths = _comment_policy_surfaces(_PACKAGE_ROOT)
132
+
133
+ for each_surface_path in all_surface_paths:
134
+ surface_text = each_surface_path.read_text(encoding="utf-8").lower()
135
+ for each_phrase in expected_phrases:
136
+ assert each_phrase in surface_text, each_surface_path
137
+ for each_surface_path in _worker_policy_surfaces(_PACKAGE_ROOT):
138
+ surface_text = each_surface_path.read_text(encoding="utf-8").lower()
139
+ for each_phrase in _worker_policy_phrases():
140
+ assert each_phrase in surface_text, each_surface_path
141
+
142
+
143
+ def _category_l_policy_texts() -> tuple[str, str, str]:
144
+ category_l_rubric = (
145
+ _PACKAGE_ROOT
146
+ / "audit-rubrics"
147
+ / "category_rubrics"
148
+ / "category-l-behavior-equivalence.md"
149
+ )
150
+ category_l_prompt = (
151
+ _PACKAGE_ROOT
152
+ / "audit-rubrics"
153
+ / "prompts"
154
+ / "category-l-behavior-equivalence.md"
155
+ )
156
+ audit_categories = _PACKAGE_ROOT / "audit-rubrics" / "audit-categories.json"
157
+ return tuple(
158
+ each_path.read_text(encoding="utf-8").lower()
159
+ for each_path in (category_l_rubric, category_l_prompt, audit_categories)
160
+ )
161
+
162
+
163
+ def _comment_policy_summary_texts() -> tuple[str, str, str, str]:
164
+ clean_coder = _PACKAGE_ROOT / ".agents" / "agents" / "clean-coder.md"
165
+ category_j = (
166
+ _PACKAGE_ROOT
167
+ / "audit-rubrics"
168
+ / "category_rubrics"
169
+ / "category-j-code-rules-compliance.md"
170
+ )
171
+ code_quality = _PACKAGE_ROOT / ".agents" / "agents" / "code-quality-agent.md"
172
+ gate = _PACKAGE_ROOT / "_shared" / "pr-loop" / "code-rules-gate.md"
173
+ return tuple(
174
+ each_path.read_text(encoding="utf-8").lower()
175
+ for each_path in (clean_coder, category_j, code_quality, gate)
176
+ )
177
+
178
+
179
+ def test_comment_policy_uses_changed_comment_names_and_type_directives() -> None:
180
+ category_l_rubric, category_l_prompt, audit_categories = _category_l_policy_texts()
181
+ clean_coder, category_j, code_quality, gate = _comment_policy_summary_texts()
182
+ assert "| l7 | changed-comment handling" in category_l_rubric
183
+ assert "**l7. changed-comment handling**" in category_l_prompt
184
+ assert '"axis_name": "changed-comment handling"' in audit_categories
185
+ assert "no type-ignore directives" in clean_coder
186
+ assert "no `# type: ignore` directives" in category_j
187
+ assert "production and test code" in code_quality
188
+ assert "comments tied to touched code are removed" in gate
189
+ assert "comments tied to untouched code remain unchanged" in gate
190
+
191
+
192
+ def test_comment_policy_removes_directive_justification_guidance() -> None:
193
+ react_patterns = (
194
+ _PACKAGE_ROOT / "docs" / "REACT_PATTERNS.md"
195
+ ).read_text(encoding="utf-8").lower()
196
+ category_e_prompt = (
197
+ _PACKAGE_ROOT
198
+ / "audit-rubrics"
199
+ / "prompts"
200
+ / "category-e-dead-code.md"
201
+ ).read_text(encoding="utf-8").lower()
202
+
203
+ assert "resolve `@ts-ignore` and `@ts-expect-error`" in react_patterns
204
+ assert "without clear justification" not in react_patterns
205
+ assert "changed `# noqa` directives are removed or resolved" in category_e_prompt
206
+ assert "every `# noqa` on an import line must be justified" not in category_e_prompt
@@ -115,8 +115,7 @@
115
115
  Types — complete coverage required:
116
116
  - Every parameter and return value carries an explicit type hint
117
117
  - Replace Any with the most specific type available
118
- - Prefer a real type over `# type: ignore`; when an ignore is required, add a second
119
- trailing `#` justification of at least five characters (AGENTS Types)
118
+ - Avoid `# type: ignore`; remove it and use a typed boundary or real type (AGENTS Types)
120
119
 
121
120
  Configuration — one source of truth:
122
121
  - Every constant lives in config/ only
@@ -124,10 +123,12 @@
124
123
  - UPPER_SNAKE names belong in config/ only
125
124
 
126
125
  Comments — preservation, add-time blocking:
127
- - Keep every comment on an untouched line untouched; removing one prints a stderr advisory and lets the edit through
128
- - Adding a new inline comment in production code blocks the edit
126
+ - Preserve existing comments.
127
+ - Do not add code comments.
128
+ - When a change touches code that an existing comment describes or is attached to, remove that comment in the same change and carry its meaning through clear names and structure. Leave comments tied to untouched code unchanged. Keep comment cleanup inside the requested task.
129
+ - Production and tests follow one rule. Changed directive, TODO, FIXME, HACK, XXX, and type-ignore comments are removed rather than added or justified.
129
130
  - Express new explanations through self-documenting names
130
- - Docstrings on new files, methods, and classes are permitted
131
+ - Docstrings remain allowed.
131
132
 
132
133
  Style — enforced at write time:
133
134
  - Imports belong at the top of the file
@@ -197,11 +198,6 @@
197
198
  </scope_discipline>
198
199
 
199
200
  <git_workflow>
200
- Maintain one commit per review stage. The initial feature is one commit. Each subsequent
201
- review round adds exactly one commit. Each round's fixes belong in a single, dedicated
202
- commit; keep the initial feature commit and each review-fix commit separate to preserve
203
- the full review history.
204
-
205
201
  Keep working documents and image files out of the repository. Files matching these patterns
206
202
  belong outside the repo:
207
203
  - docs/plans/*.md — working planning documents
@@ -212,13 +208,6 @@
212
208
  When multiple features need shared infrastructure, extract the shared infrastructure into
213
209
  its own PR, get it reviewed and merged, then launch parallel feature PRs that import it.
214
210
 
215
- When running as a GitHub Action (detected via the GITHUB_ACTIONS=true environment variable),
216
- commit and push each logical change as it completes rather than batching the full session
217
- into a single commit at the end. Each incremental commit carries a focused message
218
- describing only that change. This gives PR observers real-time progress and lets them
219
- intervene mid-run. The "one commit per review stage" rule above applies to interactive
220
- local sessions; in the GitHub Actions context the entire run is the review stage, and
221
- incremental commits within it are the expected shape.
222
211
  </git_workflow>
223
212
 
224
213
  <agent_workflow>
@@ -251,8 +240,6 @@
251
240
 
252
241
  Reply to each comment inline on the PR after the fix is in place.
253
242
 
254
- Produce one review-fix commit per review round. Each round gets exactly one commit covering
255
- all fixes for that round.
256
243
  </code_review_response>
257
244
 
258
245
  <documentation>