claude-dev-env 2.0.0 → 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.
Files changed (44) hide show
  1. package/agents/clean-coder.md +31 -1
  2. package/audit-rubrics/prompts/category-e-dead-code.md +2 -2
  3. package/bin/install.test.mjs +2 -2
  4. package/docs/CLAUDE.md +6 -3
  5. package/docs/CODE_RULES.md +5 -1
  6. package/docs/agent-spawn-protocol.md +39 -0
  7. package/docs/nas-ssh-invocation.md +23 -0
  8. package/docs/worker-completion-gate.md +33 -0
  9. package/hooks/blocking/code_rules_dead_module_constant.py +11 -5
  10. package/hooks/blocking/config/verified_commit_constants.py +8 -1
  11. package/hooks/blocking/test_code_rules_enforcer_dead_module_constant.py +58 -0
  12. package/hooks/blocking/test_code_rules_enforcer_dead_module_constant_alias.py +133 -0
  13. package/hooks/blocking/test_verification_verdict_store.py +12 -0
  14. package/hooks/blocking/test_verified_commit_docs_delta.py +176 -0
  15. package/hooks/blocking/test_verifier_verdict_minter.py +55 -158
  16. package/hooks/blocking/tests/test_pii_prevention_blocker.py +0 -1
  17. package/hooks/hooks_constants/dead_module_constant_constants.py +8 -0
  18. package/hooks/hooks_constants/pii_prevention_constants.py +1 -0
  19. package/package.json +1 -1
  20. package/rules/CLAUDE.md +2 -3
  21. package/rules/agent-spawn-protocol.md +5 -43
  22. package/rules/code-standards.md +1 -36
  23. package/rules/env-var-table-code-drift.md +2 -21
  24. package/rules/hook-prose-matches-detector.md +5 -16
  25. package/rules/nas-ssh-invocation.md +3 -15
  26. package/rules/no-historical-clutter.md +7 -49
  27. package/rules/no-inline-destructive-literals.md +3 -5
  28. package/rules/package-inventory-stale-entry.md +7 -32
  29. package/rules/re-stage-before-commit.md +6 -23
  30. package/rules/shell-invocation-policy.md +1 -1
  31. package/rules/vault-context.md +3 -3
  32. package/rules/workers-done-before-complete.md +2 -30
  33. package/scripts/claude_chain_runner.py +39 -3
  34. package/scripts/dev_env_scripts_constants/claude_chain_constants.py +3 -0
  35. package/scripts/test_claude_chain_runner.py +112 -0
  36. package/scripts/test_grok_headless_runner.py +0 -1
  37. package/skills/auditing-claude-config/SKILL.md +1 -1
  38. package/skills/everything-search/SKILL.md +7 -1
  39. package/skills/orchestrator/SKILL.md +166 -143
  40. package/skills/orchestrator-refresh/SKILL.md +12 -4
  41. package/system-prompts/CLAUDE.md +3 -3
  42. package/docs/agents-md-alignment-plan.md +0 -123
  43. package/docs/emotion-informed-prompt-design.md +0 -362
  44. 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
@@ -69,6 +69,36 @@ def _write_sidecar(agent_transcript_file: pathlib.Path, agent_type: str) -> None
69
69
  )
70
70
 
71
71
 
72
+ def _write_verdict_transcript(agent_transcript_file: pathlib.Path, verdict_body: str) -> None:
73
+ verdict_text = f"ok\n```verdict\n{verdict_body}\n```\n"
74
+ agent_transcript_file.write_text(
75
+ json.dumps(
76
+ {
77
+ "type": "assistant",
78
+ "message": {"content": [{"type": "text", "text": verdict_text}]},
79
+ }
80
+ )
81
+ + "\n",
82
+ encoding="utf-8",
83
+ )
84
+
85
+
86
+ def _make_repo(tmp_path: pathlib.Path) -> pathlib.Path:
87
+ repo_root = tmp_path / "repo"
88
+ repo_root.mkdir()
89
+ return repo_root
90
+
91
+
92
+ def _mint_payload(
93
+ agent_transcript_file: pathlib.Path, repo_root: pathlib.Path, agent_id: str
94
+ ) -> dict[str, str]:
95
+ return {
96
+ "agent_transcript_path": str(agent_transcript_file),
97
+ "cwd": str(repo_root),
98
+ "agent_id": agent_id,
99
+ }
100
+
101
+
72
102
  def test_resolves_subagent_type_from_sidecar(tmp_path: pathlib.Path) -> None:
73
103
  agent_transcript = tmp_path / "agent-7.jsonl"
74
104
  agent_transcript.write_text("", encoding="utf-8")
@@ -141,7 +171,7 @@ def test_verifier_type_without_a_verdict_mints_nothing(tmp_path: pathlib.Path) -
141
171
  assert mint_for_payload(payload) is None
142
172
 
143
173
 
144
- def _init_repo_with_upstream_and_edit(repo_root: pathlib.Path) -> None:
174
+ def _init_repo_with_upstream(repo_root: pathlib.Path) -> None:
145
175
  subprocess.run(["git", "-C", str(repo_root), "init", "-q"], check=True)
146
176
  subprocess.run(
147
177
  ["git", "-C", str(repo_root), "config", "user.email", "verifier@test"], check=True
@@ -151,37 +181,20 @@ def _init_repo_with_upstream_and_edit(repo_root: pathlib.Path) -> None:
151
181
  subprocess.run(["git", "-C", str(repo_root), "add", "-A"], check=True)
152
182
  subprocess.run(["git", "-C", str(repo_root), "commit", "-qm", "init"], check=True)
153
183
  subprocess.run(["git", "-C", str(repo_root), "branch", "-f", "origin/main", "HEAD"], check=True)
184
+
185
+
186
+ def _init_repo_with_upstream_and_edit(repo_root: pathlib.Path) -> None:
187
+ _init_repo_with_upstream(repo_root)
154
188
  (repo_root / "module.py").write_text("answer = 2\n", encoding="utf-8")
155
189
 
156
190
 
157
191
  def test_clean_verifier_verdict_mints_a_verdict_file(tmp_path: pathlib.Path) -> None:
158
- repo_root = tmp_path / "repo"
159
- repo_root.mkdir()
192
+ repo_root = _make_repo(tmp_path)
160
193
  _init_repo_with_upstream_and_edit(repo_root)
161
194
  agent_transcript = tmp_path / "agent-7.jsonl"
162
- agent_transcript.write_text(
163
- json.dumps(
164
- {
165
- "type": "assistant",
166
- "message": {
167
- "content": [
168
- {
169
- "type": "text",
170
- "text": 'ok\n```verdict\n{"all_pass": true, "findings": []}\n```\n',
171
- }
172
- ]
173
- },
174
- }
175
- )
176
- + "\n",
177
- encoding="utf-8",
178
- )
195
+ _write_verdict_transcript(agent_transcript, '{"all_pass": true, "findings": []}')
179
196
  _write_sidecar(agent_transcript, MINTING_AGENT_TYPE)
180
- payload = {
181
- "agent_transcript_path": str(agent_transcript),
182
- "cwd": str(repo_root),
183
- "agent_id": "a02b9583eedc74093",
184
- }
197
+ payload = _mint_payload(agent_transcript, repo_root, "a02b9583eedc74093")
185
198
  verdict_path = mint_for_payload(payload)
186
199
  try:
187
200
  assert verdict_path is not None
@@ -199,7 +212,8 @@ def _deny_rules() -> list[str]:
199
212
 
200
213
 
201
214
  def test_settings_deny_verdict_directory_write() -> None:
202
- assert "Write($HOME/.claude/verification/**)" in _deny_rules()
215
+ assert "Edit($HOME/.claude/verification/**)" in _deny_rules()
216
+ assert "Write($HOME/.claude/verification/**)" not in _deny_rules()
203
217
 
204
218
 
205
219
  def test_settings_deny_verdict_directory_edit() -> None:
@@ -209,87 +223,28 @@ def test_settings_deny_verdict_directory_edit() -> None:
209
223
  def test_minter_refuses_when_attested_hash_equals_empty_surface_hash(
210
224
  tmp_path: pathlib.Path,
211
225
  ) -> None:
212
- repo_root = tmp_path / "repo"
213
- repo_root.mkdir()
226
+ repo_root = _make_repo(tmp_path)
214
227
  _init_repo_with_upstream_and_edit(repo_root)
215
228
  attested_empty = empty_surface_hash()
216
229
  verdict_fence = json.dumps(
217
230
  {"all_pass": True, "findings": [], "manifest_sha256": attested_empty}
218
231
  )
219
232
  agent_transcript = tmp_path / "agent-7.jsonl"
220
- agent_transcript.write_text(
221
- json.dumps(
222
- {
223
- "type": "assistant",
224
- "message": {
225
- "content": [
226
- {
227
- "type": "text",
228
- "text": f"ok\n```verdict\n{verdict_fence}\n```\n",
229
- }
230
- ]
231
- },
232
- }
233
- )
234
- + "\n",
235
- encoding="utf-8",
236
- )
233
+ _write_verdict_transcript(agent_transcript, verdict_fence)
237
234
  _write_sidecar(agent_transcript, MINTING_AGENT_TYPE)
238
- payload = {
239
- "agent_transcript_path": str(agent_transcript),
240
- "cwd": str(repo_root),
241
- "agent_id": "empty-surface-1",
242
- }
235
+ payload = _mint_payload(agent_transcript, repo_root, "empty-surface-1")
243
236
  assert mint_for_payload(payload) is None
244
237
 
245
238
 
246
239
  def test_minter_refuses_when_recomputed_surface_is_empty(
247
240
  tmp_path: pathlib.Path,
248
241
  ) -> None:
249
- repo_root = tmp_path / "repo"
250
- repo_root.mkdir()
251
- subprocess.run(["git", "-C", str(repo_root), "init", "-q"], check=True)
252
- subprocess.run(
253
- ["git", "-C", str(repo_root), "config", "user.email", "verifier@test"],
254
- check=True,
255
- )
256
- subprocess.run(
257
- ["git", "-C", str(repo_root), "config", "user.name", "verifier"],
258
- check=True,
259
- )
260
- (repo_root / "module.py").write_text("answer = 1\n", encoding="utf-8")
261
- subprocess.run(["git", "-C", str(repo_root), "add", "-A"], check=True)
262
- subprocess.run(
263
- ["git", "-C", str(repo_root), "commit", "-qm", "init"], check=True
264
- )
265
- subprocess.run(
266
- ["git", "-C", str(repo_root), "branch", "-f", "origin/main", "HEAD"],
267
- check=True,
268
- )
242
+ repo_root = _make_repo(tmp_path)
243
+ _init_repo_with_upstream(repo_root)
269
244
  agent_transcript = tmp_path / "agent-7.jsonl"
270
- agent_transcript.write_text(
271
- json.dumps(
272
- {
273
- "type": "assistant",
274
- "message": {
275
- "content": [
276
- {
277
- "type": "text",
278
- "text": 'ok\n```verdict\n{"all_pass": true, "findings": []}\n```\n',
279
- }
280
- ]
281
- },
282
- }
283
- )
284
- + "\n",
285
- encoding="utf-8",
286
- )
245
+ _write_verdict_transcript(agent_transcript, '{"all_pass": true, "findings": []}')
287
246
  _write_sidecar(agent_transcript, MINTING_AGENT_TYPE)
288
- payload = {
289
- "agent_transcript_path": str(agent_transcript),
290
- "cwd": str(repo_root),
291
- "agent_id": "empty-recompute-1",
292
- }
247
+ payload = _mint_payload(agent_transcript, repo_root, "empty-recompute-1")
293
248
  assert mint_for_payload(payload) is None
294
249
 
295
250
 
@@ -297,23 +252,7 @@ def test_resolves_none_when_sidecar_absent_even_with_verdict_fence(
297
252
  tmp_path: pathlib.Path,
298
253
  ) -> None:
299
254
  agent_transcript = tmp_path / "agent-7.jsonl"
300
- agent_transcript.write_text(
301
- json.dumps(
302
- {
303
- "type": "assistant",
304
- "message": {
305
- "content": [
306
- {
307
- "type": "text",
308
- "text": 'ok\n```verdict\n{"all_pass": true, "findings": []}\n```\n',
309
- }
310
- ]
311
- },
312
- }
313
- )
314
- + "\n",
315
- encoding="utf-8",
316
- )
255
+ _write_verdict_transcript(agent_transcript, '{"all_pass": true, "findings": []}')
317
256
  payload = {"agent_transcript_path": str(agent_transcript)}
318
257
  assert resolved_subagent_type(payload) is None
319
258
 
@@ -321,67 +260,25 @@ def test_resolves_none_when_sidecar_absent_even_with_verdict_fence(
321
260
  def test_does_not_mint_when_sidecar_absent_but_transcript_has_verdict(
322
261
  tmp_path: pathlib.Path,
323
262
  ) -> None:
324
- repo_root = tmp_path / "repo"
325
- repo_root.mkdir()
263
+ repo_root = _make_repo(tmp_path)
326
264
  _init_repo_with_upstream_and_edit(repo_root)
327
265
  agent_transcript = tmp_path / "agent-7.jsonl"
328
- agent_transcript.write_text(
329
- json.dumps(
330
- {
331
- "type": "assistant",
332
- "message": {
333
- "content": [
334
- {
335
- "type": "text",
336
- "text": 'ok\n```verdict\n{"all_pass": true, "findings": []}\n```\n',
337
- }
338
- ]
339
- },
340
- }
341
- )
342
- + "\n",
343
- encoding="utf-8",
344
- )
345
- payload = {
346
- "agent_transcript_path": str(agent_transcript),
347
- "cwd": str(repo_root),
348
- "agent_id": "no-sidecar-1",
349
- }
266
+ _write_verdict_transcript(agent_transcript, '{"all_pass": true, "findings": []}')
267
+ payload = _mint_payload(agent_transcript, repo_root, "no-sidecar-1")
350
268
  assert mint_for_payload(payload) is None
351
269
 
352
270
 
353
271
  def test_attested_manifest_hash_binds_over_cwd_surface(tmp_path: pathlib.Path) -> None:
354
- repo_root = tmp_path / "repo"
355
- repo_root.mkdir()
272
+ repo_root = _make_repo(tmp_path)
356
273
  _init_repo_with_upstream_and_edit(repo_root)
357
274
  attested_hash = "c" * 64
358
- agent_transcript = tmp_path / "agent-7.jsonl"
359
275
  verdict_fence = json.dumps(
360
276
  {"all_pass": True, "findings": [], "manifest_sha256": attested_hash}
361
277
  )
362
- agent_transcript.write_text(
363
- json.dumps(
364
- {
365
- "type": "assistant",
366
- "message": {
367
- "content": [
368
- {
369
- "type": "text",
370
- "text": f"ok\n```verdict\n{verdict_fence}\n```\n",
371
- }
372
- ]
373
- },
374
- }
375
- )
376
- + "\n",
377
- encoding="utf-8",
378
- )
278
+ agent_transcript = tmp_path / "agent-7.jsonl"
279
+ _write_verdict_transcript(agent_transcript, verdict_fence)
379
280
  _write_sidecar(agent_transcript, MINTING_AGENT_TYPE)
380
- payload = {
381
- "agent_transcript_path": str(agent_transcript),
382
- "cwd": str(repo_root),
383
- "agent_id": "attest-1",
384
- }
281
+ payload = _mint_payload(agent_transcript, repo_root, "attest-1")
385
282
  verdict_path = mint_for_payload(payload)
386
283
  try:
387
284
  assert verdict_path is not None
@@ -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
+ )
@@ -132,6 +132,7 @@ ALL_SOURCE_TEST_FILE_SUFFIXES: tuple[str, ...] = (
132
132
  )
133
133
  ALL_SAFE_EMAIL_DOMAINS: frozenset[str] = frozenset(
134
134
  {
135
+ "anthropic.com",
135
136
  "example.com",
136
137
  "example.org",
137
138
  "example.net",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "2.0.0",
3
+ "version": "2.0.2",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {
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`. Claude Code loads these as always-on behavioral constraints for every session. Each `.md` file covers one named rule; hook-enforced rules are also backed by a Python hook in `hooks/`.
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`; `powershell.exe`, `cmd /c`, and `bash -c` are blocked |
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
- **When this applies:** Before any Agent or Task tool invocation (Explore, implementation, research, or team subagents).
3
+ Before any Agent or Task tool spawn (Explore, implementation, research, or team subagents):
4
4
 
5
- <agent_spawn_protocol>
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
- ## Before spawning ANY agent no exceptions
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`.
@@ -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
- **Key principles (see CODE_RULES.md for complete reference):**
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
- **When this applies:** Any Write, Edit, or MultiEdit to a markdown (`.md`) file that carries an environment-variable summary table a markdown table whose rows pair a `` `VARIABLE` `` name with the `` `code/file.py` `` that reads it.
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
- ## Rule
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).