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
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
"""Code quality checks validator.
|
|
2
2
|
|
|
3
3
|
Implements:
|
|
4
|
-
- Check 30: Function too long (max 30 lines)
|
|
4
|
+
- Check 30: Function too long (max 30 lines), production files only
|
|
5
5
|
- Check 31: Nesting too deep (max 2 levels)
|
|
6
|
-
- Check 32: File too long (max 400 lines)
|
|
6
|
+
- Check 32: File too long (max 400 lines), production files only
|
|
7
|
+
|
|
8
|
+
The two length caps read production files only. A test module grows with the
|
|
9
|
+
surface it covers, and a linear list of assertions is what a reader wants there,
|
|
10
|
+
so capping its length pushes a contributor to split a suite for the counter
|
|
11
|
+
rather than for the reader. ``magic_value_checks`` short-circuits on the same
|
|
12
|
+
category through the same helper.
|
|
7
13
|
"""
|
|
8
14
|
|
|
9
15
|
import ast
|
|
@@ -11,6 +17,7 @@ import sys
|
|
|
11
17
|
from pathlib import Path
|
|
12
18
|
from typing import List
|
|
13
19
|
|
|
20
|
+
from .exempt_paths import is_test_file
|
|
14
21
|
from .validator_base import Violation, source_text, syntax_tree
|
|
15
22
|
|
|
16
23
|
|
|
@@ -22,6 +29,9 @@ MAX_FILE_LINES = 400
|
|
|
22
29
|
def check_function_length(tree: ast.AST, filename: str) -> List[Violation]:
|
|
23
30
|
violations: List[Violation] = []
|
|
24
31
|
|
|
32
|
+
if is_test_file(filename):
|
|
33
|
+
return violations
|
|
34
|
+
|
|
25
35
|
for node in ast.walk(tree):
|
|
26
36
|
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
27
37
|
if node.end_lineno and node.lineno:
|
|
@@ -74,6 +84,9 @@ def check_file_length(file_path: Path) -> List[Violation]:
|
|
|
74
84
|
violations: List[Violation] = []
|
|
75
85
|
filename = str(file_path)
|
|
76
86
|
|
|
87
|
+
if is_test_file(filename):
|
|
88
|
+
return violations
|
|
89
|
+
|
|
77
90
|
try:
|
|
78
91
|
lines = source_text(file_path).splitlines()
|
|
79
92
|
except Exception as error:
|
|
@@ -127,3 +127,25 @@ class TestFileLength:
|
|
|
127
127
|
assert "400" in violations[0].message or "lines" in violations[0].message.lower()
|
|
128
128
|
finally:
|
|
129
129
|
temp_path.unlink()
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class TestTestFileExemption:
|
|
133
|
+
def test_long_function_in_a_test_file_passes(self) -> None:
|
|
134
|
+
tree = ast.parse(BAD_LONG_FUNCTION)
|
|
135
|
+
violations = check_function_length(tree, "packages/app/test_widget.py")
|
|
136
|
+
assert violations == []
|
|
137
|
+
|
|
138
|
+
def test_long_function_outside_a_test_file_still_fails(self) -> None:
|
|
139
|
+
tree = ast.parse(BAD_LONG_FUNCTION)
|
|
140
|
+
violations = check_function_length(tree, "packages/app/widget.py")
|
|
141
|
+
assert len(violations) == 1
|
|
142
|
+
|
|
143
|
+
def test_long_test_file_passes(self, tmp_path: Path) -> None:
|
|
144
|
+
temp_path = tmp_path / "test_widget.py"
|
|
145
|
+
temp_path.write_text("x = 1\n" * 450, encoding="utf-8")
|
|
146
|
+
assert check_file_length(temp_path) == []
|
|
147
|
+
|
|
148
|
+
def test_long_conftest_passes(self, tmp_path: Path) -> None:
|
|
149
|
+
temp_path = tmp_path / "conftest.py"
|
|
150
|
+
temp_path.write_text("x = 1\n" * 450, encoding="utf-8")
|
|
151
|
+
assert check_file_length(temp_path) == []
|
package/package.json
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-dev-env",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "8.0.0",
|
|
4
4
|
"description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"claude-dev-env": "bin/install.mjs",
|
|
8
|
-
"cde-pstack": "bin/pstack.mjs",
|
|
9
8
|
"codex-compat": "bin/codex-compat.mjs",
|
|
10
9
|
"cde": "bin/cde.mjs"
|
|
11
10
|
},
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# CI Owns the Gate
|
|
2
|
+
|
|
3
|
+
**When this applies:** Before pushing a branch, and any time a repository's full
|
|
4
|
+
check suite or policy gate is about to run on your own machine.
|
|
5
|
+
|
|
6
|
+
## Rule
|
|
7
|
+
|
|
8
|
+
The gate runs once, and it runs on CI. Push the branch, read the verdict, act on
|
|
9
|
+
what it says.
|
|
10
|
+
|
|
11
|
+
The inner development loop stays yours. Run the single test you are writing, as
|
|
12
|
+
often as it helps. That is how the change gets built. This rule governs the
|
|
13
|
+
second full pass, the one whose only product is a prediction of CI's answer.
|
|
14
|
+
Push instead, and spend the wait on the next piece of work.
|
|
15
|
+
|
|
16
|
+
## Why
|
|
17
|
+
|
|
18
|
+
**A pinned gate answers only from its pinned revision.** The workflow names an
|
|
19
|
+
exact revision of the policy package, and that revision decides which rules run.
|
|
20
|
+
The copy under your home directory is a separate artifact at its own revision. A
|
|
21
|
+
run against the home copy reports on those rules, which are a different question
|
|
22
|
+
from the one CI asks. Treat its exit code as information about the home copy
|
|
23
|
+
alone.
|
|
24
|
+
|
|
25
|
+
**Self-hosted runners often share your machine.** Where the runners execute on
|
|
26
|
+
the same host as your shell, a local suite run draws its processor time from the
|
|
27
|
+
runners, and it does so while they work on the branch you just pushed. Read
|
|
28
|
+
where the runners live, and count a local run against the same budget.
|
|
29
|
+
|
|
30
|
+
**One authoritative answer beats two.** Where both runs agree, the second one
|
|
31
|
+
restated the first. Where they differ, the environments differ, and CI is the
|
|
32
|
+
environment that decides.
|
|
33
|
+
|
|
34
|
+
## Running a gate locally
|
|
35
|
+
|
|
36
|
+
Clone the revision the workflow pins, then point the gate at that clone. That
|
|
37
|
+
run asks CI's question and its answer carries. Report a local result by naming
|
|
38
|
+
the revision it used, so a reader can tell which question it answered.
|
|
39
|
+
|
|
40
|
+
## The verdict belongs to CI
|
|
41
|
+
|
|
42
|
+
CI decides whether a change passed, from evidence CI gathered. Keep that loop
|
|
43
|
+
closed. A flag, trailer, receipt, or environment variable through which the
|
|
44
|
+
change under test announces its own result hands the verdict to the subject.
|
|
45
|
+
Where the runner and the agent share one host, a signature names the same party
|
|
46
|
+
twice, so it carries the claim no further.
|
|
47
|
+
|
|
48
|
+
A cache stays available on one condition. CI derives the key itself from the
|
|
49
|
+
tree it is about to test, looks for a previous run under that key, and
|
|
50
|
+
republishes that result. CI computes, CI verifies, CI decides.
|
|
51
|
+
|
|
52
|
+
## Sibling rules
|
|
53
|
+
|
|
54
|
+
| Rule | Role |
|
|
55
|
+
|---|---|
|
|
56
|
+
| [`git-workflow.md`](git-workflow.md) | Confirm each required context fired after the push |
|
|
57
|
+
| [`verify-runtime-state.md`](verify-runtime-state.md) | A status field is a report; read the thing the work was meant to make |
|
|
58
|
+
| [`falsify-before-green.md`](falsify-before-green.md) | A green counts as evidence once the check has run red on a deliberate break |
|
package/rules/code-standards.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> **Canonical review contract:** [`CODE_RULES.md`](../docs/CODE_RULES.md) — the human and AI review contract for code quality, loaded on demand.
|
|
4
4
|
> **Checked-in pointer:** [`.cursor/BUGBOT.md`](../../../.cursor/BUGBOT.md) — the file Cursor BugBot reads; it points at `CODE_RULES.md`.
|
|
5
|
-
> **Production enforcement.** The staged policy lint runs `hooks/blocking/code_rules_enforcer.py` over each changed file. No write-time hook runs it.
|
|
5
|
+
> **Production enforcement.** The staged policy lint runs `hooks/blocking/code_rules_enforcer.py` over each changed file. No write-time hook runs it. CI runs that lint against the merge base. Each mechanical rule carries a synchronization test.
|
|
6
6
|
|
|
7
7
|
## Policy surface map
|
|
8
8
|
|
|
@@ -27,7 +27,6 @@ BDD is the outer process and TDD is the inner loop: [`bdd.md`](bdd.md) discovers
|
|
|
27
27
|
|
|
28
28
|
| Concern | Rule file |
|
|
29
29
|
|---|---|
|
|
30
|
-
| Question routing | [`ask-user-question-required.md`](ask-user-question-required.md) |
|
|
31
30
|
| Task tracking / worker completion | [`workers-done-before-complete.md`](workers-done-before-complete.md) |
|
|
32
31
|
| Multi-step task list | skill `task-build` (see agents catalog) |
|
|
33
32
|
|
|
@@ -28,13 +28,13 @@ Read the body and the docstring side by side. Apply each check that matches the
|
|
|
28
28
|
- **Gate-outcome status flags** — an outcome routed to a blocker (`blocker = ...; break`) reads as blocked everywhere, never as a bypass.
|
|
29
29
|
- **Returns / Raises / Note claims** — each free-form claim matches the body.
|
|
30
30
|
|
|
31
|
-
Many deterministic shapes of this drift are checked in `code_rules_docstrings.py` (and the JS and `.mjs` slices in `code_rules_imports_logging.py`). The staged policy lint reaches both through `code_rules_enforcer.py`, and
|
|
31
|
+
Many deterministic shapes of this drift are checked in `code_rules_docstrings.py` (and the JS and `.mjs` slices in `code_rules_imports_logging.py`). The staged policy lint reaches both through `code_rules_enforcer.py`, and CI runs it against the merge base. Free-form rest is judgment.
|
|
32
32
|
|
|
33
33
|
## Hook prose matches its detector
|
|
34
34
|
|
|
35
35
|
A hook module is the sharpest case of the same rule: its docstring lead narrative and its `CORRECTIVE_MESSAGE` describe exactly the shapes the detector flags, and claim no broader trigger surface than the regex enforces.
|
|
36
36
|
|
|
37
|
-
The staged policy lint carries this as its `hook-prose-consistency` rule, covering hook modules and their `*_constants.py` companions. It reports prose that claims a trigger the detector never fires on, and names the fix. No write-time hook runs it, so
|
|
37
|
+
The staged policy lint carries this as its `hook-prose-consistency` rule, covering hook modules and their `*_constants.py` companions. It reports prose that claims a trigger the detector never fires on, and names the fix. No write-time hook runs it, so CI is where it reports.
|
|
38
38
|
|
|
39
39
|
After writing a hook, ask: would a token matching every word of this message actually trip the detector? When the message names a shape the regex skips, rewrite the message to name only what the regex catches. The path-shape case is the common overstatement — a detector that keys off a path separator must not claim it blocks an "output-key segment". The corrective message spells the rewrite.
|
|
40
40
|
|
|
@@ -64,7 +64,7 @@ The report then presents each candidate to the owner for a durable-fix decision,
|
|
|
64
64
|
|
|
65
65
|
## Enforcement
|
|
66
66
|
|
|
67
|
-
`code_rules_blast_radius.py` runs inside `code_rules_enforcer.py`, which the staged policy lint applies to each changed file under its `code-rules` rule. No write-time hook runs it, so
|
|
67
|
+
`code_rules_blast_radius.py` runs inside `code_rules_enforcer.py`, which the staged policy lint applies to each changed file under its `code-rules` rule. No write-time hook runs it, so CI is where it reports. CI runs the same lint against the merge base.
|
|
68
68
|
|
|
69
69
|
The check requires each raised type written directly inside a loop body to end in `RunFatal` or `ItemBlocked`. The lexical check covers raises written directly in loop bodies. Shared helpers carry multiple caller contexts, so their callers classify the boundary.
|
|
70
70
|
|
package/rules/git-workflow.md
CHANGED
|
@@ -29,9 +29,10 @@ opens with the bot's own marker line. The description rules in this file, the
|
|
|
29
29
|
failure signature in the release job log reads
|
|
30
30
|
`could not parse pull request body as a release PR`.
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
The `pr-description-writer` agent writes a title and body from the diff when
|
|
33
|
+
you want one. Publish the title and body file through
|
|
34
|
+
`~/.agents/skills/pull-request/scripts/pull_request.py`. That path is under the
|
|
35
|
+
agents home, not the repository. A worktree holds no `.agents/` copy.
|
|
35
36
|
|
|
36
37
|
Resolve the active managed root (`CLAUDE_CONFIG_DIR` when set, `~/.claude`
|
|
37
38
|
otherwise), then run `<managed-root>/scripts/durable_post_lint.py` before any
|
|
@@ -50,14 +51,25 @@ untouched.
|
|
|
50
51
|
- Before push: `gh pr ready --undo`
|
|
51
52
|
- After review approved: `gh pr ready`
|
|
52
53
|
|
|
53
|
-
##
|
|
54
|
+
## Confirm the required checks fired, and let CI run them
|
|
54
55
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
56
|
+
The gate runs once, and it runs on CI. Push the branch and read its verdict.
|
|
57
|
+
[`ci-owns-the-gate.md`](ci-owns-the-gate.md) holds the reasoning and the shape
|
|
58
|
+
a local run takes when one is warranted.
|
|
59
|
+
|
|
60
|
+
Read the branch ruleset for the required check contexts before you push a
|
|
61
|
+
branch, or any level of a stack: `gh api repos/<owner>/<repo>/rules/branches/<trunk>`.
|
|
62
|
+
Read it to learn which checks must report. After the push, confirm each of those
|
|
63
|
+
contexts appears on that level's head. A required check that never fired is
|
|
64
|
+
invisible debt at every level, and it surfaces only after the whole stack is
|
|
65
|
+
pushed, when the repair costs a second pass over every branch.
|
|
66
|
+
|
|
67
|
+
A red required check blocks the branch, whoever owns the failing line. The gate
|
|
68
|
+
charges a change for the whole file it touches, so "pre-existing on main" is a
|
|
69
|
+
blame note, not a pass. Fix the line in the next push or report the branch
|
|
70
|
+
blocked. Read the gate's own report rather than a narrower substitute. A
|
|
71
|
+
single-file mypy call cannot see sibling modules and reports false import
|
|
72
|
+
errors, so it neither clears nor convicts a change.
|
|
61
73
|
|
|
62
74
|
A checks listing that reports nothing on the branch is a finding, not a neutral
|
|
63
75
|
state. Find out whether the workflow's event filters exclude the branch, or whether
|
|
@@ -82,7 +94,7 @@ what it calls.
|
|
|
82
94
|
Force-with-lease protects the ref. It protects nobody's understanding of what the
|
|
83
95
|
branch now holds. When a rewrite moves content between levels of a stack, or
|
|
84
96
|
otherwise changes what a branch contains, refresh that pull request's description
|
|
85
|
-
|
|
97
|
+
before you ask anyone to read or merge it.
|
|
86
98
|
|
|
87
99
|
## Never commit working documents or images
|
|
88
100
|
|
|
@@ -15,7 +15,7 @@ When you add a `class="..."` attribute, add its `.<class>` selector to the `<sty
|
|
|
15
15
|
|
|
16
16
|
## What the check covers
|
|
17
17
|
|
|
18
|
-
The `check_orphan_css_classes` check in `code_rules_orphan_css_class.py` reaches production Python through the staged policy lint, whose `code-rules` rule loads `code_rules_enforcer.py` and applies it to each changed file.
|
|
18
|
+
The `check_orphan_css_classes` check in `code_rules_orphan_css_class.py` reaches production Python through the staged policy lint, whose `code-rules` rule loads `code_rules_enforcer.py` and applies it to each changed file. CI runs that lint against the merge base. It:
|
|
19
19
|
|
|
20
20
|
1. Collects each class name referenced in a `class="..."` attribute across the file's string literals.
|
|
21
21
|
2. Collects each class selector defined in a `<style>` block — both in the file under edit and in every Python module beside it (its own directory and immediate child directories), since a markup module commonly imports its style constant from a companion package directory.
|
|
@@ -18,7 +18,7 @@ When you add a public function to a module whose test suite already exercises th
|
|
|
18
18
|
|
|
19
19
|
## What the check covers
|
|
20
20
|
|
|
21
|
-
Two complementary checks in `code_rules_paired_test.py` reach changed files through `code_rules_enforcer.py`, which the staged policy lint runs under its `code-rules` rule. No write-time hook runs them, so
|
|
21
|
+
Two complementary checks in `code_rules_paired_test.py` reach changed files through `code_rules_enforcer.py`, which the staged policy lint runs under its `code-rules` rule. No write-time hook runs them, so CI is where they report, against the merge base. The two checks cover the two write orders.
|
|
22
22
|
|
|
23
23
|
`check_public_function_missing_paired_test` runs on a production Python write or edit and flags a public function when all of these hold:
|
|
24
24
|
|
|
@@ -7,7 +7,7 @@ paths:
|
|
|
7
7
|
|
|
8
8
|
A hook module runs when `hooks/hooks.json` registers it or a dispatcher roster hosts it. A module can survive on disk while appearing in neither place, and it then reaches no tool call. Prose that gives such a module a present-tense action describes a gate the reader never faces, and a reader who trusts it skips the check they were meant to run by hand.
|
|
9
9
|
|
|
10
|
-
The staged policy lint carries this check as its `retired-hook-prose` rule. It reads each Markdown file under a `rules/` directory, takes every backticked hook module name, and reports the ones that are registered nowhere and that carry a present-tense action verb in the rest of the sentence.
|
|
10
|
+
The staged policy lint carries this check as its `retired-hook-prose` rule. It reads each Markdown file under a `rules/` directory, takes every backticked hook module name, and reports the ones that are registered nowhere and that carry a present-tense action verb in the rest of the sentence. CI runs that lint against the merge base.
|
|
11
11
|
|
|
12
12
|
A module name counts as a hook when it ends in one of the hook families, such as `_blocker`, `_enforcer`, `_gate`, `_tracker`, or `_dispatcher`, and when it either sits under `hooks/` or appears in `RETIRED_HOOK_REGISTRATION_RELATIVE_PATHS` in `bin/install.mjs`. A support module inside the hooks tree carries no such family suffix, so it stays out of the check. A hook deleted from disk without an entry in that roster resolves nowhere, so add the roster entry when you retire a hook.
|
|
13
13
|
|
|
@@ -53,7 +53,6 @@
|
|
|
53
53
|
{"model": "luna", "effort": "high"},
|
|
54
54
|
{"model": "luna", "effort": "xhigh"},
|
|
55
55
|
{"model": "luna", "effort": "max"},
|
|
56
|
-
{"model": "sol", "effort": "medium"},
|
|
57
56
|
{"model": "astra", "effort": "low"},
|
|
58
57
|
{"model": "astra", "effort": "medium"},
|
|
59
58
|
{"model": "astra", "effort": "high", "roles": ["advisor"]}
|
|
@@ -66,6 +65,7 @@
|
|
|
66
65
|
{"requested": {"model": "luna", "effort": "low"}, "selected": {"model": "luna", "effort": "high"}},
|
|
67
66
|
{"requested": {"model": "luna", "effort": "medium"}, "selected": {"model": "luna", "effort": "high"}},
|
|
68
67
|
{"requested": {"model": "sol", "effort": "low"}, "selected": {"model": "luna", "effort": "xhigh"}},
|
|
68
|
+
{"requested": {"model": "sol", "effort": "medium"}, "selected": {"model": "luna", "effort": "max"}},
|
|
69
69
|
{"requested": {"model": "terra", "effort": "low"}, "selected": {"model": "luna", "effort": "xhigh"}},
|
|
70
70
|
{"requested": {"model": "terra", "effort": "medium"}, "selected": {"model": "luna", "effort": "xhigh"}},
|
|
71
71
|
{"requested": {"model": "terra", "effort": "high"}, "selected": {"model": "luna", "effort": "xhigh"}},
|
|
@@ -27,7 +27,7 @@ Only after confirming the answer cannot be obtained through any available tool,
|
|
|
27
27
|
A path, port, branch name, or config value you recall from an earlier session counts as unanswered until a tool re-checks it this session. Memory records what was true when it was written; the file may have moved, the port may be down, the branch may have merged. Treat every recalled fact as a claim to re-ground, not an answer to reuse.
|
|
28
28
|
|
|
29
29
|
- When a tool can settle it, re-check in silence and act on the fresh result — no question to the user.
|
|
30
|
-
- When no tool can settle it and the user has a stake in the answer, ask through `AskUserQuestion
|
|
30
|
+
- When no tool can settle it and the user has a stake in the answer, ask through `AskUserQuestion`.
|
|
31
31
|
|
|
32
32
|
## Questions That Belong to the User
|
|
33
33
|
|
|
@@ -12,6 +12,6 @@ Never call `shutil.rmtree` with `ignore_errors=True` — Windows `ReadOnly` file
|
|
|
12
12
|
|
|
13
13
|
In Node, call `mkdirSync(targetPath, { recursive: true })` on possibly-existing paths — `ReadOnly` directories break the non-recursive form. When the call must be non-recursive, strip the attribute first (`(Get-Item $path -Force).Attributes = "Directory"` / `os.chmod(path, stat.S_IWRITE)`).
|
|
14
14
|
|
|
15
|
-
The staged policy lint carries this check as its `rmtree-safety` rule. It reports the unsafe rmtree pattern and returns the full `force_rmtree` safe-pattern code.
|
|
15
|
+
The staged policy lint carries this check as its `rmtree-safety` rule. It reports the unsafe rmtree pattern and returns the full `force_rmtree` safe-pattern code. CI runs that lint against the merge base.
|
|
16
16
|
|
|
17
17
|
Define the safe handler trio (`_strip_read_only_and_retry`, `_force_remove_tree` / `force_rmtree`, and the `inspect.signature` onexc/onerror guard) once in a shared Windows-filesystem utility module, and import it from every call site. A second local copy drifts from the first, so a fix lands in one and the other keeps the bug (CODE_RULES.md, CORE PRINCIPLES, "Reuse before create"). The same `rmtree-safety` lint rule reports a local re-definition of any trio member outside the shared home and points the writer at the import. This complements the same-directory `check_duplicate_function_body_across_files` check, which a copy between two distant packages slips past.
|
|
@@ -9,4 +9,4 @@ In a `.workflow.js` agent-prompt template, every per-call or per-iteration value
|
|
|
9
9
|
|
|
10
10
|
When a loop builds a per-iteration path or output key, write the index as a slot — `cand_<i>` — or spell out `replace <i> with the iteration index 0, 1, 2` in the step text. Every per-call value in a `.workflow.js` template carries angle brackets so an agent fills in a fresh value per call.
|
|
11
11
|
|
|
12
|
-
The staged policy lint carries this check as its `workflow-substitution` rule. It reports a `.workflow.js` file whose looped content holds a bare `<word>_<i|j|k>` token as a per-iteration path segment.
|
|
12
|
+
The staged policy lint carries this check as its `workflow-substitution` rule. It reports a `.workflow.js` file whose looped content holds a bare `<word>_<i|j|k>` token as a per-iteration path segment. CI runs that lint against the merge base. No write-time hook reports this, so a bare token stays on disk until the lint runs.
|
|
@@ -11,7 +11,10 @@
|
|
|
11
11
|
- Only runs when the checked-out branch is the named branch
|
|
12
12
|
- Only runs when there are no tracked changes (untracked files are fine),
|
|
13
13
|
unless -StashDirty is passed
|
|
14
|
-
- Uses merge --ff-only
|
|
14
|
+
- Uses merge --ff-only (never force, never hard reset, never clean)
|
|
15
|
+
- When the fast-forward fails because the files were already copied in,
|
|
16
|
+
moves HEAD with a mixed reset, which writes no files. It keeps that move
|
|
17
|
+
only when every file the remote changed now matches the remote
|
|
15
18
|
- Logs and exits non-zero on skip, diverge, or failure so a Task Scheduler
|
|
16
19
|
history row can surface a problem instead of a silent no-op
|
|
17
20
|
|
|
@@ -128,6 +131,64 @@ function Invoke-GitOrExit {
|
|
|
128
131
|
return $result.Output
|
|
129
132
|
}
|
|
130
133
|
|
|
134
|
+
function Get-NulSeparatedPaths {
|
|
135
|
+
param(
|
|
136
|
+
[Parameter(Mandatory)]
|
|
137
|
+
[AllowEmptyString()]
|
|
138
|
+
[string]$Output
|
|
139
|
+
)
|
|
140
|
+
return @($Output -split "`0" | Where-Object { $_ })
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function Invoke-AdoptCopiedTree {
|
|
144
|
+
<#
|
|
145
|
+
Handles a mirror whose files are refreshed by copy while its HEAD stays
|
|
146
|
+
behind. The fast-forward then refuses to overwrite the copied files,
|
|
147
|
+
even though they already hold the remote content.
|
|
148
|
+
|
|
149
|
+
A mixed reset moves HEAD and the index to the remote tip and writes no
|
|
150
|
+
files. If any file the remote changed still differs afterwards, the
|
|
151
|
+
reset goes back to the old tip, so the tree ends as it started.
|
|
152
|
+
#>
|
|
153
|
+
param(
|
|
154
|
+
[Parameter(Mandatory)]
|
|
155
|
+
[string]$LocalTip,
|
|
156
|
+
[Parameter(Mandatory)]
|
|
157
|
+
[string]$RemoteRef
|
|
158
|
+
)
|
|
159
|
+
$stagedResult = Invoke-Git -Arguments @('diff', '--cached', '--quiet')
|
|
160
|
+
if ($stagedResult.ExitCode -ne 0) {
|
|
161
|
+
Write-SyncLog -Level 'ERROR' -Message 'adopt skipped: the index holds staged changes'
|
|
162
|
+
return $false
|
|
163
|
+
}
|
|
164
|
+
$changedResult = Invoke-Git -Arguments @('diff', '--name-only', '--no-renames', '-z', $LocalTip, $RemoteRef)
|
|
165
|
+
if ($changedResult.ExitCode -ne 0) {
|
|
166
|
+
Write-SyncLog -Level 'ERROR' -Message "adopt skipped: cannot list remote changes: $($changedResult.Output)"
|
|
167
|
+
return $false
|
|
168
|
+
}
|
|
169
|
+
$allRemoteChangedPaths = [System.Collections.Generic.HashSet[string]]::new([string[]](Get-NulSeparatedPaths -Output $changedResult.Output))
|
|
170
|
+
|
|
171
|
+
$resetResult = Invoke-Git -Arguments @('reset', '-q', $RemoteRef)
|
|
172
|
+
if ($resetResult.ExitCode -ne 0) {
|
|
173
|
+
Write-SyncLog -Level 'ERROR' -Message "adopt skipped: mixed reset failed: $($resetResult.Output)"
|
|
174
|
+
return $false
|
|
175
|
+
}
|
|
176
|
+
$differResult = Invoke-Git -Arguments @('diff', '--name-only', '--no-renames', '-z')
|
|
177
|
+
$allMismatchedPaths = @(Get-NulSeparatedPaths -Output $differResult.Output | Where-Object { $allRemoteChangedPaths.Contains($_) })
|
|
178
|
+
if ($differResult.ExitCode -eq 0 -and $allMismatchedPaths.Count -eq 0) {
|
|
179
|
+
return $true
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
$revertResult = Invoke-Git -Arguments @('reset', '-q', $LocalTip)
|
|
183
|
+
if ($revertResult.ExitCode -ne 0) {
|
|
184
|
+
Write-SyncLog -Level 'ERROR' -Message "adopt revert to $LocalTip failed; HEAD is at $RemoteRef with no file changed: $($revertResult.Output)"
|
|
185
|
+
return $false
|
|
186
|
+
}
|
|
187
|
+
$shownPaths = ($allMismatchedPaths | Select-Object -First 20) -join '; '
|
|
188
|
+
Write-SyncLog -Level 'ERROR' -Message "adopt refused: $($allMismatchedPaths.Count) file(s) the remote changed differ from it; HEAD kept at ${LocalTip}: $shownPaths"
|
|
189
|
+
return $false
|
|
190
|
+
}
|
|
191
|
+
|
|
131
192
|
try {
|
|
132
193
|
if (-not (Test-Path -LiteralPath $RepoPath)) {
|
|
133
194
|
Write-SyncLog -Level 'ERROR' -Message "repo path missing: $RepoPath"
|
|
@@ -194,14 +255,18 @@ try {
|
|
|
194
255
|
Write-SyncLog -Level 'ERROR' -Message "merge --ff-only failed: $($mergeResult.Output)"
|
|
195
256
|
if ($didStash) {
|
|
196
257
|
$popResult = Invoke-Git -Arguments @('stash', 'pop')
|
|
197
|
-
if ($popResult.ExitCode -
|
|
198
|
-
Write-SyncLog -Level 'OK' -Message "restored the stashed edits to the working tree after the failed fast-forward"
|
|
199
|
-
}
|
|
200
|
-
else {
|
|
258
|
+
if ($popResult.ExitCode -ne 0) {
|
|
201
259
|
Write-SyncLog -Level 'ERROR' -Message ("stash pop failed after the failed fast-forward; the edits are still stashed as '" + $stashMessage + "' — recover them with git stash list and git stash pop: " + $popResult.Output)
|
|
260
|
+
exit 1
|
|
202
261
|
}
|
|
262
|
+
Write-SyncLog -Level 'OK' -Message "restored the stashed edits to the working tree after the failed fast-forward"
|
|
203
263
|
}
|
|
204
|
-
|
|
264
|
+
if (-not (Invoke-AdoptCopiedTree -LocalTip $localTip -RemoteRef "$Remote/$Branch")) {
|
|
265
|
+
exit 1
|
|
266
|
+
}
|
|
267
|
+
$subject = Get-GitDecoration -Arguments @('log', '-1', '--format=%h %s') -Placeholder '<subject unavailable>'
|
|
268
|
+
Write-SyncLog -Level 'OK' -Message "$Branch $localTip -> $remoteTip adopted (files already matched) | $subject"
|
|
269
|
+
exit 0
|
|
205
270
|
}
|
|
206
271
|
|
|
207
272
|
$subject = Get-GitDecoration -Arguments @('log', '-1', '--format=%h %s') -Placeholder '<subject unavailable>'
|
|
@@ -91,31 +91,137 @@ def _messages_for_validator_outcomes(
|
|
|
91
91
|
)
|
|
92
92
|
|
|
93
93
|
|
|
94
|
+
def _validator_messages_for_text(
|
|
95
|
+
document: Document,
|
|
96
|
+
absolute_path: Path,
|
|
97
|
+
text: str,
|
|
98
|
+
validator_module: ModuleType,
|
|
99
|
+
) -> tuple[str, ...]:
|
|
100
|
+
all_validator_outcomes = validator_module.validate_proposed_file(
|
|
101
|
+
absolute_path.as_posix(),
|
|
102
|
+
text,
|
|
103
|
+
config_source_path=absolute_path,
|
|
104
|
+
include_ruff=False,
|
|
105
|
+
excluded_validator_names=constants.ALL_OVERLAPPING_VALIDATOR_NAMES,
|
|
106
|
+
)
|
|
107
|
+
return _messages_for_validator_outcomes(document, all_validator_outcomes)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _message_body(message: str) -> str:
|
|
111
|
+
_head, separator, body = message.partition(
|
|
112
|
+
constants.VALIDATOR_MESSAGE_PREFIX_SEPARATOR
|
|
113
|
+
)
|
|
114
|
+
return body if separator and body else message
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _message_identity(message: str) -> str:
|
|
118
|
+
return constants.VALIDATOR_MESSAGE_DIGIT_PATTERN.sub(
|
|
119
|
+
constants.VALIDATOR_MESSAGE_DIGIT_PLACEHOLDER, _message_body(message)
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _all_measured_sizes(message: str) -> tuple[int, ...]:
|
|
124
|
+
return tuple(
|
|
125
|
+
int(each_digit_run)
|
|
126
|
+
for each_digit_run in constants.VALIDATOR_MESSAGE_DIGIT_PATTERN.findall(
|
|
127
|
+
_message_body(message)
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _has_grown_since(message: str, prior_message: str) -> bool:
|
|
133
|
+
return any(
|
|
134
|
+
each_size > each_prior_size
|
|
135
|
+
for each_size, each_prior_size in zip(
|
|
136
|
+
_all_measured_sizes(message), _all_measured_sizes(prior_message)
|
|
137
|
+
)
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _prior_messages_by_identity(
|
|
142
|
+
all_prior_messages: tuple[str, ...],
|
|
143
|
+
) -> dict[str, list[str]]:
|
|
144
|
+
all_prior_by_identity: dict[str, list[str]] = {}
|
|
145
|
+
for each_prior_message in all_prior_messages:
|
|
146
|
+
all_prior_by_identity.setdefault(
|
|
147
|
+
_message_identity(each_prior_message), []
|
|
148
|
+
).append(each_prior_message)
|
|
149
|
+
return all_prior_by_identity
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def _is_introduced_message(
|
|
153
|
+
message: str, all_prior_by_identity: dict[str, list[str]]
|
|
154
|
+
) -> bool:
|
|
155
|
+
all_matches = all_prior_by_identity.get(_message_identity(message))
|
|
156
|
+
if not all_matches:
|
|
157
|
+
return True
|
|
158
|
+
return _has_grown_since(message, all_matches.pop())
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _messages_introduced_by_the_change(
|
|
162
|
+
all_candidate_messages: tuple[str, ...],
|
|
163
|
+
all_prior_messages: tuple[str, ...],
|
|
164
|
+
) -> tuple[str, ...]:
|
|
165
|
+
"""Keep the messages the prior text did not already carry.
|
|
166
|
+
|
|
167
|
+
::
|
|
168
|
+
|
|
169
|
+
prior: Function 'main' is 47 lines (max 30)
|
|
170
|
+
now: Function 'main' is 47 lines (max 30) -> dropped, untouched debt
|
|
171
|
+
now: Function 'main' is 61 lines (max 30) -> kept, the change grew it
|
|
172
|
+
now: Function 'added' is 47 lines (max 30) -> kept, a new breach
|
|
173
|
+
|
|
174
|
+
A message keeps its identity when it moves down the file or its measured
|
|
175
|
+
size falls, so only a new breach or a worse one survives. Each prior message
|
|
176
|
+
covers one candidate.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
all_candidate_messages: Messages the post-edit text produced.
|
|
180
|
+
all_prior_messages: Messages the prior text produced.
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
The messages this change is answerable for.
|
|
184
|
+
"""
|
|
185
|
+
all_prior_by_identity = _prior_messages_by_identity(all_prior_messages)
|
|
186
|
+
return tuple(
|
|
187
|
+
each_message
|
|
188
|
+
for each_message in all_candidate_messages
|
|
189
|
+
if _is_introduced_message(each_message, all_prior_by_identity)
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
|
|
94
193
|
def validator_diagnostics(
|
|
95
194
|
document: Document,
|
|
96
195
|
repository_root: Path,
|
|
97
196
|
load_module: adapter_support.HookModuleLoader,
|
|
98
197
|
) -> tuple[Diagnostic, ...]:
|
|
99
|
-
"""Run the fast validator set on one document.
|
|
198
|
+
"""Run the fast validator set on one document, scoped to the change.
|
|
199
|
+
|
|
200
|
+
A module already over the file-length cap reported that breach on every edit
|
|
201
|
+
it received, so a one-line change failed on debt it did not create. Grading
|
|
202
|
+
the prior text the same way and subtracting leaves the breaches this change
|
|
203
|
+
introduced or made worse.
|
|
100
204
|
|
|
101
205
|
Args:
|
|
102
|
-
document: Current source text and path.
|
|
206
|
+
document: Current source text, prior text, and path.
|
|
103
207
|
repository_root: Request repository root for configuration resolution.
|
|
104
208
|
load_module: Hook module loader.
|
|
105
209
|
|
|
106
210
|
Returns:
|
|
107
|
-
Path-normalized diagnostics for
|
|
211
|
+
Path-normalized diagnostics for the failures this change is answerable for.
|
|
108
212
|
"""
|
|
109
213
|
absolute_path = adapter_support._document_path(repository_root, document)
|
|
110
214
|
validator_module = load_module("validators.run_all_validators")
|
|
111
|
-
|
|
112
|
-
absolute_path.
|
|
113
|
-
document.text,
|
|
114
|
-
config_source_path=absolute_path,
|
|
115
|
-
include_ruff=False,
|
|
116
|
-
excluded_validator_names=constants.ALL_OVERLAPPING_VALIDATOR_NAMES,
|
|
215
|
+
all_messages = _validator_messages_for_text(
|
|
216
|
+
document, absolute_path, document.text, validator_module
|
|
117
217
|
)
|
|
118
|
-
|
|
218
|
+
if document.prior_text is not None:
|
|
219
|
+
all_prior_messages = _validator_messages_for_text(
|
|
220
|
+
document, absolute_path, document.prior_text, validator_module
|
|
221
|
+
)
|
|
222
|
+
all_messages = _messages_introduced_by_the_change(
|
|
223
|
+
all_messages, all_prior_messages
|
|
224
|
+
)
|
|
119
225
|
return adapter_support._diagnostics_for_messages(document, "validators", all_messages)
|
|
120
226
|
|
|
121
227
|
|
|
@@ -30,6 +30,18 @@ APPROVED_TEST_PATHS_BY_PRODUCTION_PATH: dict[
|
|
|
30
30
|
),
|
|
31
31
|
}
|
|
32
32
|
),
|
|
33
|
+
PurePosixPath(
|
|
34
|
+
"packages/claude-dev-env/hooks/blocking/code_rules_enforcer.py"
|
|
35
|
+
): frozenset(
|
|
36
|
+
{
|
|
37
|
+
PurePosixPath(
|
|
38
|
+
"packages/claude-dev-env/hooks/blocking/test_code_rules_enforcer_dispatch_wiring.py"
|
|
39
|
+
),
|
|
40
|
+
PurePosixPath(
|
|
41
|
+
"packages/claude-dev-env/hooks/blocking/test_code_rules_enforcer_cap_meta.py"
|
|
42
|
+
),
|
|
43
|
+
}
|
|
44
|
+
),
|
|
33
45
|
PurePosixPath(
|
|
34
46
|
"packages/claude-dev-env/hooks/git-hooks/verification_notice_context.py"
|
|
35
47
|
): frozenset(
|
|
@@ -72,6 +72,9 @@ FAST_SAVE_VALIDATORS_STEM = "fast_save_validators"
|
|
|
72
72
|
ALL_OVERLAPPING_VALIDATOR_NAMES = frozenset(
|
|
73
73
|
{"Python Style", "Magic Values", "Type Safety", "Test Safety", "React"}
|
|
74
74
|
)
|
|
75
|
+
VALIDATOR_MESSAGE_PREFIX_SEPARATOR = ": "
|
|
76
|
+
VALIDATOR_MESSAGE_DIGIT_PATTERN = re.compile(r"\d+")
|
|
77
|
+
VALIDATOR_MESSAGE_DIGIT_PLACEHOLDER = "#"
|
|
75
78
|
ALL_PR_LOOP_SCRIPTS_PATH_SEGMENTS = ("_shared", "pr-loop", "scripts")
|
|
76
79
|
TERMINOLOGY_SWEEP_MODULE_NAME = "terminology_sweep"
|
|
77
80
|
TERMINOLOGY_SWEEP_RULE_ID = "terminology-sweep"
|