claude-dev-env 7.7.1 → 8.1.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 (118) 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/audit-categories.json +0 -4
  15. package/audit-rubrics/category_rubrics/category-e-dead-code.md +1 -1
  16. package/audit-rubrics/category_rubrics/category-j-code-rules-compliance.md +1 -2
  17. package/audit-rubrics/prompts/category-e-dead-code.md +5 -5
  18. package/audit-rubrics/prompts/category-j-code-rules-compliance.md +1 -24
  19. package/bin/install-constants.mjs +0 -5
  20. package/bin/install-pstack-plugin.mjs +192 -0
  21. package/bin/install-pstack-plugin.test.mjs +181 -0
  22. package/bin/install.agents-home.test.mjs +0 -4
  23. package/bin/install.codex-rules.test.mjs +6 -0
  24. package/bin/install.cursor-rules.test.mjs +3 -253
  25. package/bin/install.mjs +55 -160
  26. package/bin/install.test.mjs +110 -149
  27. package/bin/prune-retired-pstack-store.mjs +83 -0
  28. package/bin/prune-retired-pstack-store.test.mjs +105 -0
  29. package/bin/prune-session-continuity.mjs +57 -0
  30. package/bin/prune-session-continuity.test.mjs +70 -0
  31. package/bin/resolve-install-root.mjs +2 -0
  32. package/bin/resolve-install-root.test.mjs +69 -0
  33. package/docs/CODE_RULES.md +1 -1
  34. package/docs/agent-spawn-protocol.md +1 -1
  35. package/docs/codex-compatibility.md +4 -16
  36. package/docs/nas-ssh-invocation.md +1 -1
  37. package/docs/references/prose-style-enforcement.md +2 -2
  38. package/hooks/HOOK_SPECS_PROMPT_WORKFLOW.md +1 -3
  39. package/hooks/blocking/code_rules_constants_config.py +1 -108
  40. package/hooks/blocking/code_rules_enforcer.py +9 -5
  41. package/hooks/blocking/code_rules_test_layout.py +6 -7
  42. package/hooks/blocking/test_code_rules_constants_config.py +13 -0
  43. package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +0 -1
  44. package/hooks/blocking/test_code_rules_enforcer_dispatch_wiring.py +15 -0
  45. package/hooks/blocking/test_code_rules_enforcer_split_constants_config.py +0 -60
  46. package/hooks/blocking/test_context_control_policy_files.py +0 -11
  47. package/hooks/hooks_constants/blocking_check_limits.py +1 -1
  48. package/hooks/hooks_constants/code_rules_enforcer_constants.py +0 -1
  49. package/hooks/hooks_constants/setup_project_paths_constants.py +1 -2
  50. package/hooks/session/test_untracked_repo_detector.py +1 -1
  51. package/hooks/validators/code_quality_checks.py +15 -2
  52. package/hooks/validators/test_code_quality_checks.py +22 -0
  53. package/package.json +1 -2
  54. package/rules/anti-corollary-tests.md +3 -1
  55. package/rules/archiving-agent-config.md +70 -0
  56. package/rules/asd-ste100-language.md +1 -1
  57. package/rules/ci-owns-the-gate.md +73 -0
  58. package/rules/cleanup-temp-files.md +1 -1
  59. package/rules/code-standards.md +3 -4
  60. package/rules/docstring-prose-matches-implementation.md +3 -3
  61. package/rules/explore-thoroughly.md +2 -2
  62. package/rules/failure-blast-radius.md +10 -9
  63. package/rules/git-workflow.md +27 -12
  64. package/rules/long-horizon-autonomy.md +3 -1
  65. package/rules/nas-ssh-invocation.md +1 -1
  66. package/rules/orphan-css-class.md +1 -1
  67. package/rules/paired-test-coverage.md +1 -1
  68. package/rules/research-mode.md +3 -1
  69. package/rules/retired-hook-prose.md +10 -2
  70. package/rules/testing.md +3 -3
  71. package/rules/verify-before-asking.md +4 -1
  72. package/rules/verify-runtime-state.md +2 -2
  73. package/rules/windows-filesystem-safe.md +1 -1
  74. package/rules/workflow-substitution-slots.md +1 -1
  75. package/scripts/Sync-RepoMain.ps1 +71 -6
  76. package/scripts/policy_lint/adapter_detectors.py +116 -10
  77. package/scripts/policy_lint/config/approved_test_pairs.py +12 -0
  78. package/scripts/policy_lint/config/constants.py +3 -0
  79. package/scripts/tests/Sync-RepoMain.Tests.ps1 +108 -0
  80. package/scripts/tests/test_codex_compat_materializer.py +3 -3
  81. package/scripts/tests/test_policy_lint_rules_validator_baseline.py +126 -0
  82. package/.agents/skills/session-continuity/AGENTS.md +0 -1
  83. package/.agents/skills/session-continuity/CLAUDE.md +0 -1
  84. package/.agents/skills/session-continuity/README.md +0 -223
  85. package/.agents/skills/session-continuity/SKILL.md +0 -125
  86. package/.agents/skills/session-continuity/continuity.mjs +0 -445
  87. package/.agents/skills/session-continuity/continuity.test.mjs +0 -381
  88. package/bin/install-session-continuity.mjs +0 -131
  89. package/bin/install-session-continuity.test.mjs +0 -106
  90. package/bin/install.pstack-plugin.test.mjs +0 -115
  91. package/bin/pstack-codex-discovery.test.mjs +0 -79
  92. package/bin/pstack-model-preferences.claude.json +0 -23
  93. package/bin/pstack-model-preferences.codex.json +0 -24
  94. package/bin/pstack.mjs +0 -392
  95. package/bin/pstack.test.mjs +0 -347
  96. package/bin/refresh-pstack-plugin-skills.test.mjs +0 -211
  97. package/bin/select-pstack-models.test.mjs +0 -776
  98. package/hooks/blocking/test_code_rules_enforcer_file_global_constants.py +0 -231
  99. package/rules/agent-spawn-protocol.md +0 -9
  100. package/rules/ask-user-question-required.md +0 -31
  101. package/rules/claims-as-quotes.md +0 -55
  102. package/rules/confirm-implementation-forks.md +0 -11
  103. package/rules/file-global-constants.md +0 -81
  104. package/rules/hedging-claims.md +0 -7
  105. package/rules/measurement-denominators.md +0 -39
  106. package/rules/prompt-workflow-context-controls.md +0 -19
  107. package/rules/pstack-host-mapping.md +0 -48
  108. package/rules/pstack-models.md +0 -142
  109. package/rules/vault-context.md +0 -5
  110. package/scripts/pstack-adapters/common.md +0 -17
  111. package/scripts/pstack-adapters/create-skill.md +0 -14
  112. package/scripts/pstack-adapters/host-claude.md +0 -7
  113. package/scripts/pstack-adapters/host-codex.md +0 -11
  114. package/scripts/pstack-adapters/host-cursor.md +0 -7
  115. package/scripts/pstack.lock.json +0 -11
  116. package/scripts/refresh_pstack_plugin_skills.mjs +0 -200
  117. package/scripts/select_pstack_models.mjs +0 -469
  118. /package/.agents/{skills → skills-archived}/source-command-logifix/SKILL.md +0 -0
@@ -60,6 +60,7 @@ export const MANIFEST_FILE_NAME = '.claude-dev-env-manifest.json';
60
60
  * hooksLookupDirectory: string,
61
61
  * scriptsLookupDirectory: string,
62
62
  * codexHooksInstallDirectory: string,
63
+ * codexHomeDirectory: string,
63
64
  * }} InstallRootResolution
64
65
  */
65
66
 
@@ -179,6 +180,7 @@ export function resolveInstallRoot(options = {}) {
179
180
  hooksLookupDirectory,
180
181
  scriptsLookupDirectory,
181
182
  codexHooksInstallDirectory,
183
+ codexHomeDirectory,
182
184
  };
183
185
  }
184
186
 
@@ -0,0 +1,69 @@
1
+ import { test } from 'node:test';
2
+ import { strict as assert } from 'node:assert';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import {
6
+ resolveInstallRoot,
7
+ CLAUDE_CONFIG_DIR_ENVIRONMENT_VARIABLE,
8
+ DEFAULT_CLAUDE_DIRECTORY_NAME,
9
+ } from './resolve-install-root.mjs';
10
+ import {
11
+ CODEX_HOME_ENVIRONMENT_VARIABLE,
12
+ CODEX_HOOKS_DIRECTORY_NAME,
13
+ CODEX_RULES_DIRECTORY_NAME,
14
+ DEFAULT_CODEX_DIRECTORY_NAME,
15
+ MANAGED_SKILLS_DIRECTORY_NAME,
16
+ PACKAGE_AGENTS_HOME_DIRECTORY_NAME,
17
+ } from './install-constants.mjs';
18
+
19
+ const HOME_DIRECTORY = join(tmpdir(), 'cdev-resolve-roots-home');
20
+
21
+ function resolveFrom(environment, explicitTarget = null) {
22
+ return resolveInstallRoot({ homeDirectory: HOME_DIRECTORY, environment, explicitTarget });
23
+ }
24
+
25
+ test('the managed root falls back to the default Claude directory under the home', () => {
26
+ const resolution = resolveFrom({});
27
+ assert.equal(resolution.managedRoot, join(HOME_DIRECTORY, DEFAULT_CLAUDE_DIRECTORY_NAME));
28
+ assert.equal(resolution.source, 'default-home');
29
+ });
30
+
31
+ test('CLAUDE_CONFIG_DIR names the managed root and an explicit target outranks it', () => {
32
+ const configuredRoot = join(tmpdir(), 'cdev-configured');
33
+ const explicitRoot = join(tmpdir(), 'cdev-explicit');
34
+ const configured = resolveFrom({ [CLAUDE_CONFIG_DIR_ENVIRONMENT_VARIABLE]: configuredRoot });
35
+ assert.equal(configured.managedRoot, configuredRoot);
36
+ assert.equal(configured.source, 'claude-config-dir');
37
+
38
+ const explicit = resolveFrom(
39
+ { [CLAUDE_CONFIG_DIR_ENVIRONMENT_VARIABLE]: configuredRoot },
40
+ explicitRoot,
41
+ );
42
+ assert.equal(explicit.managedRoot, explicitRoot);
43
+ assert.equal(explicit.source, 'explicit-target');
44
+ });
45
+
46
+ test('the Codex home is the directory holding the rules and hooks destinations', () => {
47
+ const resolution = resolveFrom({});
48
+ const codexHome = join(HOME_DIRECTORY, DEFAULT_CODEX_DIRECTORY_NAME);
49
+ assert.equal(resolution.codexHomeDirectory, codexHome);
50
+ assert.equal(resolution.codexRulesInstallDirectory, join(codexHome, CODEX_RULES_DIRECTORY_NAME));
51
+ assert.equal(resolution.codexHooksInstallDirectory, join(codexHome, CODEX_HOOKS_DIRECTORY_NAME));
52
+ });
53
+
54
+ test('CODEX_HOME relocates the Codex home the pstack plugin step installs into', () => {
55
+ const relocatedHome = join(tmpdir(), 'cdev-relocated-codex');
56
+ const resolution = resolveFrom({ [CODEX_HOME_ENVIRONMENT_VARIABLE]: relocatedHome });
57
+ assert.equal(resolution.codexHomeDirectory, relocatedHome);
58
+ assert.equal(resolution.codexRulesInstallDirectory, join(relocatedHome, CODEX_RULES_DIRECTORY_NAME));
59
+ });
60
+
61
+ test('a managed root named .claude pairs with the sibling agents home', () => {
62
+ const resolution = resolveFrom({});
63
+ assert.equal(resolution.agentsHome, join(HOME_DIRECTORY, PACKAGE_AGENTS_HOME_DIRECTORY_NAME));
64
+ assert.equal(resolution.skillsInstallDirectory, join(HOME_DIRECTORY, PACKAGE_AGENTS_HOME_DIRECTORY_NAME, MANAGED_SKILLS_DIRECTORY_NAME));
65
+ assert.equal(
66
+ resolution.skillsLookupDirectory,
67
+ join(HOME_DIRECTORY, DEFAULT_CLAUDE_DIRECTORY_NAME, MANAGED_SKILLS_DIRECTORY_NAME),
68
+ );
69
+ });
@@ -31,7 +31,7 @@ Production and tests follow one rule. Changed directive, TODO, FIXME, HACK, XXX,
31
31
 
32
32
  no new comments · imports at top · logging format args (`log_*("...", arg)`) · no `%s`/`%d` printf tokens in a `str.format`-logger message (`log_*` imported from `automation_logging`; `str.format` drops the args — use `{}`) · no magic values in production bodies (0, 1, -1 exempt) · UPPER_SNAKE constants only in `config/` (exempt: `config/*`; `/migrations/`; Workflow registries: path contains any of these substrings — `/workflow/`, `_tab.py`, `/states.py`, or `/modules.py`, each matching independently as a substring, so `pkg/states.py` qualifies while a top-level `states.py` follows the standard `config/` rule; test files — path or filename matches `test_`, `_test.`, `.spec.`, `conftest`, or `/tests/`) · no hardcoded user home paths · guarded `sys.path.insert` · banned identifiers (`ctx`, `cfg`, `msg`, `btn`, `idx`, `cnt`, `tmp`, `elem`, `val`) · banned function prefixes (`handle_`, `process_`, `manage_`, `do_`) · no type escape hatches (`Any` import, `cast()`, inline `Any`, a parameter typed bare `object` whose body reads `param.attribute`) outside boundary files · no bare/broad `except` · no `Any` in signatures or class attributes · no stub bodies (`pass`/`...`/`raise NotImplementedError`) outside abstract/Protocol · TypedDict `_encode_*`/`_decode_*` companions in the same module · no test-mode branching in production (use dependency injection) · no thin wrapper modules · Google-style docstrings on public functions with `Args:` matching the signature · boolean names prefixed `is_`/`has_`/`should_`/`can_`/`was_`/`did_` (assignments AND bool-typed parameters) · must-check returns (`find_and_click`, `write_outcome`) assigned and checked · known pytest fixture parameters in test files annotated with their single documented type (`tmp_path: Path`, `monkeypatch: pytest.MonkeyPatch`, `capsys`, `caplog`, `request`, …) · known pytest fixture parameters a test function declares but never references (drop the unused parameter — pytest still pays its setup cost) · JavaScript/TypeScript boolean declarations (`const`/`let`/`var` bound to a boolean literal or negation) and `@param {boolean}` JSDoc names prefixed `is`/`has`/`should`/`can`/`was`/`did` (camelCase forms) · banned identifiers as `.mjs`/`.js` declaration names (`result`, `data`, `ctx`, `msg`, …), scoped to changed lines · in test files, banned identifiers fire on changed lines, and pytest-collectable `test_*` functions need a return annotation · unused module-level imports and unsorted import blocks are ruff's job (F401, isort I001), not this hook's · a `hooks/blocking/` command classifier anchors its multi-word command regex to the command start (`^`/`\A`) or tokenizes the first word (`shlex.split`), never matching a command as a bare substring
33
33
 
34
- Test files follow the comment policy above; other test-specific exemptions are listed here. The one annotation the test-file exemption does NOT cover is a known pytest builtin fixture parameter: `tmp_path`, `monkeypatch`, `capsys`, `capfd`, `caplog`, `request`, and `tmp_path_factory` each have a single documented injected type, so the gate requires that annotation (`tmp_path: Path`) even inside a test file. The same set of fixtures is also subject to a use check: a pytest-collected test function that declares one of these parameters and never references it in its body fails the gate, because pytest materializes the fixture's setup (the temp directory, the monkeypatch context, the output capture) on every run whether or not the body reads the value — drop the unused parameter. A parameter counts as referenced when its name is read, augmented-assigned, or deleted anywhere in the body, including inside a nested function or comprehension. Only pytest-collectable functions are inspected — those at module top level or defined directly in a class body; a function nested inside another function's body is a local helper pytest never collects, so its fixture-named parameter is exempt. A `@pytest.fixture`-decorated function is exempt from the use check, since injecting one fixture into another purely to order its setup is intentional. Ordinary test parameters stay exempt from both checks. See also the file-global constants use-count rule: [`rules/file-global-constants.md`](../rules/file-global-constants.md).
34
+ Test files follow the comment policy above; other test-specific exemptions are listed here. The one annotation the test-file exemption does NOT cover is a known pytest builtin fixture parameter: `tmp_path`, `monkeypatch`, `capsys`, `capfd`, `caplog`, `request`, and `tmp_path_factory` each have a single documented injected type, so the gate requires that annotation (`tmp_path: Path`) even inside a test file. The same set of fixtures is also subject to a use check: a pytest-collected test function that declares one of these parameters and never references it in its body fails the gate, because pytest materializes the fixture's setup (the temp directory, the monkeypatch context, the output capture) on every run whether or not the body reads the value — drop the unused parameter. A parameter counts as referenced when its name is read, augmented-assigned, or deleted anywhere in the body, including inside a nested function or comprehension. Only pytest-collectable functions are inspected — those at module top level or defined directly in a class body; a function nested inside another function's body is a local helper pytest never collects, so its fixture-named parameter is exempt. A `@pytest.fixture`-decorated function is exempt from the use check, since injecting one fixture into another purely to order its setup is intentional. Ordinary test parameters stay exempt from both checks.
35
35
 
36
36
  ---
37
37
 
@@ -1,6 +1,6 @@
1
1
  # Agent Spawn Protocol
2
2
 
3
- Full protocol behind the always-on `rules/agent-spawn-protocol.md` kernel. It applies before any Agent or Task tool invocation — Explore, implementation, research, or team subagents.
3
+ Full protocol for briefing a subagent. It applies before any Agent or Task tool invocation — Explore, implementation, research, or team subagents.
4
4
 
5
5
  ## Step 1: Context sufficiency check
6
6
 
@@ -65,22 +65,10 @@ hand when the host should receive that context.
65
65
 
66
66
  ## Luna fast-mode guard
67
67
 
68
- `hooks/blocking/luna_fast_mode_gate.py` serves Claude and Codex. Codex reads its own `hooks.json`, so add this `PreToolUse` group to that file. Replace `<CODEX_HOOKS_ROOT>` with the directory that holds the shipped hook:
69
-
70
- ```json
71
- {
72
- "matcher": "Agent|Task|multi_agent_v1__spawn_agent",
73
- "hooks": [
74
- {
75
- "type": "command",
76
- "command": "python <CODEX_HOOKS_ROOT>/blocking/luna_fast_mode_gate.py",
77
- "timeout": 10
78
- }
79
- ]
80
- }
81
- ```
82
-
83
- The guard requires exact `fast` for Luna spawns through `Agent` and `Task`. Native Codex `multi_agent_v1__spawn_agent` accepts exact `fast` or `priority`. Other models and tools pass through.
68
+ Commit `0f21faf8` retired `luna_fast_mode_gate.py` with the other blocking
69
+ policy hooks. The package ships no replacement, so a Codex `hooks.json` needs
70
+ no entry for it, and the spawn tier a Luna spawn asks for is the caller's to
71
+ hold.
84
72
 
85
73
  ## Roots and safety
86
74
 
@@ -23,7 +23,7 @@ from there when something needs them, and keep them out of anything committed or
23
23
 
24
24
  Write the script file with the Write tool. Keeping the commands in a file has a second benefit: a
25
25
  destructive word such as `rm -rf` inside the file never appears in a Bash tool command string, so the
26
- `destructive_command_blocker` hook stays out of the way.
26
+ harness permission prompt never fires on it and an unattended run keeps going.
27
27
 
28
28
  The script runs through `bash -s` in a single shell. Variables, `cd`, and `source` all carry from one
29
29
  line to the next. Standard output, standard error and the exit code all come back.
@@ -10,8 +10,8 @@ The flag is `PROSE_STYLE_ENFORCEMENT_ENABLED` in `hooks/blocking/config/prose_st
10
10
 
11
11
  ## Always on
12
12
 
13
- Structural AskUserQuestion lean-block validation in `ask_user_question_shape_blocker` (chat detail, length caps on question blocks) stays active regardless of the flag. Historical/comparative language detection in `state_description_blocker.py` runs unconditionally and does not read this flag. Apply `rules/asd-ste100-language.md` for user-facing word choice and sentence style. Code-rules and security blockers are out of scope for this switch.
13
+ Historical/comparative language detection in `state_description_blocker.py` runs unconditionally and does not read this flag. Apply `rules/asd-ste100-language.md` for user-facing word choice and sentence style. Code-rules and security blockers are out of scope for this switch.
14
14
 
15
15
  ## Hedging claims
16
16
 
17
- Hedge-word discipline is prose guidance, not a hook: `rules/hedging-claims.md`. State a claim with its evidence, or label it unverified in the same sentence as the hedge word.
17
+ Hedge-word discipline is prose guidance, not a hook. State a claim with its evidence, or label it unverified in the same sentence as the hedge word.
@@ -49,6 +49,4 @@ Generated prompt-workflow outputs must declare, in their audit frontmatter:
49
49
  - `base_minimal_instruction_layer: true`
50
50
  - `on_demand_skill_loading: true`
51
51
 
52
- The Stop guard blocks responses that omit either signal. These signals are
53
- the machine-checkable counterpart to the policy text in
54
- `rules/prompt-workflow-context-controls.md`.
52
+ The Stop guard blocks responses that omit either signal.
@@ -1,4 +1,4 @@
1
- """Constants-outside-config checks and the file-global constant use-count check.
1
+ """Constants-outside-config checks.
2
2
 
3
3
  Also carries check_config_duplicate_path_anchor, which flags a config module
4
4
  that rebuilds a directory a sibling module in the same package already
@@ -22,7 +22,6 @@ from code_rules_path_utils import ( # noqa: E402
22
22
  is_config_file,
23
23
  )
24
24
  from code_rules_shared import ( # noqa: E402
25
- _build_parent_map,
26
25
  docstring_line_numbers,
27
26
  get_file_extension,
28
27
  is_migration_file,
@@ -35,7 +34,6 @@ from hooks_constants.blocking_check_limits import ( # noqa: E402
35
34
  )
36
35
  from hooks_constants.code_rules_enforcer_constants import ( # noqa: E402
37
36
  ALL_PYTHON_EXTENSIONS,
38
- FILE_GLOBAL_UPPER_SNAKE_PATTERN,
39
37
  )
40
38
 
41
39
 
@@ -164,111 +162,6 @@ def check_constants_outside_config_advisory(content: str, file_path: str) -> Non
164
162
  sys.stderr.write(f"[CODE_RULES advisory] {file_path}: {each_issue}\n")
165
163
 
166
164
 
167
- def _is_upper_snake_constant_name(name: str) -> bool:
168
- """Return True for UPPER_SNAKE identifiers including those with a leading underscore."""
169
- return bool(FILE_GLOBAL_UPPER_SNAKE_PATTERN.match(name))
170
-
171
-
172
- def _collect_module_level_upper_snake_constants(
173
- module_tree: ast.Module,
174
- ) -> dict[str, int]:
175
- """Return mapping of module-level UPPER_SNAKE constant name to its line number."""
176
- constants_by_name: dict[str, int] = {}
177
- for each_node in module_tree.body:
178
- if isinstance(each_node, ast.Assign):
179
- for each_target in each_node.targets:
180
- if isinstance(each_target, ast.Name) and _is_upper_snake_constant_name(each_target.id):
181
- constants_by_name.setdefault(each_target.id, each_node.lineno)
182
- elif isinstance(each_node, ast.AnnAssign):
183
- if isinstance(each_node.target, ast.Name) and _is_upper_snake_constant_name(each_node.target.id):
184
- constants_by_name.setdefault(each_node.target.id, each_node.lineno)
185
- return constants_by_name
186
-
187
-
188
- def _resolve_enclosing_function_qname(
189
- load_node: ast.Name,
190
- parent_by_child_id: dict[int, ast.AST],
191
- ) -> Optional[str]:
192
- """Return 'ClassName.function_name' or 'function_name' for the enclosing function.
193
-
194
- Returns None when the reference is at module scope (no enclosing function).
195
- Decorator expressions on a function/method count as belonging to that function.
196
- """
197
- enclosing_function_name: Optional[str] = None
198
- enclosing_class_name: Optional[str] = None
199
- current_ancestor = parent_by_child_id.get(id(load_node))
200
- while current_ancestor is not None:
201
- if isinstance(current_ancestor, (ast.FunctionDef, ast.AsyncFunctionDef)) and enclosing_function_name is None:
202
- enclosing_function_name = current_ancestor.name
203
- elif isinstance(current_ancestor, ast.ClassDef):
204
- enclosing_class_name = current_ancestor.name
205
- break
206
- current_ancestor = parent_by_child_id.get(id(current_ancestor))
207
- if enclosing_function_name is None:
208
- if enclosing_class_name is not None:
209
- return f"<class:{enclosing_class_name}>"
210
- return None
211
- if enclosing_class_name is not None:
212
- return f"{enclosing_class_name}.{enclosing_function_name}"
213
- return enclosing_function_name
214
-
215
-
216
- def check_file_global_constants_use_count(content: str, file_path: str) -> list[str]:
217
- """Flag module-level UPPER_SNAKE constants referenced by only one function/method.
218
-
219
- Enforces the file-global-constants use-count rule: a constant used by just
220
- one caller belongs in that caller's scope. Test files, config files,
221
- workflow-registry files, and non-Python files are exempt. Constants with
222
- zero references are out of scope. The enforcer entry module
223
- (``hooks/blocking/code_rules_enforcer.py``) is exempt to avoid
224
- self-blocking.
225
- """
226
- if is_test_file(file_path):
227
- return []
228
- if is_config_file(file_path):
229
- return []
230
- if is_workflow_registry_file(file_path):
231
- return []
232
- if get_file_extension(file_path) not in ALL_PYTHON_EXTENSIONS:
233
- return []
234
- if file_path.replace("\\", "/").endswith("hooks/blocking/code_rules_enforcer.py"):
235
- return []
236
-
237
- try:
238
- module_tree = ast.parse(content)
239
- except SyntaxError:
240
- return []
241
-
242
- constants_by_name = _collect_module_level_upper_snake_constants(module_tree)
243
- if not constants_by_name:
244
- return []
245
-
246
- parent_by_child_id = _build_parent_map(module_tree)
247
- callers_by_constant: dict[str, set[str]] = {name: set() for name in constants_by_name}
248
- for each_node in ast.walk(module_tree):
249
- if not isinstance(each_node, ast.Name):
250
- continue
251
- if not isinstance(each_node.ctx, ast.Load):
252
- continue
253
- if each_node.id not in callers_by_constant:
254
- continue
255
- enclosing_qname = _resolve_enclosing_function_qname(each_node, parent_by_child_id)
256
- if enclosing_qname is None:
257
- callers_by_constant[each_node.id].add("<module-scope>")
258
- else:
259
- callers_by_constant[each_node.id].add(enclosing_qname)
260
-
261
- issues: list[str] = []
262
- for each_constant_name, each_line_number in sorted(constants_by_name.items(), key=lambda pair: pair[1]):
263
- caller_count = len(callers_by_constant[each_constant_name])
264
- if caller_count == 1:
265
- issues.append(
266
- f"Line {each_line_number}: File-global constant {each_constant_name} used by only 1 function/method - move to method scope or add a second caller"
267
- )
268
-
269
- return issues
270
-
271
-
272
165
  def _references_dunder_file(node: ast.AST) -> bool:
273
166
  """Return True when the expression tree reads ``__file__``."""
274
167
  return any(
@@ -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: