claude-dev-env 2.32.0 → 2.33.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/.agents/agents/clean-coder.md +8 -7
- package/.agents/agents/code-quality-agent.md +4 -2
- package/.agents/agents/pr-description-writer.md +8 -3
- package/.agents/agents/reference/pr-description-illustrative-voice.md +6 -1
- package/.agents/agents/test_agent_frontmatter.py +0 -9
- package/.agents/agents/test_pr_description_writer_style.py +24 -1
- package/.agents/skills/e-simplify/SKILL.md +1 -15
- package/.agents/skills/grok-spawn/reference/worker-briefs.md +4 -0
- package/.agents/skills/pr-refinement/SKILL.md +2 -2
- package/.agents/skills/pr-shared-extraction/SKILL.md +2 -2
- package/.agents/skills/pr-title-description/SKILL.md +6 -2
- package/.agents/skills/review-tier/SKILL.md +1 -1
- package/.agents/skills/review-tier/reference/tier-policy.json +2 -2
- package/.agents/skills/review-tier/scripts/review_tier.py +31 -4
- package/.agents/skills/review-tier/scripts/test_generation.py +7 -19
- package/.agents/skills/review-tier/scripts/test_review_tier.py +11 -11
- package/AGENTS.md +83 -0
- package/_shared/pr-loop/code-rules-gate.md +5 -2
- package/_shared/pr-loop/scripts/code_rules_gate.py +7 -3
- package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_arguments.py +13 -1
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_gate_arguments.py +7 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +47 -1
- package/audit-rubrics/audit-categories.json +2 -2
- package/audit-rubrics/category_rubrics/category-e-dead-code.md +1 -1
- package/audit-rubrics/category_rubrics/category-j-code-rules-compliance.md +5 -3
- package/audit-rubrics/category_rubrics/category-l-behavior-equivalence.md +6 -3
- package/audit-rubrics/prompts/category-e-dead-code.md +6 -6
- package/audit-rubrics/prompts/category-j-code-rules-compliance.md +25 -19
- package/audit-rubrics/prompts/category-l-behavior-equivalence.md +28 -25
- package/docs/CODE_RULES.md +7 -8
- package/hooks/blocking/code_rules_comments.py +118 -59
- package/hooks/blocking/code_rules_enforcer.py +19 -7
- package/hooks/blocking/code_rules_type_escape.py +16 -23
- package/hooks/blocking/javascript_comment_scanner.py +99 -0
- package/hooks/blocking/test_code_rules_enforcer_any_type_ignore.py +24 -4
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +0 -1
- package/hooks/blocking/test_code_rules_enforcer_comment_string_awareness.py +190 -3
- package/hooks/blocking/test_code_rules_enforcer_javascript_comments.py +44 -0
- package/hooks/blocking/test_code_rules_enforcer_join_separator_magic.py +0 -1
- package/hooks/git-hooks/pull_request_handoff.py +1 -1
- package/hooks/git-hooks/test_post_commit.py +8 -0
- package/hooks/hooks_constants/stale_comment_reference_blocker_constants.py +3 -4
- package/hooks/hooks_constants/test_unused_module_import_constants.py +9 -1
- package/hooks/hooks_constants/unused_module_import_constants.py +1 -1
- package/hooks/validators/comment_checks.py +2 -2
- package/hooks/validators/fast_save_validators.py +0 -9
- package/hooks/validators/git_checks.py +0 -84
- package/hooks/validators/run_all_validators.py +2 -9
- package/hooks/validators/test_fast_save_validators.py +2 -8
- package/hooks/validators/test_git_checks.py +0 -249
- package/hooks/validators/test_todo_checks.py +1 -0
- package/hooks/validators/todo_checks.py +1 -1
- package/package.json +1 -1
- package/rules/git-workflow.md +0 -8
- package/scripts/check.ps1 +35 -1
- package/scripts/codex_compat_materializer.py +1 -0
- package/scripts/tests/test_rules.py +111 -0
- package/system-prompts/software-engineer.xml +6 -19
- package/.agents/skills-archived/split-pr/SKILL.md +0 -85
- package/.agents/skills-archived/split-pr/reference/path-layers.md +0 -16
- package/.agents/skills-archived/split-pr/reference/proposal-format.md +0 -15
- package/.agents/skills-archived/split-pr/reference/split-further-loop.md +0 -10
- package/.agents/skills-archived/split-pr/reference/splitting-principles.md +0 -26
- package/.agents/skills-archived/split-pr/scripts/analyze_pr.py +0 -279
- package/.agents/skills-archived/split-pr/scripts/categorize_files.py +0 -106
- package/.agents/skills-archived/split-pr/scripts/config/__init__.py +0 -1
- package/.agents/skills-archived/split-pr/scripts/config/dependency_constants.py +0 -14
- package/.agents/skills-archived/split-pr/scripts/config/git_operations_constants.py +0 -36
- package/.agents/skills-archived/split-pr/scripts/config/packing_constants.py +0 -61
- package/.agents/skills-archived/split-pr/scripts/config/plan_constants.py +0 -49
- package/.agents/skills-archived/split-pr/scripts/config/split_pr_constants.py +0 -110
- package/.agents/skills-archived/split-pr/scripts/execute_split_slices.py +0 -82
- package/.agents/skills-archived/split-pr/scripts/pack_files_into_slices.py +0 -212
- package/.agents/skills-archived/split-pr/scripts/split_pr_dependency_graph.py +0 -70
- package/.agents/skills-archived/split-pr/scripts/split_pr_git_operations.py +0 -184
- package/.agents/skills-archived/split-pr/scripts/split_pr_layer_order.py +0 -58
- package/.agents/skills-archived/split-pr/scripts/split_pr_paginate.py +0 -119
- package/.agents/skills-archived/split-pr/scripts/split_pr_process_runner.py +0 -52
- package/.agents/skills-archived/split-pr/scripts/split_pr_script_types.py +0 -126
- package/.agents/skills-archived/split-pr/scripts/split_pr_title.py +0 -41
- package/.agents/skills-archived/split-pr/scripts/test_analyze_pr.py +0 -228
- package/.agents/skills-archived/split-pr/scripts/test_categorize_files.py +0 -55
- package/.agents/skills-archived/split-pr/scripts/test_categorize_files_packing.py +0 -59
- package/.agents/skills-archived/split-pr/scripts/test_execute_split_slices.py +0 -99
- package/.agents/skills-archived/split-pr/scripts/test_split_pr_dependency_graph.py +0 -47
- package/.agents/skills-archived/split-pr/scripts/test_split_pr_git_operations.py +0 -125
- package/.agents/skills-archived/split-pr/scripts/test_split_pr_layer_order.py +0 -36
- package/.agents/skills-archived/split-pr/scripts/test_split_pr_paginate.py +0 -65
- package/.agents/skills-archived/split-pr/scripts/test_split_pr_script_types.py +0 -73
- package/.agents/skills-archived/split-pr/scripts/test_split_pr_title.py +0 -28
- package/.agents/skills-archived/split-pr/scripts/test_verify_dependency_graph.py +0 -46
- package/.agents/skills-archived/split-pr/scripts/test_verify_plan.py +0 -56
- package/.agents/skills-archived/split-pr/scripts/test_verify_plan_contract.py +0 -50
- package/.agents/skills-archived/split-pr/scripts/test_verify_plan_path_normalization.py +0 -45
- package/.agents/skills-archived/split-pr/scripts/verify_dependency_graph.py +0 -111
- 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
|
|
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
package/rules/git-workflow.md
CHANGED
|
@@ -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,114 @@ 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
|
|
@@ -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
|
-
-
|
|
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
|
-
-
|
|
128
|
-
-
|
|
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
|
|
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>
|