claude-dev-env 7.7.1 → 8.0.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 +5 -6
- package/.agents/agents/issue-tracker.md +1 -1
- package/.agents/agents/pr-description-writer.md +1 -1
- package/.agents/agents/skill-writer-agent.md +1 -12
- package/.agents/agents/test_agent_frontmatter.py +2 -19
- package/.agents/skills/issue-tracker/SKILL.md +1 -1
- package/.agents/skills/orchestrator/SKILL.md +4 -5
- package/.agents/skills/pull-request/SKILL.md +8 -7
- package/.agents/skills/pull-request/reference/publication-tasks.md +1 -1
- package/.agents/skills/team-advisor/SKILL.md +2 -3
- package/_shared/advisor/advisor-protocol.md +0 -1
- package/_shared/advisor/reference/lifecycle.md +0 -1
- package/_shared/advisor/reference/warm-up.md +1 -2
- package/audit-rubrics/prompts/category-e-dead-code.md +3 -3
- package/bin/install-constants.mjs +0 -5
- package/bin/install-pstack-plugin.mjs +192 -0
- package/bin/install-pstack-plugin.test.mjs +181 -0
- package/bin/install.agents-home.test.mjs +0 -4
- package/bin/install.codex-rules.test.mjs +6 -0
- package/bin/install.cursor-rules.test.mjs +3 -253
- package/bin/install.mjs +55 -160
- package/bin/install.test.mjs +110 -149
- package/bin/prune-retired-pstack-store.mjs +83 -0
- package/bin/prune-retired-pstack-store.test.mjs +105 -0
- package/bin/prune-session-continuity.mjs +57 -0
- package/bin/prune-session-continuity.test.mjs +70 -0
- package/bin/resolve-install-root.mjs +2 -0
- package/bin/resolve-install-root.test.mjs +69 -0
- package/docs/CODE_RULES.md +1 -1
- package/docs/agent-spawn-protocol.md +1 -1
- package/docs/codex-compatibility.md +4 -16
- package/docs/nas-ssh-invocation.md +1 -1
- package/docs/references/prose-style-enforcement.md +2 -2
- package/hooks/HOOK_SPECS_PROMPT_WORKFLOW.md +1 -3
- package/hooks/blocking/code_rules_constants_config.py +1 -108
- package/hooks/blocking/code_rules_enforcer.py +9 -5
- package/hooks/blocking/code_rules_test_layout.py +6 -7
- package/hooks/blocking/test_code_rules_constants_config.py +13 -0
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +0 -1
- package/hooks/blocking/test_code_rules_enforcer_dispatch_wiring.py +15 -0
- package/hooks/blocking/test_code_rules_enforcer_split_constants_config.py +0 -60
- package/hooks/blocking/test_context_control_policy_files.py +0 -11
- package/hooks/hooks_constants/blocking_check_limits.py +1 -1
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +0 -1
- package/hooks/hooks_constants/setup_project_paths_constants.py +1 -2
- package/hooks/session/test_untracked_repo_detector.py +1 -1
- package/hooks/validators/code_quality_checks.py +15 -2
- package/hooks/validators/test_code_quality_checks.py +22 -0
- package/package.json +1 -2
- package/rules/ci-owns-the-gate.md +58 -0
- package/rules/code-standards.md +1 -2
- package/rules/docstring-prose-matches-implementation.md +2 -2
- package/rules/failure-blast-radius.md +1 -1
- package/rules/git-workflow.md +23 -11
- package/rules/orphan-css-class.md +1 -1
- package/rules/paired-test-coverage.md +1 -1
- package/rules/retired-hook-prose.md +1 -1
- package/rules/verify-before-asking.md +1 -1
- package/rules/windows-filesystem-safe.md +1 -1
- package/rules/workflow-substitution-slots.md +1 -1
- package/scripts/Sync-RepoMain.ps1 +71 -6
- package/scripts/policy_lint/adapter_detectors.py +116 -10
- package/scripts/policy_lint/config/approved_test_pairs.py +12 -0
- package/scripts/policy_lint/config/constants.py +3 -0
- package/scripts/tests/Sync-RepoMain.Tests.ps1 +108 -0
- package/scripts/tests/test_policy_lint_rules_validator_baseline.py +126 -0
- package/.agents/skills/session-continuity/AGENTS.md +0 -1
- package/.agents/skills/session-continuity/CLAUDE.md +0 -1
- package/.agents/skills/session-continuity/README.md +0 -223
- package/.agents/skills/session-continuity/SKILL.md +0 -125
- package/.agents/skills/session-continuity/continuity.mjs +0 -445
- package/.agents/skills/session-continuity/continuity.test.mjs +0 -381
- package/bin/install-session-continuity.mjs +0 -131
- package/bin/install-session-continuity.test.mjs +0 -106
- package/bin/install.pstack-plugin.test.mjs +0 -115
- package/bin/pstack-codex-discovery.test.mjs +0 -79
- package/bin/pstack-model-preferences.claude.json +0 -23
- package/bin/pstack-model-preferences.codex.json +0 -24
- package/bin/pstack.mjs +0 -392
- package/bin/pstack.test.mjs +0 -347
- package/bin/refresh-pstack-plugin-skills.test.mjs +0 -211
- package/bin/select-pstack-models.test.mjs +0 -776
- package/hooks/blocking/test_code_rules_enforcer_file_global_constants.py +0 -231
- package/rules/agent-spawn-protocol.md +0 -9
- package/rules/ask-user-question-required.md +0 -31
- package/rules/claims-as-quotes.md +0 -55
- package/rules/confirm-implementation-forks.md +0 -11
- package/rules/file-global-constants.md +0 -81
- package/rules/hedging-claims.md +0 -7
- package/rules/measurement-denominators.md +0 -39
- package/rules/prompt-workflow-context-controls.md +0 -19
- package/rules/pstack-host-mapping.md +0 -48
- package/rules/pstack-models.md +0 -142
- package/rules/vault-context.md +0 -5
- package/scripts/pstack-adapters/common.md +0 -17
- package/scripts/pstack-adapters/create-skill.md +0 -14
- package/scripts/pstack-adapters/host-claude.md +0 -7
- package/scripts/pstack-adapters/host-codex.md +0 -11
- package/scripts/pstack-adapters/host-cursor.md +0 -7
- package/scripts/pstack.lock.json +0 -11
- package/scripts/refresh_pstack_plugin_skills.mjs +0 -200
- package/scripts/select_pstack_models.mjs +0 -469
- /package/.agents/{skills → skills-archived}/source-command-logifix/SKILL.md +0 -0
|
@@ -60,7 +60,6 @@ from code_rules_constants_config import ( # noqa: E402
|
|
|
60
60
|
check_config_duplicate_path_anchor,
|
|
61
61
|
check_constants_outside_config,
|
|
62
62
|
check_constants_outside_config_advisory,
|
|
63
|
-
check_file_global_constants_use_count,
|
|
64
63
|
)
|
|
65
64
|
from code_rules_docstrings import ( # noqa: E402
|
|
66
65
|
check_class_docstring_names_public_methods,
|
|
@@ -358,7 +357,6 @@ def _python_magic_value_and_constant_issues(context: _ValidationContext) -> list
|
|
|
358
357
|
all_issues.extend(check_fstring_structural_literals(content, file_path))
|
|
359
358
|
all_issues.extend(check_constants_outside_config(content, file_path))
|
|
360
359
|
check_constants_outside_config_advisory(content, file_path)
|
|
361
|
-
all_issues.extend(check_file_global_constants_use_count(content, file_path))
|
|
362
360
|
return all_issues
|
|
363
361
|
|
|
364
362
|
|
|
@@ -1240,7 +1238,9 @@ def _deny_reason_for_issues(
|
|
|
1240
1238
|
Returns:
|
|
1241
1239
|
The complete ``permissionDecisionReason`` text.
|
|
1242
1240
|
"""
|
|
1243
|
-
issue_list =
|
|
1241
|
+
issue_list = VIOLATION_SEPARATOR.join(
|
|
1242
|
+
all_blocking_issues[:DENY_REASON_ISSUE_PREVIEW_COUNT]
|
|
1243
|
+
)
|
|
1244
1244
|
deny_reason = (
|
|
1245
1245
|
f"BLOCKED: [CODE_RULES] {len(all_blocking_issues)} violation(s): {issue_list}"
|
|
1246
1246
|
)
|
|
@@ -1257,7 +1257,9 @@ def _deny_reason_for_issues(
|
|
|
1257
1257
|
all_blocking_issues=all_blocking_issues,
|
|
1258
1258
|
)
|
|
1259
1259
|
if forecast_issues:
|
|
1260
|
-
forecast_list =
|
|
1260
|
+
forecast_list = VIOLATION_SEPARATOR.join(
|
|
1261
|
+
forecast_issues[:DENY_REASON_ISSUE_PREVIEW_COUNT]
|
|
1262
|
+
)
|
|
1261
1263
|
deny_reason += (
|
|
1262
1264
|
f"; FULL-FILE FORECAST — {len(forecast_issues)} additional "
|
|
1263
1265
|
"violation(s) elsewhere in this file will block future edits "
|
|
@@ -1362,7 +1364,9 @@ def _report_hook_blocking_issues(
|
|
|
1362
1364
|
)
|
|
1363
1365
|
if not all_blocking_issues:
|
|
1364
1366
|
return
|
|
1365
|
-
issue_list =
|
|
1367
|
+
issue_list = VIOLATION_SEPARATOR.join(
|
|
1368
|
+
all_blocking_issues[:DENY_REASON_ISSUE_PREVIEW_COUNT]
|
|
1369
|
+
)
|
|
1366
1370
|
deny_reason = (
|
|
1367
1371
|
f"BLOCKED: [CODE_RULES] {len(all_blocking_issues)} violation(s): {issue_list}"
|
|
1368
1372
|
+ _precheck_hint()
|
|
@@ -10,8 +10,8 @@ test functions, which exports nothing, so a single-file scan proves a constant
|
|
|
10
10
|
no other line reads is dead. A ``conftest.py``, a shared helper module reached
|
|
11
11
|
only through the ``tests/`` directory gate, and a dedicated constants or
|
|
12
12
|
``config/`` module all export their constants to importer modules, so the
|
|
13
|
-
audit lane governs their constants cross-module
|
|
14
|
-
|
|
13
|
+
audit lane governs their constants cross-module and this write-time check
|
|
14
|
+
leaves them alone.
|
|
15
15
|
"""
|
|
16
16
|
|
|
17
17
|
import ast
|
|
@@ -54,7 +54,7 @@ def _is_dead_constant_scan_target(file_path: str) -> bool:
|
|
|
54
54
|
constant no other line reads is dead. A ``conftest.py``, a shared helper
|
|
55
55
|
module reached only through the ``tests/`` directory gate, and a dedicated
|
|
56
56
|
constants or ``config/`` module all export to importer modules, where the
|
|
57
|
-
cross-module audit lane governs their constants
|
|
57
|
+
cross-module audit lane governs their constants.
|
|
58
58
|
|
|
59
59
|
Args:
|
|
60
60
|
file_path: The destination path of the write.
|
|
@@ -77,8 +77,8 @@ def _is_private_constant_name(name: str) -> bool:
|
|
|
77
77
|
EXPECTED_TOTAL -> public constant, left alone
|
|
78
78
|
|
|
79
79
|
Only a leading-underscore name qualifies. A public module constant exports
|
|
80
|
-
to importer modules, so the cross-module audit lane governs it
|
|
81
|
-
|
|
80
|
+
to importer modules, so the cross-module audit lane governs it and this
|
|
81
|
+
single-file scan leaves it alone.
|
|
82
82
|
"""
|
|
83
83
|
if not name.startswith(PRIVATE_NAME_PREFIX):
|
|
84
84
|
return False
|
|
@@ -163,8 +163,7 @@ def check_dead_test_module_constant(content: str, file_path: str) -> list[str]:
|
|
|
163
163
|
dead scaffolding an edit stranded. A public constant, a ``conftest.py``, a
|
|
164
164
|
shared helper module under ``tests/``, and a dedicated constants or
|
|
165
165
|
``config/`` module all export to importer modules, so the cross-module
|
|
166
|
-
audit lane judges those
|
|
167
|
-
check skips them.
|
|
166
|
+
audit lane judges those and this check skips them.
|
|
168
167
|
|
|
169
168
|
Args:
|
|
170
169
|
content: The post-edit file content under validation.
|
|
@@ -24,3 +24,16 @@ def test_module_source_carries_no_docstring_magic_value() -> None:
|
|
|
24
24
|
"bare-number token that the magic-value check flags as a literal, "
|
|
25
25
|
f"got: {magic_value_issues}"
|
|
26
26
|
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
ARCHIVED_USE_COUNT_CHECK_NAME = "check_file_global_constants_use_count"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_module_defines_no_check_for_an_archived_rule() -> None:
|
|
33
|
+
module_path = Path(__file__).resolve().parent / "code_rules_constants_config.py"
|
|
34
|
+
module_source = module_path.read_text(encoding="utf-8")
|
|
35
|
+
assert ARCHIVED_USE_COUNT_CHECK_NAME not in module_source, (
|
|
36
|
+
"The file-global constant use-count rule moved to rules-archived, so this "
|
|
37
|
+
"module must define no check that enforces it. Code left behind keeps "
|
|
38
|
+
"enforcing a rule the package no longer documents."
|
|
39
|
+
)
|
|
@@ -71,7 +71,6 @@ KNOWN_UNCAPPED_CHECKS_PENDING_REVIEW: frozenset[str] = frozenset(
|
|
|
71
71
|
"check_constant_equality_tests",
|
|
72
72
|
"check_constants_outside_config",
|
|
73
73
|
"check_existence_check_tests",
|
|
74
|
-
"check_file_global_constants_use_count",
|
|
75
74
|
"check_imports_at_top",
|
|
76
75
|
"check_inline_literal_collections",
|
|
77
76
|
"check_join_separator_string_magic",
|
|
@@ -173,3 +173,18 @@ def test_known_undispatched_set_lists_only_existing_checks() -> None:
|
|
|
173
173
|
f"KNOWN_UNDISPATCHED_CHECKS lists functions that no longer exist: "
|
|
174
174
|
f"{sorted(stale_names)}. Restore the function or remove it from the set."
|
|
175
175
|
)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
ARCHIVED_USE_COUNT_CHECK_NAME = "check_file_global_constants_use_count"
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def test_enforcer_dispatches_no_check_for_an_archived_rule() -> None:
|
|
182
|
+
enforcer_source = (_HOOK_DIRECTORY / "code_rules_enforcer.py").read_text(
|
|
183
|
+
encoding="utf-8"
|
|
184
|
+
)
|
|
185
|
+
assert ARCHIVED_USE_COUNT_CHECK_NAME not in enforcer_source, (
|
|
186
|
+
"The file-global constant use-count rule moved to rules-archived, so the "
|
|
187
|
+
"enforcer must import and dispatch no check that enforces it. A check "
|
|
188
|
+
"left wired here blocks a write against a rule the package no longer "
|
|
189
|
+
"documents."
|
|
190
|
+
)
|
|
@@ -20,7 +20,6 @@ from code_rules_constants_config import ( # noqa: E402
|
|
|
20
20
|
_scan_function_body_constants,
|
|
21
21
|
check_constants_outside_config,
|
|
22
22
|
check_constants_outside_config_advisory,
|
|
23
|
-
check_file_global_constants_use_count,
|
|
24
23
|
)
|
|
25
24
|
|
|
26
25
|
code_rules_enforcer = SimpleNamespace(
|
|
@@ -28,7 +27,6 @@ code_rules_enforcer = SimpleNamespace(
|
|
|
28
27
|
_scan_function_body_constants=_scan_function_body_constants,
|
|
29
28
|
check_constants_outside_config=check_constants_outside_config,
|
|
30
29
|
check_constants_outside_config_advisory=check_constants_outside_config_advisory,
|
|
31
|
-
check_file_global_constants_use_count=check_file_global_constants_use_count,
|
|
32
30
|
)
|
|
33
31
|
|
|
34
32
|
|
|
@@ -37,37 +35,6 @@ CONSTANTS_OUTSIDE_CONFIG_PRODUCTION_FILE_PATH = "packages/app/services/encoding.
|
|
|
37
35
|
PRODUCTION_FILE_PATH = "packages/claude-dev-env/hooks/blocking/example_production.py"
|
|
38
36
|
|
|
39
37
|
|
|
40
|
-
def test_should_flag_constant_used_only_in_class_level_decorator() -> None:
|
|
41
|
-
source = (
|
|
42
|
-
"TIMEOUT = 5\n"
|
|
43
|
-
"\n"
|
|
44
|
-
"def register(value):\n"
|
|
45
|
-
" def wrap(cls):\n"
|
|
46
|
-
" return cls\n"
|
|
47
|
-
" return wrap\n"
|
|
48
|
-
"\n"
|
|
49
|
-
"@register(TIMEOUT)\n"
|
|
50
|
-
"class Foo:\n"
|
|
51
|
-
" pass\n"
|
|
52
|
-
)
|
|
53
|
-
issues = code_rules_enforcer.check_file_global_constants_use_count(
|
|
54
|
-
source, PRODUCTION_FILE_PATH
|
|
55
|
-
)
|
|
56
|
-
assert any(
|
|
57
|
-
"TIMEOUT" in issue and "only 1 function/method" in issue for issue in issues
|
|
58
|
-
), f"Expected class-decorator usage to register as a caller, got: {issues}"
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def test_should_flag_constant_used_once_at_module_scope_and_once_in_function() -> None:
|
|
62
|
-
source = "UPPER = 1\nSHADOW = UPPER\n\ndef lonely_caller():\n return UPPER\n"
|
|
63
|
-
issues = code_rules_enforcer.check_file_global_constants_use_count(
|
|
64
|
-
source, PRODUCTION_FILE_PATH
|
|
65
|
-
)
|
|
66
|
-
assert issues == [], (
|
|
67
|
-
f"Expected module-scope + function usage to count as 2 distinct callers, got: {issues}"
|
|
68
|
-
)
|
|
69
|
-
|
|
70
|
-
|
|
71
38
|
def test_is_exempt_for_advisory_scan_returns_true_for_config_file() -> None:
|
|
72
39
|
assert code_rules_enforcer._is_exempt_for_advisory_scan("project/config/constants.py") is True
|
|
73
40
|
|
|
@@ -241,30 +208,3 @@ def test_check_constants_outside_config_reports_more_than_three_constants() -> N
|
|
|
241
208
|
)
|
|
242
209
|
|
|
243
210
|
|
|
244
|
-
_SINGLE_CALLER_CONSTANT_SOURCE = (
|
|
245
|
-
"TIMEOUT = 5\n"
|
|
246
|
-
"\n"
|
|
247
|
-
"def lonely_caller() -> int:\n"
|
|
248
|
-
" return TIMEOUT\n"
|
|
249
|
-
)
|
|
250
|
-
|
|
251
|
-
_ENFORCER_ENTRY_FILE_PATH = "packages/claude-dev-env/hooks/blocking/code_rules_enforcer.py"
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
def test_use_count_flags_single_caller_constant_for_ordinary_production_path() -> None:
|
|
255
|
-
issues = code_rules_enforcer.check_file_global_constants_use_count(
|
|
256
|
-
_SINGLE_CALLER_CONSTANT_SOURCE, PRODUCTION_FILE_PATH
|
|
257
|
-
)
|
|
258
|
-
assert any(
|
|
259
|
-
"TIMEOUT" in issue and "only 1 function/method" in issue for issue in issues
|
|
260
|
-
), f"Expected single-caller constant flagged on an ordinary production path, got: {issues}"
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
def test_use_count_exempts_enforcer_entry_module_path() -> None:
|
|
264
|
-
issues = code_rules_enforcer.check_file_global_constants_use_count(
|
|
265
|
-
_SINGLE_CALLER_CONSTANT_SOURCE, _ENFORCER_ENTRY_FILE_PATH
|
|
266
|
-
)
|
|
267
|
-
assert issues == [], (
|
|
268
|
-
"The enforcer entry module must be exempt to avoid self-blocking, "
|
|
269
|
-
f"got: {issues}"
|
|
270
|
-
)
|
|
@@ -4,20 +4,9 @@ from pathlib import Path
|
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
ROOT = Path(__file__).resolve().parents[2]
|
|
7
|
-
RULE_PATH = ROOT / "rules" / "prompt-workflow-context-controls.md"
|
|
8
7
|
HOOK_SPEC_PATH = ROOT / "hooks" / "HOOK_SPECS_PROMPT_WORKFLOW.md"
|
|
9
8
|
|
|
10
9
|
|
|
11
|
-
def test_context_control_rule_exists_with_required_sections() -> None:
|
|
12
|
-
text = RULE_PATH.read_text(encoding="utf-8")
|
|
13
|
-
assert "Base Minimal Instruction Layer" in text
|
|
14
|
-
assert "On-Demand Skill Loading" in text
|
|
15
|
-
assert "Compaction and Caching Strategy" in text
|
|
16
|
-
assert "Runtime Enforcement Signals" in text
|
|
17
|
-
assert "base_minimal_instruction_layer: true" in text
|
|
18
|
-
assert "on_demand_skill_loading: true" in text
|
|
19
|
-
|
|
20
|
-
|
|
21
10
|
def test_hook_spec_exists_with_required_gates() -> None:
|
|
22
11
|
text = HOOK_SPEC_PATH.read_text(encoding="utf-8")
|
|
23
12
|
assert "PreToolUse Task/Agent (removed)" in text
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Each constant is consumed by exactly one check function in the enforcer. They
|
|
4
4
|
live here (not at module scope of the enforcer) so the enforcer file stays
|
|
5
|
-
|
|
5
|
+
because each has more than one consumer in this package.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
@@ -185,7 +185,6 @@ DEAD_SPLIT_BRANCH_MESSAGE_SUFFIX: str = (
|
|
|
185
185
|
)
|
|
186
186
|
DUPLICATED_FORMAT_MINIMUM_REPETITION_COUNT = 3
|
|
187
187
|
DUPLICATED_FORMAT_MINIMUM_LITERAL_CHARACTER_COUNT = 5
|
|
188
|
-
FILE_GLOBAL_UPPER_SNAKE_PATTERN = re.compile(r"^_?[A-Z][A-Z0-9_]*$")
|
|
189
188
|
|
|
190
189
|
ALL_COLLECTION_TYPE_NAMES: frozenset[str] = frozenset({
|
|
191
190
|
"list", "tuple", "set", "frozenset",
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
"""Configuration constants for the setup_project_paths bootstrap script.
|
|
2
2
|
|
|
3
3
|
Shared constants consumed by two or more modules across the hook subsystem.
|
|
4
|
-
Single-use values are inlined into their consuming functions
|
|
5
|
-
file-global-constants use-count rule.
|
|
4
|
+
Single-use values are inlined into their consuming functions.
|
|
6
5
|
"""
|
|
7
6
|
|
|
8
7
|
from __future__ import annotations
|
|
@@ -151,7 +151,7 @@ class TestSharedRegistryPath:
|
|
|
151
151
|
def test_config_file_path_not_a_module_level_attribute(self) -> None:
|
|
152
152
|
"""Pin PR #230 round 7: _CONFIG_FILE_PATH inlined into _build_confirm_instruction.
|
|
153
153
|
|
|
154
|
-
Single-consumer module-level constant
|
|
154
|
+
Single-consumer module-level constant kept in the scope that reads it.
|
|
155
155
|
"""
|
|
156
156
|
assert not hasattr(detector, "_CONFIG_FILE_PATH")
|
|
157
157
|
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
"""Code quality checks validator.
|
|
2
2
|
|
|
3
3
|
Implements:
|
|
4
|
-
- Check 30: Function too long (max 30 lines)
|
|
4
|
+
- Check 30: Function too long (max 30 lines), production files only
|
|
5
5
|
- Check 31: Nesting too deep (max 2 levels)
|
|
6
|
-
- Check 32: File too long (max 400 lines)
|
|
6
|
+
- Check 32: File too long (max 400 lines), production files only
|
|
7
|
+
|
|
8
|
+
The two length caps read production files only. A test module grows with the
|
|
9
|
+
surface it covers, and a linear list of assertions is what a reader wants there,
|
|
10
|
+
so capping its length pushes a contributor to split a suite for the counter
|
|
11
|
+
rather than for the reader. ``magic_value_checks`` short-circuits on the same
|
|
12
|
+
category through the same helper.
|
|
7
13
|
"""
|
|
8
14
|
|
|
9
15
|
import ast
|
|
@@ -11,6 +17,7 @@ import sys
|
|
|
11
17
|
from pathlib import Path
|
|
12
18
|
from typing import List
|
|
13
19
|
|
|
20
|
+
from .exempt_paths import is_test_file
|
|
14
21
|
from .validator_base import Violation, source_text, syntax_tree
|
|
15
22
|
|
|
16
23
|
|
|
@@ -22,6 +29,9 @@ MAX_FILE_LINES = 400
|
|
|
22
29
|
def check_function_length(tree: ast.AST, filename: str) -> List[Violation]:
|
|
23
30
|
violations: List[Violation] = []
|
|
24
31
|
|
|
32
|
+
if is_test_file(filename):
|
|
33
|
+
return violations
|
|
34
|
+
|
|
25
35
|
for node in ast.walk(tree):
|
|
26
36
|
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
27
37
|
if node.end_lineno and node.lineno:
|
|
@@ -74,6 +84,9 @@ def check_file_length(file_path: Path) -> List[Violation]:
|
|
|
74
84
|
violations: List[Violation] = []
|
|
75
85
|
filename = str(file_path)
|
|
76
86
|
|
|
87
|
+
if is_test_file(filename):
|
|
88
|
+
return violations
|
|
89
|
+
|
|
77
90
|
try:
|
|
78
91
|
lines = source_text(file_path).splitlines()
|
|
79
92
|
except Exception as error:
|
|
@@ -127,3 +127,25 @@ class TestFileLength:
|
|
|
127
127
|
assert "400" in violations[0].message or "lines" in violations[0].message.lower()
|
|
128
128
|
finally:
|
|
129
129
|
temp_path.unlink()
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class TestTestFileExemption:
|
|
133
|
+
def test_long_function_in_a_test_file_passes(self) -> None:
|
|
134
|
+
tree = ast.parse(BAD_LONG_FUNCTION)
|
|
135
|
+
violations = check_function_length(tree, "packages/app/test_widget.py")
|
|
136
|
+
assert violations == []
|
|
137
|
+
|
|
138
|
+
def test_long_function_outside_a_test_file_still_fails(self) -> None:
|
|
139
|
+
tree = ast.parse(BAD_LONG_FUNCTION)
|
|
140
|
+
violations = check_function_length(tree, "packages/app/widget.py")
|
|
141
|
+
assert len(violations) == 1
|
|
142
|
+
|
|
143
|
+
def test_long_test_file_passes(self, tmp_path: Path) -> None:
|
|
144
|
+
temp_path = tmp_path / "test_widget.py"
|
|
145
|
+
temp_path.write_text("x = 1\n" * 450, encoding="utf-8")
|
|
146
|
+
assert check_file_length(temp_path) == []
|
|
147
|
+
|
|
148
|
+
def test_long_conftest_passes(self, tmp_path: Path) -> None:
|
|
149
|
+
temp_path = tmp_path / "conftest.py"
|
|
150
|
+
temp_path.write_text("x = 1\n" * 450, encoding="utf-8")
|
|
151
|
+
assert check_file_length(temp_path) == []
|
package/package.json
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-dev-env",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "8.0.0",
|
|
4
4
|
"description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"claude-dev-env": "bin/install.mjs",
|
|
8
|
-
"cde-pstack": "bin/pstack.mjs",
|
|
9
8
|
"codex-compat": "bin/codex-compat.mjs",
|
|
10
9
|
"cde": "bin/cde.mjs"
|
|
11
10
|
},
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# CI Owns the Gate
|
|
2
|
+
|
|
3
|
+
**When this applies:** Before pushing a branch, and any time a repository's full
|
|
4
|
+
check suite or policy gate is about to run on your own machine.
|
|
5
|
+
|
|
6
|
+
## Rule
|
|
7
|
+
|
|
8
|
+
The gate runs once, and it runs on CI. Push the branch, read the verdict, act on
|
|
9
|
+
what it says.
|
|
10
|
+
|
|
11
|
+
The inner development loop stays yours. Run the single test you are writing, as
|
|
12
|
+
often as it helps. That is how the change gets built. This rule governs the
|
|
13
|
+
second full pass, the one whose only product is a prediction of CI's answer.
|
|
14
|
+
Push instead, and spend the wait on the next piece of work.
|
|
15
|
+
|
|
16
|
+
## Why
|
|
17
|
+
|
|
18
|
+
**A pinned gate answers only from its pinned revision.** The workflow names an
|
|
19
|
+
exact revision of the policy package, and that revision decides which rules run.
|
|
20
|
+
The copy under your home directory is a separate artifact at its own revision. A
|
|
21
|
+
run against the home copy reports on those rules, which are a different question
|
|
22
|
+
from the one CI asks. Treat its exit code as information about the home copy
|
|
23
|
+
alone.
|
|
24
|
+
|
|
25
|
+
**Self-hosted runners often share your machine.** Where the runners execute on
|
|
26
|
+
the same host as your shell, a local suite run draws its processor time from the
|
|
27
|
+
runners, and it does so while they work on the branch you just pushed. Read
|
|
28
|
+
where the runners live, and count a local run against the same budget.
|
|
29
|
+
|
|
30
|
+
**One authoritative answer beats two.** Where both runs agree, the second one
|
|
31
|
+
restated the first. Where they differ, the environments differ, and CI is the
|
|
32
|
+
environment that decides.
|
|
33
|
+
|
|
34
|
+
## Running a gate locally
|
|
35
|
+
|
|
36
|
+
Clone the revision the workflow pins, then point the gate at that clone. That
|
|
37
|
+
run asks CI's question and its answer carries. Report a local result by naming
|
|
38
|
+
the revision it used, so a reader can tell which question it answered.
|
|
39
|
+
|
|
40
|
+
## The verdict belongs to CI
|
|
41
|
+
|
|
42
|
+
CI decides whether a change passed, from evidence CI gathered. Keep that loop
|
|
43
|
+
closed. A flag, trailer, receipt, or environment variable through which the
|
|
44
|
+
change under test announces its own result hands the verdict to the subject.
|
|
45
|
+
Where the runner and the agent share one host, a signature names the same party
|
|
46
|
+
twice, so it carries the claim no further.
|
|
47
|
+
|
|
48
|
+
A cache stays available on one condition. CI derives the key itself from the
|
|
49
|
+
tree it is about to test, looks for a previous run under that key, and
|
|
50
|
+
republishes that result. CI computes, CI verifies, CI decides.
|
|
51
|
+
|
|
52
|
+
## Sibling rules
|
|
53
|
+
|
|
54
|
+
| Rule | Role |
|
|
55
|
+
|---|---|
|
|
56
|
+
| [`git-workflow.md`](git-workflow.md) | Confirm each required context fired after the push |
|
|
57
|
+
| [`verify-runtime-state.md`](verify-runtime-state.md) | A status field is a report; read the thing the work was meant to make |
|
|
58
|
+
| [`falsify-before-green.md`](falsify-before-green.md) | A green counts as evidence once the check has run red on a deliberate break |
|
package/rules/code-standards.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> **Canonical review contract:** [`CODE_RULES.md`](../docs/CODE_RULES.md) — the human and AI review contract for code quality, loaded on demand.
|
|
4
4
|
> **Checked-in pointer:** [`.cursor/BUGBOT.md`](../../../.cursor/BUGBOT.md) — the file Cursor BugBot reads; it points at `CODE_RULES.md`.
|
|
5
|
-
> **Production enforcement.** The staged policy lint runs `hooks/blocking/code_rules_enforcer.py` over each changed file. No write-time hook runs it.
|
|
5
|
+
> **Production enforcement.** The staged policy lint runs `hooks/blocking/code_rules_enforcer.py` over each changed file. No write-time hook runs it. CI runs that lint against the merge base. Each mechanical rule carries a synchronization test.
|
|
6
6
|
|
|
7
7
|
## Policy surface map
|
|
8
8
|
|
|
@@ -27,7 +27,6 @@ BDD is the outer process and TDD is the inner loop: [`bdd.md`](bdd.md) discovers
|
|
|
27
27
|
|
|
28
28
|
| Concern | Rule file |
|
|
29
29
|
|---|---|
|
|
30
|
-
| Question routing | [`ask-user-question-required.md`](ask-user-question-required.md) |
|
|
31
30
|
| Task tracking / worker completion | [`workers-done-before-complete.md`](workers-done-before-complete.md) |
|
|
32
31
|
| Multi-step task list | skill `task-build` (see agents catalog) |
|
|
33
32
|
|
|
@@ -28,13 +28,13 @@ Read the body and the docstring side by side. Apply each check that matches the
|
|
|
28
28
|
- **Gate-outcome status flags** — an outcome routed to a blocker (`blocker = ...; break`) reads as blocked everywhere, never as a bypass.
|
|
29
29
|
- **Returns / Raises / Note claims** — each free-form claim matches the body.
|
|
30
30
|
|
|
31
|
-
Many deterministic shapes of this drift are checked in `code_rules_docstrings.py` (and the JS and `.mjs` slices in `code_rules_imports_logging.py`). The staged policy lint reaches both through `code_rules_enforcer.py`, and
|
|
31
|
+
Many deterministic shapes of this drift are checked in `code_rules_docstrings.py` (and the JS and `.mjs` slices in `code_rules_imports_logging.py`). The staged policy lint reaches both through `code_rules_enforcer.py`, and CI runs it against the merge base. Free-form rest is judgment.
|
|
32
32
|
|
|
33
33
|
## Hook prose matches its detector
|
|
34
34
|
|
|
35
35
|
A hook module is the sharpest case of the same rule: its docstring lead narrative and its `CORRECTIVE_MESSAGE` describe exactly the shapes the detector flags, and claim no broader trigger surface than the regex enforces.
|
|
36
36
|
|
|
37
|
-
The staged policy lint carries this as its `hook-prose-consistency` rule, covering hook modules and their `*_constants.py` companions. It reports prose that claims a trigger the detector never fires on, and names the fix. No write-time hook runs it, so
|
|
37
|
+
The staged policy lint carries this as its `hook-prose-consistency` rule, covering hook modules and their `*_constants.py` companions. It reports prose that claims a trigger the detector never fires on, and names the fix. No write-time hook runs it, so CI is where it reports.
|
|
38
38
|
|
|
39
39
|
After writing a hook, ask: would a token matching every word of this message actually trip the detector? When the message names a shape the regex skips, rewrite the message to name only what the regex catches. The path-shape case is the common overstatement — a detector that keys off a path separator must not claim it blocks an "output-key segment". The corrective message spells the rewrite.
|
|
40
40
|
|
|
@@ -64,7 +64,7 @@ The report then presents each candidate to the owner for a durable-fix decision,
|
|
|
64
64
|
|
|
65
65
|
## Enforcement
|
|
66
66
|
|
|
67
|
-
`code_rules_blast_radius.py` runs inside `code_rules_enforcer.py`, which the staged policy lint applies to each changed file under its `code-rules` rule. No write-time hook runs it, so
|
|
67
|
+
`code_rules_blast_radius.py` runs inside `code_rules_enforcer.py`, which the staged policy lint applies to each changed file under its `code-rules` rule. No write-time hook runs it, so CI is where it reports. CI runs the same lint against the merge base.
|
|
68
68
|
|
|
69
69
|
The check requires each raised type written directly inside a loop body to end in `RunFatal` or `ItemBlocked`. The lexical check covers raises written directly in loop bodies. Shared helpers carry multiple caller contexts, so their callers classify the boundary.
|
|
70
70
|
|
package/rules/git-workflow.md
CHANGED
|
@@ -29,9 +29,10 @@ opens with the bot's own marker line. The description rules in this file, the
|
|
|
29
29
|
failure signature in the release job log reads
|
|
30
30
|
`could not parse pull request body as a release PR`.
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
The `pr-description-writer` agent writes a title and body from the diff when
|
|
33
|
+
you want one. Publish the title and body file through
|
|
34
|
+
`~/.agents/skills/pull-request/scripts/pull_request.py`. That path is under the
|
|
35
|
+
agents home, not the repository. A worktree holds no `.agents/` copy.
|
|
35
36
|
|
|
36
37
|
Resolve the active managed root (`CLAUDE_CONFIG_DIR` when set, `~/.claude`
|
|
37
38
|
otherwise), then run `<managed-root>/scripts/durable_post_lint.py` before any
|
|
@@ -50,14 +51,25 @@ untouched.
|
|
|
50
51
|
- Before push: `gh pr ready --undo`
|
|
51
52
|
- After review approved: `gh pr ready`
|
|
52
53
|
|
|
53
|
-
##
|
|
54
|
+
## Confirm the required checks fired, and let CI run them
|
|
54
55
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
56
|
+
The gate runs once, and it runs on CI. Push the branch and read its verdict.
|
|
57
|
+
[`ci-owns-the-gate.md`](ci-owns-the-gate.md) holds the reasoning and the shape
|
|
58
|
+
a local run takes when one is warranted.
|
|
59
|
+
|
|
60
|
+
Read the branch ruleset for the required check contexts before you push a
|
|
61
|
+
branch, or any level of a stack: `gh api repos/<owner>/<repo>/rules/branches/<trunk>`.
|
|
62
|
+
Read it to learn which checks must report. After the push, confirm each of those
|
|
63
|
+
contexts appears on that level's head. A required check that never fired is
|
|
64
|
+
invisible debt at every level, and it surfaces only after the whole stack is
|
|
65
|
+
pushed, when the repair costs a second pass over every branch.
|
|
66
|
+
|
|
67
|
+
A red required check blocks the branch, whoever owns the failing line. The gate
|
|
68
|
+
charges a change for the whole file it touches, so "pre-existing on main" is a
|
|
69
|
+
blame note, not a pass. Fix the line in the next push or report the branch
|
|
70
|
+
blocked. Read the gate's own report rather than a narrower substitute. A
|
|
71
|
+
single-file mypy call cannot see sibling modules and reports false import
|
|
72
|
+
errors, so it neither clears nor convicts a change.
|
|
61
73
|
|
|
62
74
|
A checks listing that reports nothing on the branch is a finding, not a neutral
|
|
63
75
|
state. Find out whether the workflow's event filters exclude the branch, or whether
|
|
@@ -82,7 +94,7 @@ what it calls.
|
|
|
82
94
|
Force-with-lease protects the ref. It protects nobody's understanding of what the
|
|
83
95
|
branch now holds. When a rewrite moves content between levels of a stack, or
|
|
84
96
|
otherwise changes what a branch contains, refresh that pull request's description
|
|
85
|
-
|
|
97
|
+
before you ask anyone to read or merge it.
|
|
86
98
|
|
|
87
99
|
## Never commit working documents or images
|
|
88
100
|
|
|
@@ -15,7 +15,7 @@ When you add a `class="..."` attribute, add its `.<class>` selector to the `<sty
|
|
|
15
15
|
|
|
16
16
|
## What the check covers
|
|
17
17
|
|
|
18
|
-
The `check_orphan_css_classes` check in `code_rules_orphan_css_class.py` reaches production Python through the staged policy lint, whose `code-rules` rule loads `code_rules_enforcer.py` and applies it to each changed file.
|
|
18
|
+
The `check_orphan_css_classes` check in `code_rules_orphan_css_class.py` reaches production Python through the staged policy lint, whose `code-rules` rule loads `code_rules_enforcer.py` and applies it to each changed file. CI runs that lint against the merge base. It:
|
|
19
19
|
|
|
20
20
|
1. Collects each class name referenced in a `class="..."` attribute across the file's string literals.
|
|
21
21
|
2. Collects each class selector defined in a `<style>` block — both in the file under edit and in every Python module beside it (its own directory and immediate child directories), since a markup module commonly imports its style constant from a companion package directory.
|
|
@@ -18,7 +18,7 @@ When you add a public function to a module whose test suite already exercises th
|
|
|
18
18
|
|
|
19
19
|
## What the check covers
|
|
20
20
|
|
|
21
|
-
Two complementary checks in `code_rules_paired_test.py` reach changed files through `code_rules_enforcer.py`, which the staged policy lint runs under its `code-rules` rule. No write-time hook runs them, so
|
|
21
|
+
Two complementary checks in `code_rules_paired_test.py` reach changed files through `code_rules_enforcer.py`, which the staged policy lint runs under its `code-rules` rule. No write-time hook runs them, so CI is where they report, against the merge base. The two checks cover the two write orders.
|
|
22
22
|
|
|
23
23
|
`check_public_function_missing_paired_test` runs on a production Python write or edit and flags a public function when all of these hold:
|
|
24
24
|
|
|
@@ -7,7 +7,7 @@ paths:
|
|
|
7
7
|
|
|
8
8
|
A hook module runs when `hooks/hooks.json` registers it or a dispatcher roster hosts it. A module can survive on disk while appearing in neither place, and it then reaches no tool call. Prose that gives such a module a present-tense action describes a gate the reader never faces, and a reader who trusts it skips the check they were meant to run by hand.
|
|
9
9
|
|
|
10
|
-
The staged policy lint carries this check as its `retired-hook-prose` rule. It reads each Markdown file under a `rules/` directory, takes every backticked hook module name, and reports the ones that are registered nowhere and that carry a present-tense action verb in the rest of the sentence.
|
|
10
|
+
The staged policy lint carries this check as its `retired-hook-prose` rule. It reads each Markdown file under a `rules/` directory, takes every backticked hook module name, and reports the ones that are registered nowhere and that carry a present-tense action verb in the rest of the sentence. CI runs that lint against the merge base.
|
|
11
11
|
|
|
12
12
|
A module name counts as a hook when it ends in one of the hook families, such as `_blocker`, `_enforcer`, `_gate`, `_tracker`, or `_dispatcher`, and when it either sits under `hooks/` or appears in `RETIRED_HOOK_REGISTRATION_RELATIVE_PATHS` in `bin/install.mjs`. A support module inside the hooks tree carries no such family suffix, so it stays out of the check. A hook deleted from disk without an entry in that roster resolves nowhere, so add the roster entry when you retire a hook.
|
|
13
13
|
|
|
@@ -27,7 +27,7 @@ Only after confirming the answer cannot be obtained through any available tool,
|
|
|
27
27
|
A path, port, branch name, or config value you recall from an earlier session counts as unanswered until a tool re-checks it this session. Memory records what was true when it was written; the file may have moved, the port may be down, the branch may have merged. Treat every recalled fact as a claim to re-ground, not an answer to reuse.
|
|
28
28
|
|
|
29
29
|
- When a tool can settle it, re-check in silence and act on the fresh result — no question to the user.
|
|
30
|
-
- When no tool can settle it and the user has a stake in the answer, ask through `AskUserQuestion
|
|
30
|
+
- When no tool can settle it and the user has a stake in the answer, ask through `AskUserQuestion`.
|
|
31
31
|
|
|
32
32
|
## Questions That Belong to the User
|
|
33
33
|
|
|
@@ -12,6 +12,6 @@ Never call `shutil.rmtree` with `ignore_errors=True` — Windows `ReadOnly` file
|
|
|
12
12
|
|
|
13
13
|
In Node, call `mkdirSync(targetPath, { recursive: true })` on possibly-existing paths — `ReadOnly` directories break the non-recursive form. When the call must be non-recursive, strip the attribute first (`(Get-Item $path -Force).Attributes = "Directory"` / `os.chmod(path, stat.S_IWRITE)`).
|
|
14
14
|
|
|
15
|
-
The staged policy lint carries this check as its `rmtree-safety` rule. It reports the unsafe rmtree pattern and returns the full `force_rmtree` safe-pattern code.
|
|
15
|
+
The staged policy lint carries this check as its `rmtree-safety` rule. It reports the unsafe rmtree pattern and returns the full `force_rmtree` safe-pattern code. CI runs that lint against the merge base.
|
|
16
16
|
|
|
17
17
|
Define the safe handler trio (`_strip_read_only_and_retry`, `_force_remove_tree` / `force_rmtree`, and the `inspect.signature` onexc/onerror guard) once in a shared Windows-filesystem utility module, and import it from every call site. A second local copy drifts from the first, so a fix lands in one and the other keeps the bug (CODE_RULES.md, CORE PRINCIPLES, "Reuse before create"). The same `rmtree-safety` lint rule reports a local re-definition of any trio member outside the shared home and points the writer at the import. This complements the same-directory `check_duplicate_function_body_across_files` check, which a copy between two distant packages slips past.
|
|
@@ -9,4 +9,4 @@ In a `.workflow.js` agent-prompt template, every per-call or per-iteration value
|
|
|
9
9
|
|
|
10
10
|
When a loop builds a per-iteration path or output key, write the index as a slot — `cand_<i>` — or spell out `replace <i> with the iteration index 0, 1, 2` in the step text. Every per-call value in a `.workflow.js` template carries angle brackets so an agent fills in a fresh value per call.
|
|
11
11
|
|
|
12
|
-
The staged policy lint carries this check as its `workflow-substitution` rule. It reports a `.workflow.js` file whose looped content holds a bare `<word>_<i|j|k>` token as a per-iteration path segment.
|
|
12
|
+
The staged policy lint carries this check as its `workflow-substitution` rule. It reports a `.workflow.js` file whose looped content holds a bare `<word>_<i|j|k>` token as a per-iteration path segment. CI runs that lint against the merge base. No write-time hook reports this, so a bare token stays on disk until the lint runs.
|