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.
Files changed (103) hide show
  1. package/.agents/agents/clean-coder.md +5 -6
  2. package/.agents/agents/issue-tracker.md +1 -1
  3. package/.agents/agents/pr-description-writer.md +1 -1
  4. package/.agents/agents/skill-writer-agent.md +1 -12
  5. package/.agents/agents/test_agent_frontmatter.py +2 -19
  6. package/.agents/skills/issue-tracker/SKILL.md +1 -1
  7. package/.agents/skills/orchestrator/SKILL.md +4 -5
  8. package/.agents/skills/pull-request/SKILL.md +8 -7
  9. package/.agents/skills/pull-request/reference/publication-tasks.md +1 -1
  10. package/.agents/skills/team-advisor/SKILL.md +2 -3
  11. package/_shared/advisor/advisor-protocol.md +0 -1
  12. package/_shared/advisor/reference/lifecycle.md +0 -1
  13. package/_shared/advisor/reference/warm-up.md +1 -2
  14. package/audit-rubrics/prompts/category-e-dead-code.md +3 -3
  15. package/bin/install-constants.mjs +0 -5
  16. package/bin/install-pstack-plugin.mjs +192 -0
  17. package/bin/install-pstack-plugin.test.mjs +181 -0
  18. package/bin/install.agents-home.test.mjs +0 -4
  19. package/bin/install.codex-rules.test.mjs +6 -0
  20. package/bin/install.cursor-rules.test.mjs +3 -253
  21. package/bin/install.mjs +55 -160
  22. package/bin/install.test.mjs +110 -149
  23. package/bin/prune-retired-pstack-store.mjs +83 -0
  24. package/bin/prune-retired-pstack-store.test.mjs +105 -0
  25. package/bin/prune-session-continuity.mjs +57 -0
  26. package/bin/prune-session-continuity.test.mjs +70 -0
  27. package/bin/resolve-install-root.mjs +2 -0
  28. package/bin/resolve-install-root.test.mjs +69 -0
  29. package/docs/CODE_RULES.md +1 -1
  30. package/docs/agent-spawn-protocol.md +1 -1
  31. package/docs/codex-compatibility.md +4 -16
  32. package/docs/nas-ssh-invocation.md +1 -1
  33. package/docs/references/prose-style-enforcement.md +2 -2
  34. package/hooks/HOOK_SPECS_PROMPT_WORKFLOW.md +1 -3
  35. package/hooks/blocking/code_rules_constants_config.py +1 -108
  36. package/hooks/blocking/code_rules_enforcer.py +9 -5
  37. package/hooks/blocking/code_rules_test_layout.py +6 -7
  38. package/hooks/blocking/test_code_rules_constants_config.py +13 -0
  39. package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +0 -1
  40. package/hooks/blocking/test_code_rules_enforcer_dispatch_wiring.py +15 -0
  41. package/hooks/blocking/test_code_rules_enforcer_split_constants_config.py +0 -60
  42. package/hooks/blocking/test_context_control_policy_files.py +0 -11
  43. package/hooks/hooks_constants/blocking_check_limits.py +1 -1
  44. package/hooks/hooks_constants/code_rules_enforcer_constants.py +0 -1
  45. package/hooks/hooks_constants/setup_project_paths_constants.py +1 -2
  46. package/hooks/session/test_untracked_repo_detector.py +1 -1
  47. package/hooks/validators/code_quality_checks.py +15 -2
  48. package/hooks/validators/test_code_quality_checks.py +22 -0
  49. package/package.json +1 -2
  50. package/rules/ci-owns-the-gate.md +58 -0
  51. package/rules/code-standards.md +1 -2
  52. package/rules/docstring-prose-matches-implementation.md +2 -2
  53. package/rules/failure-blast-radius.md +1 -1
  54. package/rules/git-workflow.md +23 -11
  55. package/rules/orphan-css-class.md +1 -1
  56. package/rules/paired-test-coverage.md +1 -1
  57. package/rules/retired-hook-prose.md +1 -1
  58. package/rules/verify-before-asking.md +1 -1
  59. package/rules/windows-filesystem-safe.md +1 -1
  60. package/rules/workflow-substitution-slots.md +1 -1
  61. package/scripts/Sync-RepoMain.ps1 +71 -6
  62. package/scripts/policy_lint/adapter_detectors.py +116 -10
  63. package/scripts/policy_lint/config/approved_test_pairs.py +12 -0
  64. package/scripts/policy_lint/config/constants.py +3 -0
  65. package/scripts/tests/Sync-RepoMain.Tests.ps1 +108 -0
  66. package/scripts/tests/test_policy_lint_rules_validator_baseline.py +126 -0
  67. package/.agents/skills/session-continuity/AGENTS.md +0 -1
  68. package/.agents/skills/session-continuity/CLAUDE.md +0 -1
  69. package/.agents/skills/session-continuity/README.md +0 -223
  70. package/.agents/skills/session-continuity/SKILL.md +0 -125
  71. package/.agents/skills/session-continuity/continuity.mjs +0 -445
  72. package/.agents/skills/session-continuity/continuity.test.mjs +0 -381
  73. package/bin/install-session-continuity.mjs +0 -131
  74. package/bin/install-session-continuity.test.mjs +0 -106
  75. package/bin/install.pstack-plugin.test.mjs +0 -115
  76. package/bin/pstack-codex-discovery.test.mjs +0 -79
  77. package/bin/pstack-model-preferences.claude.json +0 -23
  78. package/bin/pstack-model-preferences.codex.json +0 -24
  79. package/bin/pstack.mjs +0 -392
  80. package/bin/pstack.test.mjs +0 -347
  81. package/bin/refresh-pstack-plugin-skills.test.mjs +0 -211
  82. package/bin/select-pstack-models.test.mjs +0 -776
  83. package/hooks/blocking/test_code_rules_enforcer_file_global_constants.py +0 -231
  84. package/rules/agent-spawn-protocol.md +0 -9
  85. package/rules/ask-user-question-required.md +0 -31
  86. package/rules/claims-as-quotes.md +0 -55
  87. package/rules/confirm-implementation-forks.md +0 -11
  88. package/rules/file-global-constants.md +0 -81
  89. package/rules/hedging-claims.md +0 -7
  90. package/rules/measurement-denominators.md +0 -39
  91. package/rules/prompt-workflow-context-controls.md +0 -19
  92. package/rules/pstack-host-mapping.md +0 -48
  93. package/rules/pstack-models.md +0 -142
  94. package/rules/vault-context.md +0 -5
  95. package/scripts/pstack-adapters/common.md +0 -17
  96. package/scripts/pstack-adapters/create-skill.md +0 -14
  97. package/scripts/pstack-adapters/host-claude.md +0 -7
  98. package/scripts/pstack-adapters/host-codex.md +0 -11
  99. package/scripts/pstack-adapters/host-cursor.md +0 -7
  100. package/scripts/pstack.lock.json +0 -11
  101. package/scripts/refresh_pstack_plugin_skills.mjs +0 -200
  102. package/scripts/select_pstack_models.mjs +0 -469
  103. /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 = "; ".join(all_blocking_issues[:DENY_REASON_ISSUE_PREVIEW_COUNT])
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 = "; ".join(forecast_issues[:DENY_REASON_ISSUE_PREVIEW_COUNT])
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 = "; ".join(all_blocking_issues[:DENY_REASON_ISSUE_PREVIEW_COUNT])
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 (see
14
- rules/file-global-constants.md) and this write-time check leaves them alone.
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 (see rules/file-global-constants.md).
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 (see
81
- rules/file-global-constants.md) and this single-file scan leaves it alone.
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 (see rules/file-global-constants.md) and this
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
- under the file-global-constants use-count rule (CODE_RULES §file-global-constants).
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 per the
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 moved to local per file-global-constants rule.
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": "7.7.1",
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 |
@@ -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. Use `python packages/claude-dev-env/scripts/cde_lint.py --staged` before you commit, and CI runs the same lint against the merge base. Each mechanical rule carries a synchronization test.
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 no write-time hook runs them. Run `python packages/claude-dev-env/scripts/cde_lint.py --staged` before you commit. Free-form rest is judgment.
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 run `python packages/claude-dev-env/scripts/cde_lint.py --staged` before you commit.
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 run `python packages/claude-dev-env/scripts/cde_lint.py --staged` before you commit. CI runs the same lint against the merge base.
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
 
@@ -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
- Use the `pr-description-writer` agent before creating a pull request or
33
- rewriting its full description. Publish its title and body file through
34
- `.agents/skills/pull-request/scripts/pull_request.py`.
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
- ## Run the required checks locally before the first push
54
+ ## Confirm the required checks fired, and let CI run them
54
55
 
55
- Read the branch ruleset for the required check contexts before you push a branch,
56
- or any level of a stack: `gh api repos/<owner>/<repo>/rules/branches/<trunk>`.
57
- Then run that exact gate command locally against that level's own base, on every
58
- level. A required check that never fired is invisible debt at every level, and it
59
- surfaces only after the whole stack is pushed, when the repair costs a second pass
60
- over every branch.
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
- through the `pr-description-writer` agent before you ask anyone to read or merge it.
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. Run `python packages/claude-dev-env/scripts/cde_lint.py --staged` before you commit. CI runs the same lint against the merge base. It:
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 run `python packages/claude-dev-env/scripts/cde_lint.py --staged` before you commit. CI runs the same lint against the merge base. The two checks cover the two write orders.
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. Run `python packages/claude-dev-env/scripts/cde_lint.py --staged` before you commit. CI runs the same lint against the merge base.
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` (see [`confirm-implementation-forks`](confirm-implementation-forks.md)).
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. Run `python packages/claude-dev-env/scripts/cde_lint.py --staged` before you commit. CI runs the same lint against the merge base.
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. Run `python packages/claude-dev-env/scripts/cde_lint.py --staged` before you commit. CI runs the same lint against the merge base. No write-time hook reports this, so a bare token stays on disk until the lint runs.
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.