claude-dev-env 1.78.0 → 1.80.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/_shared/pr-loop/scripts/CLAUDE.md +1 -0
- package/_shared/pr-loop/scripts/copilot_quota.py +360 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/copilot_quota_constants.py +24 -0
- package/_shared/pr-loop/scripts/tests/CLAUDE.md +7 -0
- package/_shared/pr-loop/scripts/tests/fixtures/copilot_internal_user_jonecho.json +76 -0
- package/_shared/pr-loop/scripts/tests/test_copilot_quota.py +242 -0
- package/_shared/pr-loop/scripts/tests/test_copilot_quota_constants.py +63 -0
- package/audit-rubrics/category_rubrics/category-k-codebase-conflicts.md +1 -0
- package/bin/install.mjs +1 -0
- package/bin/install.test.mjs +3 -2
- package/hooks/blocking/CLAUDE.md +3 -2
- package/hooks/blocking/code_rules_docstrings.py +609 -16
- package/hooks/blocking/code_rules_enforcer.py +41 -0
- package/hooks/blocking/code_rules_imports_logging.py +136 -1
- package/hooks/blocking/code_rules_naming_collection.py +76 -1
- package/hooks/blocking/code_rules_paired_test.py +240 -22
- package/hooks/blocking/code_rules_test_assertions.py +159 -1
- package/hooks/blocking/code_rules_unused_imports.py +7 -63
- package/hooks/blocking/env_var_table_code_drift_blocker.py +475 -0
- package/hooks/blocking/package_inventory_stale_blocker.py +54 -15
- package/hooks/blocking/test_code_rules_enforcer_docstring_field_runmode_outcome.py +129 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_length_constant_superlative.py +198 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_no_network.py +115 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_unreferenced_param.py +160 -0
- package/hooks/blocking/test_code_rules_enforcer_js_returns_object.py +72 -0
- package/hooks/blocking/test_code_rules_enforcer_module_docstring_data_schema_scope.py +82 -0
- package/hooks/blocking/test_code_rules_enforcer_paired_test.py +190 -0
- package/hooks/blocking/test_code_rules_enforcer_polarity_name_contradiction.py +76 -0
- package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +96 -15
- package/hooks/blocking/test_code_rules_enforcer_vacuous_cleanup_assertion.py +132 -0
- package/hooks/blocking/test_code_rules_js_returns_object_schemaless.py +167 -0
- package/hooks/blocking/test_env_var_table_code_drift_blocker.py +94 -0
- package/hooks/blocking/test_package_inventory_stale_blocker.py +46 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +7 -7
- package/hooks/hooks_constants/CLAUDE.md +1 -0
- package/hooks/hooks_constants/blocking_check_limits.py +79 -0
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +28 -0
- package/hooks/hooks_constants/env_var_table_code_drift_constants.py +64 -0
- package/hooks/hooks_constants/package_inventory_stale_blocker_constants.py +20 -9
- package/hooks/hooks_constants/paired_test_coverage_constants.py +11 -3
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +4 -0
- package/hooks/hooks_constants/test_code_rules_enforcer_constants.py +93 -0
- package/hooks/hooks_constants/unused_module_import_constants.py +0 -1
- package/package.json +1 -1
- package/rules/CLAUDE.md +1 -0
- package/rules/docstring-prose-matches-implementation.md +57 -54
- package/rules/env-var-table-code-drift.md +24 -0
- package/rules/package-inventory-stale-entry.md +4 -4
- package/rules/paired-test-coverage.md +12 -5
- package/skills/autoconverge/SKILL.md +26 -5
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +4 -4
- package/skills/autoconverge/workflow/converge.contract.test.mjs +56 -120
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +45 -5
- package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +50 -46
- package/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +6 -6
- package/skills/autoconverge/workflow/converge.mjs +110 -228
- package/skills/autoconverge/workflow/converge.run-input.test.mjs +11 -0
- package/skills/autoconverge/workflow/converge_multi.mjs +23 -7
- package/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +28 -2
- package/skills/pr-converge/SKILL.md +28 -2
- package/skills/pr-converge/reference/convergence-gates.md +13 -1
- package/skills/pr-converge/reference/state-schema.md +11 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"""Direct unit tests for the copilot_quota pre-check.
|
|
2
|
+
|
|
3
|
+
Every case drives the production ``main`` / ``evaluate_copilot_quota`` path with
|
|
4
|
+
only the ``gh`` subprocess boundary (``_run_gh``) stubbed, fed a captured
|
|
5
|
+
``copilot_internal/user`` JSON fixture in the jonecho shape.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import importlib.util
|
|
9
|
+
import json
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from types import ModuleType
|
|
13
|
+
|
|
14
|
+
import pytest
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _load_copilot_quota_module() -> ModuleType:
|
|
18
|
+
scripts_directory = Path(__file__).parent.parent
|
|
19
|
+
if str(scripts_directory) not in sys.path:
|
|
20
|
+
sys.path.insert(0, str(scripts_directory))
|
|
21
|
+
module_path = scripts_directory / "copilot_quota.py"
|
|
22
|
+
specification = importlib.util.spec_from_file_location("copilot_quota", module_path)
|
|
23
|
+
assert specification is not None
|
|
24
|
+
assert specification.loader is not None
|
|
25
|
+
module = importlib.util.module_from_spec(specification)
|
|
26
|
+
sys.modules[specification.name] = module
|
|
27
|
+
specification.loader.exec_module(module)
|
|
28
|
+
return module
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
copilot_quota = _load_copilot_quota_module()
|
|
32
|
+
|
|
33
|
+
FIXTURE_PATH = Path(__file__).parent / "fixtures" / "copilot_internal_user_jonecho.json"
|
|
34
|
+
AVAILABLE_USER_JSON = FIXTURE_PATH.read_text(encoding="utf-8")
|
|
35
|
+
FAKE_TOKEN_RESULT = (0, "ghp_faketoken_value\n")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _exhausted_user_json() -> str:
|
|
39
|
+
user = json.loads(AVAILABLE_USER_JSON)
|
|
40
|
+
premium = user["quota_snapshots"]["premium_interactions"]
|
|
41
|
+
premium["remaining"] = 0
|
|
42
|
+
premium["quota_remaining"] = 0.0
|
|
43
|
+
premium["percent_remaining"] = 0.0
|
|
44
|
+
return json.dumps(user)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _user_json_without_premium_snapshot() -> str:
|
|
48
|
+
user = json.loads(AVAILABLE_USER_JSON)
|
|
49
|
+
del user["quota_snapshots"]
|
|
50
|
+
return json.dumps(user)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _gh_stub(token_result: tuple[int, str], api_result: tuple[int, str]):
|
|
54
|
+
def _fake_run_gh(
|
|
55
|
+
command_arguments: list[str],
|
|
56
|
+
extra_environment: dict[str, str] | None = None,
|
|
57
|
+
) -> tuple[int, str]:
|
|
58
|
+
if command_arguments and command_arguments[0] == "auth":
|
|
59
|
+
return token_result
|
|
60
|
+
if command_arguments and command_arguments[0] == "api":
|
|
61
|
+
return api_result
|
|
62
|
+
raise AssertionError(f"unexpected gh command {command_arguments}")
|
|
63
|
+
|
|
64
|
+
return _fake_run_gh
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@pytest.fixture(autouse=True)
|
|
68
|
+
def _isolate_account_sources(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
|
69
|
+
monkeypatch.delenv("COPILOT_QUOTA_ACCOUNT", raising=False)
|
|
70
|
+
monkeypatch.setattr(
|
|
71
|
+
copilot_quota, "COPILOT_QUOTA_DEFAULT_ENV_FILE_PATH", tmp_path / ".env"
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_main_runs_copilot_when_premium_quota_available(
|
|
76
|
+
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
77
|
+
) -> None:
|
|
78
|
+
monkeypatch.setenv("COPILOT_QUOTA_ACCOUNT", "jonecho")
|
|
79
|
+
monkeypatch.setattr(
|
|
80
|
+
copilot_quota,
|
|
81
|
+
"_run_gh",
|
|
82
|
+
_gh_stub(FAKE_TOKEN_RESULT, (0, AVAILABLE_USER_JSON)),
|
|
83
|
+
)
|
|
84
|
+
exit_code = copilot_quota.main([])
|
|
85
|
+
captured = capsys.readouterr()
|
|
86
|
+
assert exit_code == 0
|
|
87
|
+
assert "running Copilot" in captured.out
|
|
88
|
+
assert "6817" in captured.out
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_main_exits_out_of_quota_when_premium_exhausted(
|
|
92
|
+
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
93
|
+
) -> None:
|
|
94
|
+
monkeypatch.setenv("COPILOT_QUOTA_ACCOUNT", "jonecho")
|
|
95
|
+
monkeypatch.setattr(
|
|
96
|
+
copilot_quota,
|
|
97
|
+
"_run_gh",
|
|
98
|
+
_gh_stub(FAKE_TOKEN_RESULT, (0, _exhausted_user_json())),
|
|
99
|
+
)
|
|
100
|
+
exit_code = copilot_quota.main([])
|
|
101
|
+
captured = capsys.readouterr()
|
|
102
|
+
assert exit_code == 1
|
|
103
|
+
assert "scenario A" in captured.err
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_main_exits_api_down_on_non_json_response(
|
|
107
|
+
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
108
|
+
) -> None:
|
|
109
|
+
monkeypatch.setenv("COPILOT_QUOTA_ACCOUNT", "jonecho")
|
|
110
|
+
monkeypatch.setattr(
|
|
111
|
+
copilot_quota,
|
|
112
|
+
"_run_gh",
|
|
113
|
+
_gh_stub(FAKE_TOKEN_RESULT, (0, "not json at all")),
|
|
114
|
+
)
|
|
115
|
+
exit_code = copilot_quota.main([])
|
|
116
|
+
captured = capsys.readouterr()
|
|
117
|
+
assert exit_code == 2
|
|
118
|
+
assert "scenario B" in captured.err
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def test_main_exits_api_down_on_missing_premium_snapshot(
|
|
122
|
+
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
123
|
+
) -> None:
|
|
124
|
+
monkeypatch.setenv("COPILOT_QUOTA_ACCOUNT", "jonecho")
|
|
125
|
+
monkeypatch.setattr(
|
|
126
|
+
copilot_quota,
|
|
127
|
+
"_run_gh",
|
|
128
|
+
_gh_stub(FAKE_TOKEN_RESULT, (0, _user_json_without_premium_snapshot())),
|
|
129
|
+
)
|
|
130
|
+
exit_code = copilot_quota.main([])
|
|
131
|
+
captured = capsys.readouterr()
|
|
132
|
+
assert exit_code == 2
|
|
133
|
+
assert "scenario B" in captured.err
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def test_main_exits_api_down_when_gh_token_unresolved(
|
|
137
|
+
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
138
|
+
) -> None:
|
|
139
|
+
monkeypatch.setenv("COPILOT_QUOTA_ACCOUNT", "jonecho")
|
|
140
|
+
monkeypatch.setattr(
|
|
141
|
+
copilot_quota,
|
|
142
|
+
"_run_gh",
|
|
143
|
+
_gh_stub((1, ""), (0, AVAILABLE_USER_JSON)),
|
|
144
|
+
)
|
|
145
|
+
exit_code = copilot_quota.main([])
|
|
146
|
+
captured = capsys.readouterr()
|
|
147
|
+
assert exit_code == 2
|
|
148
|
+
assert "scenario B" in captured.err
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def test_main_exits_no_config_and_names_env_path_and_key(
|
|
152
|
+
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
153
|
+
) -> None:
|
|
154
|
+
def _fail_if_called(
|
|
155
|
+
command_arguments: list[str],
|
|
156
|
+
extra_environment: dict[str, str] | None = None,
|
|
157
|
+
) -> tuple[int, str]:
|
|
158
|
+
raise AssertionError("gh must not run when no account is configured")
|
|
159
|
+
|
|
160
|
+
monkeypatch.setattr(copilot_quota, "_run_gh", _fail_if_called)
|
|
161
|
+
exit_code = copilot_quota.main([])
|
|
162
|
+
captured = capsys.readouterr()
|
|
163
|
+
assert exit_code == 3
|
|
164
|
+
assert "scenario C" in captured.err
|
|
165
|
+
assert "COPILOT_QUOTA_ACCOUNT" in captured.err
|
|
166
|
+
assert ".env" in captured.err
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@pytest.mark.parametrize(
|
|
170
|
+
("api_result", "expected_exit_code", "expected_scenario"),
|
|
171
|
+
[
|
|
172
|
+
((0, _exhausted_user_json()), 1, "scenario A"),
|
|
173
|
+
((0, "not json at all"), 2, "scenario B"),
|
|
174
|
+
],
|
|
175
|
+
)
|
|
176
|
+
def test_every_gh_backed_skip_writes_a_log_line_naming_the_scenario(
|
|
177
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
178
|
+
capsys: pytest.CaptureFixture[str],
|
|
179
|
+
api_result: tuple[int, str],
|
|
180
|
+
expected_exit_code: int,
|
|
181
|
+
expected_scenario: str,
|
|
182
|
+
) -> None:
|
|
183
|
+
monkeypatch.setenv("COPILOT_QUOTA_ACCOUNT", "jonecho")
|
|
184
|
+
monkeypatch.setattr(
|
|
185
|
+
copilot_quota, "_run_gh", _gh_stub(FAKE_TOKEN_RESULT, api_result)
|
|
186
|
+
)
|
|
187
|
+
exit_code = copilot_quota.main([])
|
|
188
|
+
captured = capsys.readouterr()
|
|
189
|
+
assert exit_code == expected_exit_code
|
|
190
|
+
assert captured.out == ""
|
|
191
|
+
assert expected_scenario in captured.err
|
|
192
|
+
assert captured.err.strip() != ""
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def test_cli_account_takes_precedence_over_env_var(
|
|
196
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
197
|
+
) -> None:
|
|
198
|
+
monkeypatch.setenv("COPILOT_QUOTA_ACCOUNT", "env-user")
|
|
199
|
+
accounts_seen: list[str] = []
|
|
200
|
+
|
|
201
|
+
def _fake_run_gh(
|
|
202
|
+
command_arguments: list[str],
|
|
203
|
+
extra_environment: dict[str, str] | None = None,
|
|
204
|
+
) -> tuple[int, str]:
|
|
205
|
+
if command_arguments[0] == "auth":
|
|
206
|
+
accounts_seen.append(command_arguments[-1])
|
|
207
|
+
return FAKE_TOKEN_RESULT
|
|
208
|
+
return (0, AVAILABLE_USER_JSON)
|
|
209
|
+
|
|
210
|
+
monkeypatch.setattr(copilot_quota, "_run_gh", _fake_run_gh)
|
|
211
|
+
decision = copilot_quota.evaluate_copilot_quota(
|
|
212
|
+
cli_account="cli-user", env_file_path=tmp_path / ".env"
|
|
213
|
+
)
|
|
214
|
+
assert decision.exit_code == 0
|
|
215
|
+
assert accounts_seen == ["cli-user"]
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def test_account_resolves_from_env_file_when_flag_and_env_absent(
|
|
219
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
220
|
+
) -> None:
|
|
221
|
+
env_file = tmp_path / ".env"
|
|
222
|
+
env_file.write_text(
|
|
223
|
+
"# local copilot quota account\nCOPILOT_QUOTA_ACCOUNT=file-user\n",
|
|
224
|
+
encoding="utf-8",
|
|
225
|
+
)
|
|
226
|
+
accounts_seen: list[str] = []
|
|
227
|
+
|
|
228
|
+
def _fake_run_gh(
|
|
229
|
+
command_arguments: list[str],
|
|
230
|
+
extra_environment: dict[str, str] | None = None,
|
|
231
|
+
) -> tuple[int, str]:
|
|
232
|
+
if command_arguments[0] == "auth":
|
|
233
|
+
accounts_seen.append(command_arguments[-1])
|
|
234
|
+
return FAKE_TOKEN_RESULT
|
|
235
|
+
return (0, AVAILABLE_USER_JSON)
|
|
236
|
+
|
|
237
|
+
monkeypatch.setattr(copilot_quota, "_run_gh", _fake_run_gh)
|
|
238
|
+
decision = copilot_quota.evaluate_copilot_quota(
|
|
239
|
+
cli_account=None, env_file_path=env_file
|
|
240
|
+
)
|
|
241
|
+
assert decision.exit_code == 0
|
|
242
|
+
assert accounts_seen == ["file-user"]
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""Tests for copilot_quota_constants.py extracted constant set."""
|
|
2
|
+
|
|
3
|
+
import importlib.util
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from types import ModuleType
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _load_constants_module() -> ModuleType:
|
|
9
|
+
module_path = (
|
|
10
|
+
Path(__file__).parent.parent
|
|
11
|
+
/ "pr_loop_shared_constants"
|
|
12
|
+
/ "copilot_quota_constants.py"
|
|
13
|
+
)
|
|
14
|
+
specification = importlib.util.spec_from_file_location(
|
|
15
|
+
"pr_loop_shared_constants.copilot_quota_constants", module_path
|
|
16
|
+
)
|
|
17
|
+
assert specification is not None
|
|
18
|
+
assert specification.loader is not None
|
|
19
|
+
module = importlib.util.module_from_spec(specification)
|
|
20
|
+
specification.loader.exec_module(module)
|
|
21
|
+
return module
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
constants_module = _load_constants_module()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_copilot_quota_account_env_var_name() -> None:
|
|
28
|
+
assert (
|
|
29
|
+
constants_module.COPILOT_QUOTA_ACCOUNT_ENV_VAR_NAME == "COPILOT_QUOTA_ACCOUNT"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_gh_token_env_var_name() -> None:
|
|
34
|
+
assert constants_module.GH_TOKEN_ENV_VAR_NAME == "GH_TOKEN"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_copilot_internal_user_api_path() -> None:
|
|
38
|
+
assert constants_module.COPILOT_INTERNAL_USER_API_PATH == "copilot_internal/user"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def test_premium_interactions_gating_field_names() -> None:
|
|
42
|
+
assert constants_module.QUOTA_SNAPSHOTS_FIELD_NAME == "quota_snapshots"
|
|
43
|
+
assert constants_module.PREMIUM_INTERACTIONS_FIELD_NAME == "premium_interactions"
|
|
44
|
+
assert constants_module.PREMIUM_UNLIMITED_FIELD_NAME == "unlimited"
|
|
45
|
+
assert constants_module.PREMIUM_REMAINING_FIELD_NAME == "remaining"
|
|
46
|
+
assert constants_module.PREMIUM_OVERAGE_PERMITTED_FIELD_NAME == "overage_permitted"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_exit_codes_are_the_four_distinct_scenarios() -> None:
|
|
50
|
+
all_exit_codes = (
|
|
51
|
+
constants_module.EXIT_CODE_QUOTA_AVAILABLE,
|
|
52
|
+
constants_module.EXIT_CODE_OUT_OF_QUOTA,
|
|
53
|
+
constants_module.EXIT_CODE_QUOTA_API_DOWN,
|
|
54
|
+
constants_module.EXIT_CODE_NO_ACCOUNT_CONFIGURED,
|
|
55
|
+
)
|
|
56
|
+
assert all_exit_codes == (0, 1, 2, 3)
|
|
57
|
+
assert len(set(all_exit_codes)) == len(all_exit_codes)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_default_env_file_path_sits_at_the_package_root() -> None:
|
|
61
|
+
default_env_file_path = constants_module.COPILOT_QUOTA_DEFAULT_ENV_FILE_PATH
|
|
62
|
+
assert default_env_file_path.name == ".env"
|
|
63
|
+
assert default_env_file_path.parent.name == "claude-dev-env"
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
- A module's existing `_resolve_base_ref` guards a missing remote with `getattr(remote, "name", "") or DEFAULT_REMOTE`; the diff adds `_resolve_head_ref` beside it that dereferences `remote.name` bare, crashing on the detached-HEAD case its sibling survives.
|
|
23
23
|
- A rules reference whose enforcement table marks letter J with ⚡ (blocking hook) while its audit-surface section three paragraphs later lists J under "non-blocking, multi-file reasoning" — one letter, two contradictory enforcement claims in one document.
|
|
24
24
|
- A hooks.json with the same hook registered in two parallel matcher blocks (Write|Edit + MultiEdit) when an existing Write|Edit|MultiEdit block already handles the same surface.
|
|
25
|
+
- A new function re-implements an in-place atomic file rewrite — stream to a temp sibling, `os.replace` over the original, best-effort temp cleanup on failure — that a shared helper already centralizes (`rewrite_stp_member_atomically` and the like); the two copies drift the moment one gets a temp-naming or concurrency fix the other misses (CODE_RULES §3, reuse before create). The audit catches it by matching the diff's temp-sibling-plus-`os.replace` orchestration against the shared module the same file already imports from.
|
|
25
26
|
|
|
26
27
|
**Companion reference:** see `../source-material-section-types.md`.
|
|
27
28
|
|
package/bin/install.mjs
CHANGED
|
@@ -312,6 +312,7 @@ export const FOLDED_HOOK_RELATIVE_PATHS = new Set([
|
|
|
312
312
|
'blocking/verified_commit_message_accuracy_blocker.py',
|
|
313
313
|
'blocking/workflow_substitution_slot_blocker.py',
|
|
314
314
|
'blocking/claude_md_orphan_file_blocker.py',
|
|
315
|
+
'blocking/env_var_table_code_drift_blocker.py',
|
|
315
316
|
'blocking/pytest_testpaths_orphan_blocker.py',
|
|
316
317
|
'blocking/open_questions_in_plans_blocker.py',
|
|
317
318
|
'blocking/plain_language_blocker.py',
|
package/bin/install.test.mjs
CHANGED
|
@@ -718,8 +718,8 @@ const OLD_FOLDED_HOOKS_SETTINGS = {
|
|
|
718
718
|
};
|
|
719
719
|
|
|
720
720
|
|
|
721
|
-
test('FOLDED_HOOK_RELATIVE_PATHS contains all
|
|
722
|
-
assert.equal(FOLDED_HOOK_RELATIVE_PATHS.size,
|
|
721
|
+
test('FOLDED_HOOK_RELATIVE_PATHS contains all 16 hooks removed from hooks.json', () => {
|
|
722
|
+
assert.equal(FOLDED_HOOK_RELATIVE_PATHS.size, 16);
|
|
723
723
|
assert.ok(FOLDED_HOOK_RELATIVE_PATHS.has('blocking/write_existing_file_blocker.py'));
|
|
724
724
|
assert.ok(FOLDED_HOOK_RELATIVE_PATHS.has('blocking/plain_language_blocker.py'));
|
|
725
725
|
assert.ok(FOLDED_HOOK_RELATIVE_PATHS.has('blocking/code_rules_enforcer.py'));
|
|
@@ -741,6 +741,7 @@ test('FOLDED_HOOK_RELATIVE_PATHS lists every hook the PreToolUse dispatcher host
|
|
|
741
741
|
'blocking/verified_commit_message_accuracy_blocker.py',
|
|
742
742
|
'blocking/workflow_substitution_slot_blocker.py',
|
|
743
743
|
'blocking/claude_md_orphan_file_blocker.py',
|
|
744
|
+
'blocking/env_var_table_code_drift_blocker.py',
|
|
744
745
|
'blocking/pytest_testpaths_orphan_blocker.py',
|
|
745
746
|
'blocking/open_questions_in_plans_blocker.py',
|
|
746
747
|
'blocking/plain_language_blocker.py',
|
package/hooks/blocking/CLAUDE.md
CHANGED
|
@@ -38,7 +38,7 @@ The check modules it calls are the `code_rules_<concern>.py` files below.
|
|
|
38
38
|
| `code_rules_naming_collection.py` | Collection names must use `all_*` prefix |
|
|
39
39
|
| `code_rules_optional_params.py` | No optional parameters where a required one would do |
|
|
40
40
|
| `code_rules_orphan_css_class.py` | CSS class attributes in Python markup with no matching `.<class>` selector |
|
|
41
|
-
| `code_rules_paired_test.py` | A public function omitted by a module's established paired test suite must get a behavioral test |
|
|
41
|
+
| `code_rules_paired_test.py` | A public function omitted by a module's established paired test suite must get a behavioral test — checked on both the production-module write and the stem-matched test-file write |
|
|
42
42
|
| `code_rules_path_utils.py` | Path utility helpers shared across check modules |
|
|
43
43
|
| `code_rules_paths_syspath.py` | `sys.path.insert` must be guarded |
|
|
44
44
|
| `code_rules_probe_chains.py` | Probe-chain detection logic |
|
|
@@ -66,6 +66,7 @@ The check modules it calls are the `code_rules_<concern>.py` files below.
|
|
|
66
66
|
| `destructive_command_blocker.py` | PreToolUse (Bash/PowerShell) | Shell commands with destructive literals (`rm -rf`, `git reset --hard`, etc.) |
|
|
67
67
|
| `docstring_rule_gate_count_blocker.py` | PreToolUse (Write/Edit/MultiEdit) | A stale spelled-out gate-validator count in `docstring-prose-matches-implementation.md` — the "N more gate validators" / "M gated slices" count drifting from the `check_docstring_*` validators the prose names |
|
|
68
68
|
| `duplicate_rmtree_helper_blocker.py` | PreToolUse (Write/Edit) | A local re-definition of the Windows-safe rmtree helper trio (`_strip_read_only_and_retry`, `_force_remove_tree` / `force_rmtree`) in place of importing a shared helper |
|
|
69
|
+
| `env_var_table_code_drift_blocker.py` | PreToolUse (Write/Edit/MultiEdit) | A markdown env-var summary table row attributing an environment variable to a code file whose source never references that variable name |
|
|
69
70
|
| `es_exe_path_rewriter.py` | PreToolUse | Rewrites paths referencing `.exe` under the Everything search path |
|
|
70
71
|
| `gh_body_arg_blocker.py` | PreToolUse (Bash) | `gh` commands passing `--body`/`-b` directly (requires `--body-file` instead) |
|
|
71
72
|
| `gh_pr_author_enforcer.py` | PreToolUse | Enforces PR author identity rules |
|
|
@@ -75,7 +76,7 @@ The check modules it calls are the `code_rules_<concern>.py` files below.
|
|
|
75
76
|
| `intent_only_ending_blocker.py` | Stop | Responses that end on a plan or intent without doing the work |
|
|
76
77
|
| `md_to_html_blocker.py` | PreToolUse (Write/Edit) | Writing `.md` files when an `.html` companion is required |
|
|
77
78
|
| `open_questions_in_plans_blocker.py` | PreToolUse (Write/Edit) | Plan documents with unresolved open questions |
|
|
78
|
-
| `package_inventory_stale_blocker.py` | PreToolUse (Write) | A new production code file created in a directory whose `README.md`/`CLAUDE.md` inventory names two or more sibling files but no entry for the new file |
|
|
79
|
+
| `package_inventory_stale_blocker.py` | PreToolUse (Write) | A new production code file created in a directory whose `README.md`/`CLAUDE.md` inventory (or a parent skill's `SKILL.md` Layout table mapping the `scripts/` subdirectory) names two or more sibling files but no entry for the new file |
|
|
79
80
|
| `plain_language_blocker.py` | PreToolUse (Write/Edit/AskUserQuestion) | Heavy or jargon words in user-facing prose |
|
|
80
81
|
| `pr_converge_bugteam_enforcer.py` | PreToolUse | Enforces that bugteam runs in parallel with bugbot in pr-converge loops |
|
|
81
82
|
| `pr_description_enforcer.py` | PreToolUse (Bash) | `gh pr create`/`edit` without a PR-description-writer-authored body |
|