claude-dev-env 7.7.0 → 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/e-code-review/reference/fix.md +4 -2
- package/.agents/skills/e-code-review/reference/loop.md +4 -2
- 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/bin/subagent-model-policy.test.mjs +56 -19
- 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/docs/subagent-model-routing.md +21 -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/routing/subagent_model_routing.test.mjs +65 -28
- 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/subagent-model-policy.json +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 -727
- 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
|
@@ -32,8 +32,11 @@ example, change the `terra` and `medium` entry in `replacements` to:
|
|
|
32
32
|
|
|
33
33
|
The next routing call reads the file again. The resolver source stays unchanged.
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
Selector exclusions keep a pair out of panels. They do not change direct route
|
|
36
|
+
decisions. The shipped policy keeps Sol Medium out of panels and routes direct
|
|
37
|
+
requests for Sol Medium to Luna Max through `replacements`. Sol Medium is absent
|
|
38
|
+
from `approvedPairs`. The selector drops the requested pair before routing it,
|
|
39
|
+
so its Luna replacement does not enter the panel through that preference.
|
|
37
40
|
|
|
38
41
|
```json
|
|
39
42
|
"selectorExclusions": [
|
|
@@ -41,9 +44,25 @@ does not block the shared resolver or the spawn hook. For example:
|
|
|
41
44
|
]
|
|
42
45
|
```
|
|
43
46
|
|
|
47
|
+
The matching replacement is:
|
|
48
|
+
|
|
49
|
+
```json
|
|
50
|
+
{
|
|
51
|
+
"requested": {"model": "sol", "effort": "medium"},
|
|
52
|
+
"selected": {"model": "luna", "effort": "max"}
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
44
56
|
Unknown values, invalid policy data, and unavailable replacement models stop the
|
|
45
57
|
spawn with a short diagnostic. Parent inheritance stays unchanged.
|
|
46
58
|
|
|
59
|
+
The direct spawn hook and advisor bridge use the same shared resolver. A Sol
|
|
60
|
+
Medium remap changes only the model and effort fields in the hook's tool input.
|
|
61
|
+
The hook returns an allow decision with the updated input and emits no remap
|
|
62
|
+
notice. Luna High, Xhigh, and Max and Astra Low and Medium keep their routes.
|
|
63
|
+
Trusted advisors also keep Astra High. A worker request for Sol without an
|
|
64
|
+
effort stops because Sol has no approved pair to supply a default effort.
|
|
65
|
+
|
|
47
66
|
The hook blocks advisor role claims because the current Codex hook input has no
|
|
48
67
|
host-provided role binding. The Python advisor bridge passes trusted session
|
|
49
68
|
metadata to the shared resolver. The native advisor hook path stays pending
|
|
@@ -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.
|
|
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
|
|
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 =
|
|
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
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
|
-
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { execFileSync, spawnSync } from 'node:child_process';
|
|
3
3
|
import { test } from 'node:test';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
|
|
@@ -11,22 +11,22 @@ function buildHookPayload(toolInput, metadata = {}) {
|
|
|
11
11
|
return { tool_name: targetToolName, tool_input: toolInput, ...metadata };
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
test('remaps
|
|
14
|
+
test('remaps Sol Medium to Luna Max', () => {
|
|
15
15
|
const hookResponse = buildSubagentModelRoutingResponse(buildHookPayload({
|
|
16
|
-
model: '
|
|
16
|
+
model: 'Sol',
|
|
17
17
|
reasoning_effort: 'Medium',
|
|
18
18
|
}));
|
|
19
19
|
|
|
20
20
|
assert.equal(hookResponse.hookSpecificOutput.updatedInput.model, 'gpt-5.6-luna');
|
|
21
21
|
assert.equal(
|
|
22
22
|
hookResponse.hookSpecificOutput.updatedInput.reasoning_effort,
|
|
23
|
-
'
|
|
23
|
+
'max',
|
|
24
24
|
);
|
|
25
25
|
});
|
|
26
26
|
|
|
27
27
|
test('emits only the allow response shape after a remap', () => {
|
|
28
28
|
const hookResponse = buildSubagentModelRoutingResponse(buildHookPayload({
|
|
29
|
-
model: '
|
|
29
|
+
model: 'Sol',
|
|
30
30
|
reasoning_effort: 'medium',
|
|
31
31
|
}));
|
|
32
32
|
|
|
@@ -41,7 +41,7 @@ test('emits only the allow response shape after a remap', () => {
|
|
|
41
41
|
permissionDecision: 'allow',
|
|
42
42
|
updatedInput: {
|
|
43
43
|
model: 'gpt-5.6-luna',
|
|
44
|
-
reasoning_effort: '
|
|
44
|
+
reasoning_effort: 'max',
|
|
45
45
|
},
|
|
46
46
|
});
|
|
47
47
|
});
|
|
@@ -52,7 +52,7 @@ test('preserves every non-routing tool field', () => {
|
|
|
52
52
|
message: 'do the work',
|
|
53
53
|
child_permissions: { shell: 'ask' },
|
|
54
54
|
output_contract: ['summary'],
|
|
55
|
-
model: '
|
|
55
|
+
model: 'Sol',
|
|
56
56
|
reasoning_effort: 'medium',
|
|
57
57
|
};
|
|
58
58
|
|
|
@@ -64,14 +64,14 @@ test('preserves every non-routing tool field', () => {
|
|
|
64
64
|
child_permissions: { shell: 'ask' },
|
|
65
65
|
output_contract: ['summary'],
|
|
66
66
|
model: 'gpt-5.6-luna',
|
|
67
|
-
reasoning_effort: '
|
|
67
|
+
reasoning_effort: 'max',
|
|
68
68
|
});
|
|
69
69
|
assert.deepEqual(toolInput, {
|
|
70
70
|
assignment: 'preserve',
|
|
71
71
|
message: 'do the work',
|
|
72
72
|
child_permissions: { shell: 'ask' },
|
|
73
73
|
output_contract: ['summary'],
|
|
74
|
-
model: '
|
|
74
|
+
model: 'Sol',
|
|
75
75
|
reasoning_effort: 'medium',
|
|
76
76
|
});
|
|
77
77
|
});
|
|
@@ -94,8 +94,8 @@ test('blocks malformed policy, unknown input, unavailable replacement, and untru
|
|
|
94
94
|
buildHookPayload({ model: 'Unknown', reasoning_effort: 'medium' }),
|
|
95
95
|
);
|
|
96
96
|
const unavailableReplacementResponse = buildSubagentModelRoutingResponse(
|
|
97
|
-
buildHookPayload({ model: '
|
|
98
|
-
{ availableModelIds: ['gpt-5.6-
|
|
97
|
+
buildHookPayload({ model: 'Sol', reasoning_effort: 'medium' }),
|
|
98
|
+
{ availableModelIds: ['gpt-5.6-sol'] },
|
|
99
99
|
);
|
|
100
100
|
const untrustedAdvisorResponse = buildSubagentModelRoutingResponse(
|
|
101
101
|
buildHookPayload({
|
|
@@ -122,25 +122,62 @@ test('blocks malformed policy, unknown input, unavailable replacement, and untru
|
|
|
122
122
|
}
|
|
123
123
|
});
|
|
124
124
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
125
|
+
for (const [eachModel, eachEffort, eachSelectedModel, eachSelectedEffort] of [
|
|
126
|
+
['Sol', 'medium', 'gpt-5.6-luna', 'max'],
|
|
127
|
+
['Astra', 'high', 'gpt-6-astra', 'high'],
|
|
128
|
+
]) {
|
|
129
|
+
test(`allows a trusted session advisor requesting ${eachModel} ${eachEffort}`, () => {
|
|
130
|
+
const hookResponse = buildSubagentModelRoutingResponse(
|
|
131
|
+
buildHookPayload({
|
|
132
|
+
agent_type: 'session-advisor',
|
|
133
|
+
model: eachModel,
|
|
134
|
+
reasoning_effort: eachEffort,
|
|
135
|
+
}),
|
|
136
|
+
{
|
|
137
|
+
trustedSessionMetadata: {
|
|
138
|
+
authorized: true,
|
|
139
|
+
registeredAgentType: 'session-advisor',
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
assert.deepEqual(hookResponse.hookSpecificOutput.updatedInput, {
|
|
128
145
|
agent_type: 'session-advisor',
|
|
129
|
-
model:
|
|
130
|
-
reasoning_effort:
|
|
131
|
-
})
|
|
146
|
+
model: eachSelectedModel,
|
|
147
|
+
reasoning_effort: eachSelectedEffort,
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
test('the standalone hook silently remaps native Sol Medium and preserves other fields', () => {
|
|
153
|
+
const hookExecution = spawnSync(
|
|
154
|
+
process.execPath,
|
|
155
|
+
[fileURLToPath(new URL('./subagent_model_routing.mjs', import.meta.url))],
|
|
132
156
|
{
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
157
|
+
encoding: 'utf8',
|
|
158
|
+
input: JSON.stringify(buildHookPayload({
|
|
159
|
+
model: 'gpt-5.6-sol',
|
|
160
|
+
effort: 'medium',
|
|
161
|
+
message: 'keep',
|
|
162
|
+
child_permissions: { shell: 'ask' },
|
|
163
|
+
output_contract: ['summary'],
|
|
164
|
+
})),
|
|
137
165
|
},
|
|
138
166
|
);
|
|
139
|
-
|
|
140
|
-
assert.
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
167
|
+
assert.equal(hookExecution.status, 0);
|
|
168
|
+
assert.equal(hookExecution.stderr, '');
|
|
169
|
+
assert.deepEqual(JSON.parse(hookExecution.stdout), {
|
|
170
|
+
hookSpecificOutput: {
|
|
171
|
+
hookEventName: 'PreToolUse',
|
|
172
|
+
permissionDecision: 'allow',
|
|
173
|
+
updatedInput: {
|
|
174
|
+
model: 'gpt-5.6-luna',
|
|
175
|
+
effort: 'max',
|
|
176
|
+
message: 'keep',
|
|
177
|
+
child_permissions: { shell: 'ask' },
|
|
178
|
+
output_contract: ['summary'],
|
|
179
|
+
},
|
|
180
|
+
},
|
|
144
181
|
});
|
|
145
182
|
});
|
|
146
183
|
|
|
@@ -201,7 +238,7 @@ test('the standalone hook command blocks an advisor without host metadata', () =
|
|
|
201
238
|
|
|
202
239
|
test('keeps a second routing pass unchanged', () => {
|
|
203
240
|
const firstResponse = buildSubagentModelRoutingResponse(buildHookPayload({
|
|
204
|
-
model: '
|
|
241
|
+
model: 'Sol',
|
|
205
242
|
reasoning_effort: 'medium',
|
|
206
243
|
message: 'keep',
|
|
207
244
|
}));
|
|
@@ -212,7 +249,7 @@ test('keeps a second routing pass unchanged', () => {
|
|
|
212
249
|
assert.deepEqual(secondResponse, {});
|
|
213
250
|
assert.deepEqual(firstResponse.hookSpecificOutput.updatedInput, {
|
|
214
251
|
model: 'gpt-5.6-luna',
|
|
215
|
-
reasoning_effort: '
|
|
252
|
+
reasoning_effort: 'max',
|
|
216
253
|
message: 'keep',
|
|
217
254
|
});
|
|
218
255
|
});
|
|
@@ -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
|
|