claude-dev-env 2.0.1 → 2.0.2
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/clean-coder.md +31 -1
- package/audit-rubrics/prompts/category-e-dead-code.md +2 -2
- package/bin/install.test.mjs +2 -2
- package/docs/CLAUDE.md +6 -3
- package/docs/CODE_RULES.md +5 -1
- package/docs/agent-spawn-protocol.md +39 -0
- package/docs/nas-ssh-invocation.md +23 -0
- package/docs/worker-completion-gate.md +33 -0
- package/hooks/blocking/code_rules_dead_module_constant.py +11 -5
- package/hooks/blocking/config/verified_commit_constants.py +8 -1
- package/hooks/blocking/test_code_rules_enforcer_dead_module_constant.py +58 -0
- package/hooks/blocking/test_code_rules_enforcer_dead_module_constant_alias.py +133 -0
- package/hooks/blocking/test_verification_verdict_store.py +12 -0
- package/hooks/blocking/test_verified_commit_docs_delta.py +176 -0
- package/hooks/blocking/tests/test_pii_prevention_blocker.py +0 -1
- package/hooks/hooks_constants/dead_module_constant_constants.py +8 -0
- package/hooks/hooks_constants/pii_prevention_constants.py +1 -0
- package/package.json +1 -1
- package/rules/CLAUDE.md +2 -3
- package/rules/agent-spawn-protocol.md +5 -43
- package/rules/code-standards.md +1 -36
- package/rules/env-var-table-code-drift.md +2 -21
- package/rules/hook-prose-matches-detector.md +5 -16
- package/rules/nas-ssh-invocation.md +3 -15
- package/rules/no-historical-clutter.md +7 -49
- package/rules/no-inline-destructive-literals.md +3 -5
- package/rules/package-inventory-stale-entry.md +7 -32
- package/rules/re-stage-before-commit.md +6 -23
- package/rules/shell-invocation-policy.md +1 -1
- package/rules/vault-context.md +3 -3
- package/rules/workers-done-before-complete.md +2 -30
- package/scripts/claude_chain_runner.py +39 -3
- package/scripts/dev_env_scripts_constants/claude_chain_constants.py +3 -0
- package/scripts/test_claude_chain_runner.py +112 -0
- package/scripts/test_grok_headless_runner.py +0 -1
- package/skills/auditing-claude-config/SKILL.md +1 -1
- package/skills/everything-search/SKILL.md +7 -1
- package/skills/orchestrator/SKILL.md +161 -147
- package/system-prompts/CLAUDE.md +3 -3
- package/docs/agents-md-alignment-plan.md +0 -123
- package/docs/emotion-informed-prompt-design.md +0 -362
- package/rules/es-exe-file-search.md +0 -17
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""Gate behavior for docs-only deltas and the cross-work-tree deny message.
|
|
2
|
+
|
|
3
|
+
Each test builds a real git repository with a real origin remote and drives
|
|
4
|
+
``deny_reason_for_directory`` — the same decision the verified_commit_gate hook
|
|
5
|
+
runs — so the docs-only exemption, the docs-after-verified-code block, and the
|
|
6
|
+
denial message's work-tree keying are asserted against live git state.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import importlib.util
|
|
10
|
+
import pathlib
|
|
11
|
+
import subprocess
|
|
12
|
+
import sys
|
|
13
|
+
|
|
14
|
+
import pytest
|
|
15
|
+
|
|
16
|
+
_HOOK_DIR = pathlib.Path(__file__).parent
|
|
17
|
+
if str(_HOOK_DIR) not in sys.path:
|
|
18
|
+
sys.path.insert(0, str(_HOOK_DIR))
|
|
19
|
+
|
|
20
|
+
gate_spec = importlib.util.spec_from_file_location(
|
|
21
|
+
"verified_commit_gate",
|
|
22
|
+
_HOOK_DIR / "verified_commit_gate.py",
|
|
23
|
+
)
|
|
24
|
+
assert gate_spec is not None
|
|
25
|
+
assert gate_spec.loader is not None
|
|
26
|
+
gate_module = importlib.util.module_from_spec(gate_spec)
|
|
27
|
+
gate_spec.loader.exec_module(gate_module)
|
|
28
|
+
deny_reason_for_directory = gate_module.deny_reason_for_directory
|
|
29
|
+
|
|
30
|
+
store_spec = importlib.util.spec_from_file_location(
|
|
31
|
+
"verification_verdict_store",
|
|
32
|
+
_HOOK_DIR / "verification_verdict_store.py",
|
|
33
|
+
)
|
|
34
|
+
assert store_spec is not None
|
|
35
|
+
assert store_spec.loader is not None
|
|
36
|
+
store_module = importlib.util.module_from_spec(store_spec)
|
|
37
|
+
store_spec.loader.exec_module(store_module)
|
|
38
|
+
resolve_merge_base = store_module.resolve_merge_base
|
|
39
|
+
branch_surface_manifest = store_module.branch_surface_manifest
|
|
40
|
+
manifest_sha256 = store_module.manifest_sha256
|
|
41
|
+
write_verdict = store_module.write_verdict
|
|
42
|
+
|
|
43
|
+
PRODUCTION_SOURCE = "def add(left: int, right: int) -> int:\n return left + right\n"
|
|
44
|
+
BEHAVIORAL_EDIT_SOURCE = "def add(left: int, right: int) -> int:\n return left - right\n"
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _run_git(repo_dir: pathlib.Path, *git_arguments: str) -> None:
|
|
48
|
+
subprocess.run(
|
|
49
|
+
["git", "-C", str(repo_dir), *git_arguments],
|
|
50
|
+
check=True,
|
|
51
|
+
capture_output=True,
|
|
52
|
+
text=True,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _isolate_home(monkeypatch: pytest.MonkeyPatch, fake_home: pathlib.Path) -> None:
|
|
57
|
+
home_text = str(fake_home)
|
|
58
|
+
monkeypatch.setenv("HOME", home_text)
|
|
59
|
+
monkeypatch.setenv("USERPROFILE", home_text)
|
|
60
|
+
monkeypatch.delenv("HOMEDRIVE", raising=False)
|
|
61
|
+
monkeypatch.delenv("HOMEPATH", raising=False)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _empty_hooks_dir(tmp_path: pathlib.Path) -> pathlib.Path:
|
|
65
|
+
hooks_dir = tmp_path / "nohooks"
|
|
66
|
+
hooks_dir.mkdir(exist_ok=True)
|
|
67
|
+
return hooks_dir
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _empty_transcript(tmp_path: pathlib.Path) -> pathlib.Path:
|
|
71
|
+
transcript_path = tmp_path / "projects" / "demo" / "sess1.jsonl"
|
|
72
|
+
transcript_path.parent.mkdir(parents=True, exist_ok=True)
|
|
73
|
+
transcript_path.write_text("", encoding="utf-8")
|
|
74
|
+
return transcript_path
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _init_pushed_repo(tmp_path: pathlib.Path) -> pathlib.Path:
|
|
78
|
+
origin_dir = tmp_path / "origin.git"
|
|
79
|
+
work_dir = tmp_path / "work"
|
|
80
|
+
work_dir.mkdir()
|
|
81
|
+
subprocess.run(
|
|
82
|
+
["git", "init", "--bare", "--initial-branch=main", str(origin_dir)],
|
|
83
|
+
check=True,
|
|
84
|
+
capture_output=True,
|
|
85
|
+
text=True,
|
|
86
|
+
)
|
|
87
|
+
_run_git(work_dir, "init", "--initial-branch=main")
|
|
88
|
+
_run_git(work_dir, "config", "user.email", "tests@example.com")
|
|
89
|
+
_run_git(work_dir, "config", "user.name", "Docs Delta Tests")
|
|
90
|
+
_run_git(work_dir, "config", "core.hooksPath", str(_empty_hooks_dir(tmp_path)))
|
|
91
|
+
(work_dir / "app.py").write_text(PRODUCTION_SOURCE, encoding="utf-8")
|
|
92
|
+
(work_dir / "README.md").write_text("# base\n", encoding="utf-8")
|
|
93
|
+
_run_git(work_dir, "add", "-A")
|
|
94
|
+
_run_git(work_dir, "commit", "-m", "base")
|
|
95
|
+
_run_git(work_dir, "remote", "add", "origin", str(origin_dir))
|
|
96
|
+
_run_git(work_dir, "push", "-u", "origin", "main")
|
|
97
|
+
return work_dir
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _make_docs_only_repo(tmp_path: pathlib.Path) -> pathlib.Path:
|
|
101
|
+
work_dir = _init_pushed_repo(tmp_path)
|
|
102
|
+
(work_dir / "README.md").write_text("# base\n\nUpdated docs.\n", encoding="utf-8")
|
|
103
|
+
return work_dir
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _make_behavioral_code_repo(tmp_path: pathlib.Path) -> pathlib.Path:
|
|
107
|
+
work_dir = _init_pushed_repo(tmp_path)
|
|
108
|
+
(work_dir / "app.py").write_text(BEHAVIORAL_EDIT_SOURCE, encoding="utf-8")
|
|
109
|
+
return work_dir
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _live_surface_hash(work_dir: pathlib.Path) -> str:
|
|
113
|
+
merge_base_sha = resolve_merge_base(str(work_dir))
|
|
114
|
+
assert merge_base_sha is not None
|
|
115
|
+
surface_manifest_text = branch_surface_manifest(str(work_dir), merge_base_sha)
|
|
116
|
+
assert surface_manifest_text is not None
|
|
117
|
+
return manifest_sha256(surface_manifest_text)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def test_pure_docs_only_branch_is_allowed_without_verdict(
|
|
121
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
|
|
122
|
+
) -> None:
|
|
123
|
+
fake_home = tmp_path / "home"
|
|
124
|
+
fake_home.mkdir()
|
|
125
|
+
_isolate_home(monkeypatch, fake_home)
|
|
126
|
+
work_dir = _make_docs_only_repo(tmp_path)
|
|
127
|
+
transcript_path = _empty_transcript(tmp_path)
|
|
128
|
+
assert deny_reason_for_directory(str(work_dir), str(transcript_path)) is None
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def test_readme_change_after_verified_code_commit_is_denied(
|
|
132
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
|
|
133
|
+
) -> None:
|
|
134
|
+
fake_home = tmp_path / "home"
|
|
135
|
+
fake_home.mkdir()
|
|
136
|
+
_isolate_home(monkeypatch, fake_home)
|
|
137
|
+
work_dir = _make_behavioral_code_repo(tmp_path)
|
|
138
|
+
code_surface_hash = _live_surface_hash(work_dir)
|
|
139
|
+
write_verdict(str(work_dir), code_surface_hash, True, [], "agent-x")
|
|
140
|
+
transcript_path = _empty_transcript(tmp_path)
|
|
141
|
+
assert deny_reason_for_directory(str(work_dir), str(transcript_path)) is None
|
|
142
|
+
_run_git(work_dir, "add", "-A")
|
|
143
|
+
_run_git(work_dir, "commit", "-m", "verified code change")
|
|
144
|
+
(work_dir / "README.md").write_text("# added after verification\n", encoding="utf-8")
|
|
145
|
+
deny_reason = deny_reason_for_directory(str(work_dir), str(transcript_path))
|
|
146
|
+
assert deny_reason is not None
|
|
147
|
+
assert "VERIFIED_COMMIT_GATE" in deny_reason
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def test_readme_added_beside_unverified_code_is_denied(
|
|
151
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
|
|
152
|
+
) -> None:
|
|
153
|
+
fake_home = tmp_path / "home"
|
|
154
|
+
fake_home.mkdir()
|
|
155
|
+
_isolate_home(monkeypatch, fake_home)
|
|
156
|
+
work_dir = _make_behavioral_code_repo(tmp_path)
|
|
157
|
+
(work_dir / "README.md").write_text("# docs beside code\n", encoding="utf-8")
|
|
158
|
+
transcript_path = _empty_transcript(tmp_path)
|
|
159
|
+
deny_reason = deny_reason_for_directory(str(work_dir), str(transcript_path))
|
|
160
|
+
assert deny_reason is not None
|
|
161
|
+
assert "VERIFIED_COMMIT_GATE" in deny_reason
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def test_deny_reason_for_directory_names_worktree_keying_and_remedy(
|
|
165
|
+
monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
|
|
166
|
+
) -> None:
|
|
167
|
+
fake_home = tmp_path / "home"
|
|
168
|
+
fake_home.mkdir()
|
|
169
|
+
_isolate_home(monkeypatch, fake_home)
|
|
170
|
+
work_dir = _make_behavioral_code_repo(tmp_path)
|
|
171
|
+
transcript_path = _empty_transcript(tmp_path)
|
|
172
|
+
deny_reason = deny_reason_for_directory(str(work_dir), str(transcript_path))
|
|
173
|
+
assert deny_reason is not None
|
|
174
|
+
lowered_reason = deny_reason.lower()
|
|
175
|
+
assert "work tree" in lowered_reason
|
|
176
|
+
assert "run this command from the work tree" in lowered_reason
|
|
@@ -142,7 +142,6 @@ def test_commit_with_missing_local_identity_still_blocks_the_value(
|
|
|
142
142
|
assert deny_reason is not None
|
|
143
143
|
assert "email" in deny_reason
|
|
144
144
|
|
|
145
|
-
|
|
146
145
|
def _init_repo_with_staged_pii(repository_root: Path) -> None:
|
|
147
146
|
repository_root.mkdir(parents=True, exist_ok=True)
|
|
148
147
|
subprocess.run(["git", "init", "-q"], cwd=repository_root, check=True)
|
|
@@ -20,3 +20,11 @@ DEAD_MODULE_CONSTANT_GUIDANCE: str = (
|
|
|
20
20
|
" module in the enclosing package tree - remove the constant, or reference it"
|
|
21
21
|
" where its value is needed (CODE_RULES §9.8)"
|
|
22
22
|
)
|
|
23
|
+
DEAD_MODULE_CONSTANT_RETRY_GUIDANCE: str = (
|
|
24
|
+
"To land a constant a consumer will read, break the write-order deadlock by"
|
|
25
|
+
" writing the consumer that reads it first: that write completes even though a"
|
|
26
|
+
" transient mypy attr-defined advisory flags the not-yet-defined name (a"
|
|
27
|
+
" non-blocking post-write check), then re-issue this write, which passes once"
|
|
28
|
+
" the consumer is on disk. A constant no module ever reads stays flagged on"
|
|
29
|
+
" every attempt."
|
|
30
|
+
)
|
package/package.json
CHANGED
package/rules/CLAUDE.md
CHANGED
|
@@ -5,7 +5,7 @@ paths:
|
|
|
5
5
|
|
|
6
6
|
# rules
|
|
7
7
|
|
|
8
|
-
Rule files installed into `~/.claude/rules/` by `bin/install.mjs`.
|
|
8
|
+
Rule files installed into `~/.claude/rules/` by `bin/install.mjs`. A rule without `paths:` frontmatter loads at the start of every session; a rule with `paths:` frontmatter loads only when the session works with a file its globs match. The `InstructionsLoaded` log records that match as a `path_glob_match` event. Each `.md` file covers one named rule; hook-enforced rules are also backed by a Python hook in `hooks/`.
|
|
9
9
|
|
|
10
10
|
## Files
|
|
11
11
|
|
|
@@ -37,7 +37,6 @@ Rule files installed into `~/.claude/rules/` by `bin/install.mjs`. Claude Code l
|
|
|
37
37
|
| `no-inline-destructive-literals.md` | No destructive-command literals in Bash tool command strings, even as data |
|
|
38
38
|
| `no-justification-noise.md` | Markdown states facts a reader can act on; cut a present-tense sentence that only justifies a stated choice or restates a gain the reader already works out from the behavior or from a rule enforced elsewhere |
|
|
39
39
|
| `env-var-table-code-drift.md` | Every env-var summary table row in a `.md` file names a code file whose source references the variable |
|
|
40
|
-
| `es-exe-file-search.md` | File search on Windows routes through the `es.exe` CLI with a scoped query; the Everything HTTP server stays off |
|
|
41
40
|
| `orphan-css-class.md` | Every `class="..."` attribute in Python-generated markup has a matching selector in the `<style>` block |
|
|
42
41
|
| `package-inventory-stale-entry.md` | A new production code file added to a directory carries an entry in that directory's `README.md`/`CLAUDE.md` file inventory |
|
|
43
42
|
| `paired-test-coverage.md` | A public function omitted by a module's established paired test suite must get a behavioral test |
|
|
@@ -50,7 +49,7 @@ Rule files installed into `~/.claude/rules/` by `bin/install.mjs`. Claude Code l
|
|
|
50
49
|
| `research-mode.md` | Three anti-hallucination constraints: say "I don't know", verify with citations, quote for factual grounding |
|
|
51
50
|
| `right-sized-engineering.md` | Simple over clever; functions over classes; concrete over abstract |
|
|
52
51
|
| `self-contained-docs.md` | Every document is fully self-contained; no references to the conversation that produced it |
|
|
53
|
-
| `shell-invocation-policy.md` | All Windows shell commands use `pwsh`; `
|
|
52
|
+
| `shell-invocation-policy.md` | All Windows shell commands use `pwsh`; `Audit-ShellPolicy.ps1` reports the non-`pwsh` forms in the `settings.json` permission rules and `Migrate-ShellPolicy.ps1` rewrites them to `pwsh`, both run on demand, not as a live gate |
|
|
54
53
|
| `tdd.md` | Test-driven development: red → green → refactor, no production code before a failing test |
|
|
55
54
|
| `testing.md` | Test quality and infrastructure standards |
|
|
56
55
|
| `vault-context.md` | Search Obsidian vault for prior sessions and decisions before substantive project work |
|
|
@@ -1,47 +1,9 @@
|
|
|
1
1
|
# Agent Spawn Protocol (Mandatory)
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Before any Agent or Task tool spawn (Explore, implementation, research, or team subagents):
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
1. **Check context sufficiency** — you can name the files involved, the constraints, and what success looks like, and the task is unambiguous. When you cannot, investigate or ask the user first; do not spawn with incomplete context.
|
|
6
|
+
2. **Craft the prompt with `/prompt-generator`** — feed it the goal, the target files from step 1, the constraints, the output format, and the acceptance criteria; use its output as the agent's `prompt`.
|
|
7
|
+
3. **Spawn** with that structured prompt.
|
|
6
8
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
Every Agent and Task tool call must follow this protocol. This includes Explore agents, research agents, execution agents, and team members.
|
|
10
|
-
|
|
11
|
-
### Step 1: Context sufficiency check
|
|
12
|
-
|
|
13
|
-
Before writing any agent prompt, verify you can answer all of these:
|
|
14
|
-
- [ ] What specific files, directories, or areas of the codebase are involved?
|
|
15
|
-
- [ ] What constraints apply? (patterns to follow, things NOT to change, boundaries)
|
|
16
|
-
- [ ] What does success look like? (expected output, acceptance criteria)
|
|
17
|
-
- [ ] Is the task unambiguous enough to delegate?
|
|
18
|
-
|
|
19
|
-
If ANY answer is "I don't know" -- investigate first (read files, search code) or ask the user. Do NOT spawn with incomplete context.
|
|
20
|
-
|
|
21
|
-
### Step 2: Craft the prompt with /prompt-generator
|
|
22
|
-
|
|
23
|
-
Run the `/prompt-generator` skill to produce a structured prompt. Feed it:
|
|
24
|
-
- The task description and goal
|
|
25
|
-
- Target files/directories discovered in Step 1
|
|
26
|
-
- Constraints and boundaries
|
|
27
|
-
- Expected output format
|
|
28
|
-
- Acceptance criteria
|
|
29
|
-
|
|
30
|
-
The skill will ask 1-3 clarifying questions if information is missing -- this is the built-in context verification.
|
|
31
|
-
|
|
32
|
-
Use the skill's output as the agent's `prompt` parameter.
|
|
33
|
-
|
|
34
|
-
### Step 3: Spawn the agent
|
|
35
|
-
|
|
36
|
-
Pass the structured prompt from Step 2 to the Agent/Task tool.
|
|
37
|
-
|
|
38
|
-
</agent_spawn_protocol>
|
|
39
|
-
|
|
40
|
-
## Why
|
|
41
|
-
|
|
42
|
-
Agents receiving vague prompts waste tokens exploring in circles, produce code that misses constraints, and require expensive rework. A 30-second investment in prompt quality via /prompt-generator saves 5-minute agent failures. This applies equally to Explore agents (which waste context on unfocused searches) and execution agents (which write wrong code).
|
|
43
|
-
|
|
44
|
-
## Relationship to other rules
|
|
45
|
-
|
|
46
|
-
- **conservative-action.md** gates acting when ambiguous. This extends that: do not delegate when the task is ambiguous—investigate or ask the user first.
|
|
47
|
-
- Project-specific rules or `~/.claude/CLAUDE.md` may define *whether* to use subagents or teams; this rule governs *how* to craft prompts when you do delegate.
|
|
9
|
+
Full step detail, rationale, and relationship to other rules: `@~/.claude/docs/agent-spawn-protocol.md`.
|
package/rules/code-standards.md
CHANGED
|
@@ -3,39 +3,4 @@
|
|
|
3
3
|
> **MANDATORY REFERENCE:** CODE_RULES.md - Load for ALL code generation.
|
|
4
4
|
> This is the single source of truth for code standards. Non-negotiable.
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
- Self-documenting code (no comments)
|
|
8
|
-
- Centralized configuration (one source of truth)
|
|
9
|
-
- Reuse constants (search before creating)
|
|
10
|
-
- No magic values (everything named)
|
|
11
|
-
- No abbreviations (full words)
|
|
12
|
-
- Complete type hints
|
|
13
|
-
- TDD (test first)
|
|
14
|
-
|
|
15
|
-
## Function Parameters - Required vs Optional
|
|
16
|
-
|
|
17
|
-
**Use required parameters when no valid use case exists for optional.**
|
|
18
|
-
**Remove unused parameters.**
|
|
19
|
-
|
|
20
|
-
## Encapsulation - Logic Belongs in Models
|
|
21
|
-
|
|
22
|
-
**NEVER scatter construction logic in calling code.**
|
|
23
|
-
|
|
24
|
-
Path/URL building, formatting, transformations -> Put in model methods.
|
|
25
|
-
If you find yourself building the same string pattern in multiple places, it belongs in the model.
|
|
26
|
-
|
|
27
|
-
## Document Temporary Code
|
|
28
|
-
|
|
29
|
-
**Scaffolding/placeholder code MUST have TODO comments.**
|
|
30
|
-
|
|
31
|
-
When code exists only to enable testing before full implementation:
|
|
32
|
-
- Add `// TODO: Replace with...` explaining what will replace it
|
|
33
|
-
- Explain WHY it's temporary, not just WHAT it does
|
|
34
|
-
|
|
35
|
-
## Naming Reflects Behavior
|
|
36
|
-
|
|
37
|
-
**Name components after what they ARE, not abstract concepts.**
|
|
38
|
-
|
|
39
|
-
If it overlays the viewport -> "Overlay" not "Screen"
|
|
40
|
-
If it validates input -> "Validator" not "Handler"
|
|
41
|
-
Names should describe observable behavior or visual appearance.
|
|
6
|
+
`CODE_RULES.md` (`~/.claude/docs/CODE_RULES.md`) is the compact reference for every standard: self-documenting names, centralized configuration, constant reuse, no magic literals, full words, complete type hints, required-vs-optional parameters, construction logic in the model, temporary-code `TODO:` markers, behavior-first component names, and TDD.
|
|
@@ -5,25 +5,6 @@ paths:
|
|
|
5
5
|
|
|
6
6
|
# Env-Var Summary Table Names a Code File That Reads the Variable
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Every row in an env-var summary table pairs an UPPER_SNAKE variable with a code-file path that reads it — written as `` | `GOOGLE_APPLICATION_CREDENTIALS` | `auth/google_auth.py` | ... | ``. When a code change removes the last read of a variable from a file, the same change drops or corrects the table row that names that file.
|
|
9
9
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
Every row in an env-var summary table names a code file whose source references the variable. A row pairs an UPPER_SNAKE variable with a code-file path, written as `` | `GOOGLE_APPLICATION_CREDENTIALS` | `auth/google_auth.py` | ... | ``, and the named file reads that variable. When the file exists yet its source never mentions the variable name, the row is stale: the table points a reader at a consumer relationship the code does not have, so a reader trusts the doc to behavior the code dropped.
|
|
13
|
-
|
|
14
|
-
When a code change removes the last read of a variable from a file, the same change drops or corrects the table row that names that file. The doc and the code move together in one commit.
|
|
15
|
-
|
|
16
|
-
## What the gate checks
|
|
17
|
-
|
|
18
|
-
The `env_var_table_code_drift_blocker.py` hook runs on every Write, Edit, and MultiEdit whose target is a `.md` file. It:
|
|
19
|
-
|
|
20
|
-
1. Reads the content the tool would leave on disk, skipping lines inside a fenced code block.
|
|
21
|
-
2. Collects each table row whose first cell names an UPPER_SNAKE variable and whose later cell names a code file with a recognized extension (`.py`, `.mjs`, `.js`, `.ts`, `.ps1`, `.sh`).
|
|
22
|
-
3. Resolves the named code file under the repository root (the nearest `.git`-bearing ancestor of the markdown file) and reads its source.
|
|
23
|
-
4. Blocks the write when the file resolves yet its source never references the variable name. For an Edit, drift the file already held on an untouched row is excluded, so only drift the edit introduces is reported.
|
|
24
|
-
|
|
25
|
-
The check stays quiet for a row whose code file resolves nowhere under the repository root (it cannot prove the drift), a row whose second cell holds no code-file path, and a table row inside a fenced code block.
|
|
26
|
-
|
|
27
|
-
## Why this is a hook, not a lint pass
|
|
28
|
-
|
|
29
|
-
An env-var table that names a file whose source skips the variable reads as a correct map of which code consumes which setting, while pointing one row at behavior the code dropped. A reader trusting the row chases a setting the file ignores, and the gap survives review because the table still looks complete. Catching it as the doc is written keeps the table and the code in step.
|
|
10
|
+
`env_var_table_code_drift_blocker.py` (PreToolUse on Write|Edit|MultiEdit of `.md`) blocks a row whose named code file exists yet never references the variable, and names the fix. For an Edit, drift a file already held on an untouched row is excluded; a row whose code file resolves nowhere stays quiet (the hook cannot prove the drift).
|
|
@@ -4,23 +4,12 @@ paths: **/hooks/**/*.py
|
|
|
4
4
|
|
|
5
5
|
# Hook Prose Matches Its Detector
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
A hook's docstring lead narrative and its `CORRECTIVE_MESSAGE` describe exactly the shapes the detector flags — no broader trigger surface than the regex enforces.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
`hook_prose_detector_consistency` (PreToolUse on Write|Edit of hook modules and `*_constants.py` companions) blocks prose that claims a trigger the detector never fires on, and names the fix.
|
|
10
10
|
|
|
11
|
-
##
|
|
11
|
+
## Judgment
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
After writing a hook, ask: would a token that matches 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.
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
A path-shape blocker detects a per-iteration token only when the token sits next to a path separator (its detection regex keys off a `[\\/]`-style character class). Such a hook must not claim it blocks an "output-key segment": a quoted structured-output key alone, with no looped path, is never flagged. The `*_constants.py` companion holds the corrective message and not the detector, so the phrase "output-key segment" describing a blocked trigger is itself the violation there, regardless of which file holds the regex.
|
|
18
|
-
|
|
19
|
-
| Prohibited claim | Why it overstates | Correct phrasing |
|
|
20
|
-
|---|---|---|
|
|
21
|
-
| "appears as a path or output-key segment" | the detector keys off a path separator only | "appears as a per-iteration path segment" |
|
|
22
|
-
| docstring: "blocks a bare token like `cand_i`" | a bare prose token next to no separator is not flagged | "blocks a per-iteration path like `${work}\cand_i\plate.svg`" |
|
|
23
|
-
|
|
24
|
-
## The test
|
|
25
|
-
|
|
26
|
-
After writing a hook, ask: **would a token that matches 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.
|
|
15
|
+
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.
|
|
@@ -1,23 +1,11 @@
|
|
|
1
1
|
# NAS SSH Invocation Policy
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
## Rule
|
|
6
|
-
|
|
7
|
-
Reach the NAS through the Windows OpenSSH binary with batch mode on. Git Bash's MSYS `ssh` reads `~/.ssh/id_ed25519` as world-readable through its ACL mapping, rejects the key as bad permissions, offers no key, and falls back to an interactive password prompt. In an unattended session no one answers that prompt, so the session hangs. The `System32/OpenSSH` binary authenticates the same key without a prompt.
|
|
8
|
-
|
|
9
|
-
Use this form for every NAS ssh command:
|
|
3
|
+
Reach the NAS through the Windows `System32/OpenSSH` binary with `-o BatchMode=yes` on every `ssh`, `scp`, or `sftp` command:
|
|
10
4
|
|
|
11
5
|
```
|
|
12
6
|
"/c/Windows/System32/OpenSSH/ssh.exe" -o BatchMode=yes -o ConnectTimeout=10 -p 22 operator@nas.example.local "<cmd>"
|
|
13
7
|
```
|
|
14
8
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
The host, ssh user, and port come from the `CLAUDE_NAS_*` environment variables or `~/.claude/local-identity.json`; the committed examples show placeholders (`nas.example.local`, `operator`, `22`).
|
|
18
|
-
|
|
19
|
-
`-o BatchMode=yes` is required, not optional: it turns a key-authentication failure into a loud non-zero exit rather than a silent password prompt, so an auth regression surfaces as an error you can read.
|
|
20
|
-
|
|
21
|
-
## Enforcement
|
|
9
|
+
Git Bash's MSYS `ssh` falls back to an interactive password prompt that hangs an unattended session; the `System32/OpenSSH` binary authenticates the key without a prompt, and `-o BatchMode=yes` turns an auth failure into a loud non-zero exit. `nas_ssh_binary_enforcer.py` (PreToolUse on Bash) enforces this: it denies a bare ssh-family word aimed at the NAS, and denies the full binary when `-o BatchMode=yes` is absent.
|
|
22
10
|
|
|
23
|
-
|
|
11
|
+
Host, user, and port config, the `scp`/`sftp` forms, and the full rationale: `@~/.claude/docs/nas-ssh-invocation.md`.
|
|
@@ -11,58 +11,16 @@ paths:
|
|
|
11
11
|
|
|
12
12
|
# No Historical Clutter in Documentation or Comments
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
Never reference removed implementations, old defaults, prior behaviors, or earlier contracts when updating documentation or comments. The current state is all that matters. A module or function docstring carries the same describe-current-state-only contract as a `.md` file.
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
`state_description_blocker` (PreToolUse on Write|Edit) blocks historical and comparative phrases in `.md` prose, code comments, and Python docstrings; a phrase wrapped in double quotes or backticks inside a docstring counts as a mention and is skipped. The denial names the matched phrases and shows a rewrite example.
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
## What stays allowed
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
A module or function docstring carries the same describe-current-state-only contract as a `.md` file.
|
|
25
|
-
|
|
26
|
-
## Examples of prohibited patterns
|
|
27
|
-
|
|
28
|
-
### In documentation (.md files)
|
|
29
|
-
|
|
30
|
-
| Pattern | Why it's clutter |
|
|
31
|
-
|---------|-----------------|
|
|
32
|
-
| `` `"instead of 30"` `` in a pagination rule | The old default `no longer` exists in code; the rule reader doesn't need to know what it was |
|
|
33
|
-
| `` `"previously this used X"` `` | If X is gone, it's noise |
|
|
34
|
-
| `` `"before this rule, we did Y"` `` | The rule exists now; the before-state is irrelevant |
|
|
35
|
-
| `` `"migrated from Z to W"` `` | If Z is fully removed, the migration story is git history, not documentation |
|
|
36
|
-
| `` `"the old implementation did A"` `` | If A is gone, the reader gains nothing from knowing it existed |
|
|
37
|
-
| `` `"originally"` `` / `` `"used to be"` `` | Same — dead context |
|
|
38
|
-
|
|
39
|
-
### In code comments
|
|
40
|
-
|
|
41
|
-
| Pattern | Good replacement |
|
|
42
|
-
|---------|-----------------|
|
|
43
|
-
| `# Uses X instead of Y` | `# Uses X` |
|
|
44
|
-
| `# Previously configured via Z` | `# Configured via Z` |
|
|
45
|
-
| `# Now uses the new API client` | `# Uses the new API client` |
|
|
46
|
-
| `# No longer supports legacy mode` | `# Supports modern mode only` |
|
|
47
|
-
| `// Switched to async processing` | `// Processes asynchronously` |
|
|
48
|
-
| `# Replaced by the cache layer` | `# Cache layer handles reads` |
|
|
49
|
-
|
|
50
|
-
### Hook-detected patterns
|
|
51
|
-
|
|
52
|
-
The `state-description-blocker` hook (PreToolUse on Write\|Edit) enforces these patterns automatically:
|
|
53
|
-
|
|
54
|
-
`instead of`, `previously`, `now uses/does/handles/supports/names/includes`, `was previously`, `were previously`, `was formerly`, `was added`, `used to`, `no longer`, `has/have been updated/changed`, `replaced by`, `replaces`, `superseded by`, `supersedes`, `changed from`, `changes from`, `switched from/to`, `migrated from/to`, `moved to/into`, `extracted as`, `updated to`, `originally`, `as of`
|
|
55
|
-
|
|
56
|
-
## What IS allowed
|
|
57
|
-
|
|
58
|
-
- Comparisons to *currently existing* alternatives (e.g., "use `--paginate --slurp | jq`, not `--jq` alone")
|
|
59
|
-
- Rationale that explains *why* a pattern is wrong in terms of present behavior (e.g., "`--jq` runs per-page, so cross-page operations produce wrong results")
|
|
60
|
-
- References to external sources for defects that still exist (e.g., gh CLI #10459)
|
|
20
|
+
- Comparisons to alternatives that still exist (for example, "use `--paginate --slurp | jq`, not `--jq` alone")
|
|
21
|
+
- Rationale that explains why a pattern is wrong in terms of present behavior (for example, "`--jq` runs per-page, so cross-page operations produce wrong results")
|
|
22
|
+
- References to external sources for defects that still exist (for example, gh CLI #10459)
|
|
61
23
|
|
|
62
24
|
## The test
|
|
63
25
|
|
|
64
|
-
After writing
|
|
65
|
-
|
|
66
|
-
## Why
|
|
67
|
-
|
|
68
|
-
Historical references clog context windows and force readers to mentally filter "what was" from "what is." The git log is the authoritative record of what changed and why. Documentation describes the current contract.
|
|
26
|
+
After writing, ask: if someone reads this a year from now with no knowledge of earlier states, does every sentence still make sense and add value? If a sentence only helps someone who knew an earlier state, delete it.
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
# No Inline Destructive-Command Literals in Bash
|
|
2
2
|
|
|
3
|
-
The `destructive_command_blocker` PreToolUse hook matches destructive patterns (`rm -rf`, `git reset --hard`, `dd`, `mkfs`, `chmod -R`, fork bombs) as raw text anywhere in a Bash-tool command, with no quote-awareness — so a destructive literal carried only as
|
|
3
|
+
The `destructive_command_blocker` PreToolUse hook matches destructive patterns (`rm -rf`, `git reset --hard`, `dd`, `mkfs`, `chmod -R`, fork bombs) as raw text anywhere in a Bash-tool command, with no quote-awareness — so a destructive literal carried only as data (a commit message, a PR/issue body, an echoed string, a `python -c` / `node -e` / `awk` argument, a heredoc) trips the confirmation prompt even though the shell never executes it. In a background or auto-mode run no human can answer that prompt, so the call stalls.
|
|
4
4
|
|
|
5
5
|
Keep destructive literals out of the Bash command string:
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
- To exercise or verify
|
|
7
|
+
- Bodies that describe destructive-command behavior go in a file passed by path — `git commit -F <file>`, `gh ... --body-file <file>` (see [`gh-body-file`](gh-body-file.md)) — never `git commit -m` / `gh ... -b`.
|
|
8
|
+
- To exercise or verify the blocker (or any hook), run the committed test suite (`python -m pytest <test_file>`), which passes the command strings as in-language data — never an inline `python -c` harness.
|
|
9
9
|
- Genuine cleanup targets the OS temp dir or `$CLAUDE_JOB_DIR/tmp` (auto-allowed as ephemeral), never a repository or worktree path.
|
|
10
|
-
|
|
11
|
-
The `destructive_command_blocker` hook is the enforcement surface; this rule is how to keep a non-executing mention from tripping it.
|
|
@@ -10,41 +10,16 @@ paths:
|
|
|
10
10
|
|
|
11
11
|
# New Production File Absent From Its Package Inventory
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
A package directory that documents its own files in a `README.md` Layout table, a `CLAUDE.md` "Key files" list, or a skill `SKILL.md` Layout table keeps that inventory in step with the directory. When you create a new production file in such a directory, add an entry naming it — a row in the table or a bullet in the list — in the same change. The entry names the file in backticks and says what it does.
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
`package_inventory_stale_blocker.py` (PreToolUse on Write) blocks a new production file whose basename appears in no present inventory and names the fix. A skill `SKILL.md` Layout table that maps `scripts/` counts as the inventory for files in that subdirectory.
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
## Judgment the gate cannot derive
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
The file-list entry is the slice the gate checks by name. Two free-prose slices stay with judgment and belong in the same change:
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
1. **Purpose / scope sentence.** When the new module adds a responsibility the package `## Purpose` (or the parent inventory's one-line summary of this subdirectory) omits, broaden that sentence to name it. A hook cannot derive a module's responsibility from its filename.
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
2. **Per-file description clause.** When a file gains a responsibility the inventory's em-dash description omits — a new public function, a new module-level constant — broaden the description clause to name it. The gate only checks that the basename appears once; it never reads the description. Constants modules (`*_constants.py`, or any `.py` directly inside `config/`) are the common shape: the constant's other home is the module docstring, so the clause that lands in the docstring lands in the inventory description in the same change. The gate fires on Write of a new file and skips files directly inside `config/`, so an Edit that adds a constant to an existing config module matches neither path.
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
This scope-sentence slice is free prose: a hook cannot derive a module's responsibility from its filename, so the gate leaves it to judgment. It is the judgment companion to the file-list entry the gate enforces, and it belongs in the same change. This is the `category-o-docstring-vs-impl-drift` (O8) orphaned-doc-claim shape applied to a package inventory: a behavior change orphans a scope claim the prose still makes.
|
|
28
|
-
|
|
29
|
-
## Companion: keep a per-file description in step with the file it describes
|
|
30
|
-
|
|
31
|
-
The per-file entry a `CLAUDE.md` "Key files" list or a `README.md` Layout table gives each file carries more than the backticked filename the gate checks for. The clause after the file name — the em-dash description — is itself a free-prose scope claim about what the file holds. When the file gains a responsibility the description omits — a new public function, a new constant — the same change broadens the description clause to name it. The gate's file-list check passes the moment the file name appears once; it never reads the description clause, so a stale description beside a present file name stays invisible to the gate.
|
|
32
|
-
|
|
33
|
-
A constants module is the common shape of this drift. A file whose name ends `_constants.py`, or any `.py` directly inside a `config/` directory, holds a set of module-level constants, and a sibling inventory describes that file by listing the set — `` `stp_constants.py` — the STP archive member constants: the Properties.xml member name, the workspace prefix every asset reference carries, and the source-form nine-patch filename suffix ``. When the file gains a module-level constant the list omits, three claims drift together: the list itself, the scope label that heads it (`the STP archive member constants`), and the package `## Purpose` sentence when that sentence describes the file's contents. The constant's other home — the module docstring of the constants file — and the sibling inventory's description of that file cover the same set, so the clause that lands in the docstring lands in the inventory description in the same change.
|
|
34
|
-
|
|
35
|
-
This slice sits outside the gate. The gate fires on a Write that creates a new file, and it skips a file directly inside a `config/` directory, so an Edit that adds a constant to an existing `config/` constants module matches neither path. Like the Purpose/scope companion above, it is free prose a hook cannot derive from a file name, so it stays judgment here and a Category O8 finding at audit: a behavior change orphans a description claim the inventory still makes.
|
|
36
|
-
|
|
37
|
-
## What the gate checks
|
|
38
|
-
|
|
39
|
-
The `package_inventory_stale_blocker.py` hook runs on every Write whose target is a new file (a path not yet on disk). It:
|
|
40
|
-
|
|
41
|
-
1. Skips a target that is not a production code file (`.py`, `.mjs`, `.js`, `.ts`, `.ps1`, `.sh`), an exempt basename (`__init__.py`, `conftest.py`, `setup.py`, `_path_setup.py`), a test file (`test_*.py`, `*_test.py`, `*.spec.*`, `*.test.*`), or a file directly inside a `config/` or `tests/` directory.
|
|
42
|
-
2. Reads each `README.md`, `CLAUDE.md`, and `SKILL.md` present in the target's own directory and, when the target sits in a `scripts/` subdirectory, the parent directory's `SKILL.md`, and collects every bare filename they name in backticks. A backticked token holding a path contributes its final segment, so `pipeline/seam_continuity.py` in an inventory counts as naming `seam_continuity.py` and `scripts/stp_selection.py` in a parent `SKILL.md` Layout table counts as naming `stp_selection.py`. A multi-word command-example span — one carrying whitespace or shell punctuation (`:`, `$`, `<`, `>`), such as `parent:node_modules package.json` or `python <file>.py` — names no literal file and is dropped.
|
|
43
|
-
3. Filters the named basenames to those that exist as a file in the target's own directory — the inventory's own sibling files — and treats the directory as carrying a maintained inventory only when two or more such sibling files are named. A directory with no inventory, one whose `README.md` mentions a single file in passing, or one whose inventory prose names only files living in other directories (so no named basename is an on-disk sibling) is out of scope.
|
|
44
|
-
4. Blocks the write when the new file's basename appears in no present inventory. An unreadable or oversized inventory document is skipped, so a missing inventory never blocks a write.
|
|
45
|
-
|
|
46
|
-
The check fires on Write only — editing an existing file adds no new inventory entry — and stays quiet for a directory with no inventory document, an inventory naming too few siblings to be a maintained list, an exempt or test file, and a file the inventory already names.
|
|
47
|
-
|
|
48
|
-
## Why this is a hook, not a lint pass
|
|
49
|
-
|
|
50
|
-
A package inventory that omits a file reads as a complete map of the directory while leaving one file off it. A reader trusting the inventory to list the package misses the new file, and the gap survives review because the inventory still looks complete. Catching it as the new file is written keeps the inventory and the directory in step. This is the counterpart to `claude-md-orphan-file.md`, which catches the reverse drift: an inventory entry naming a file the directory does not hold.
|
|
25
|
+
This is the `category-o-docstring-vs-impl-drift` (O8) orphaned-doc-claim shape applied to a package inventory.
|