claude-dev-env 1.65.0 → 1.66.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/agents/plan-packet-validator.md +34 -0
  2. package/audit-rubrics/category_rubrics/category-n-test-name-scenario-verifier.md +6 -0
  3. package/commands/plan.md +6 -52
  4. package/hooks/blocking/code_rules_dead_module_constant.py +111 -24
  5. package/hooks/blocking/code_rules_enforcer.py +2 -0
  6. package/hooks/blocking/code_rules_test_assertions.py +123 -1
  7. package/hooks/blocking/open_questions_in_plans_blocker.py +8 -1
  8. package/hooks/blocking/test_code_rules_enforcer_dead_module_constant.py +88 -0
  9. package/hooks/blocking/test_code_rules_enforcer_split_test_assertions.py +90 -0
  10. package/hooks/blocking/test_open_questions_in_plans_blocker.py +43 -0
  11. package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -0
  12. package/hooks/hooks_constants/dead_module_constant_constants.py +1 -0
  13. package/hooks/hooks_constants/open_questions_in_plans_blocker_constants.py +4 -0
  14. package/hooks/hooks_constants/test_open_questions_in_plans_blocker_constants.py +13 -1
  15. package/package.json +1 -1
  16. package/skills/anthropic-plan/SKILL.md +46 -85
  17. package/skills/anthropic-plan/scripts/anthropic_plan_scripts_constants/__init__.py +0 -0
  18. package/skills/anthropic-plan/scripts/anthropic_plan_scripts_constants/validate_packet_constants.py +33 -0
  19. package/skills/anthropic-plan/scripts/test_validate_packet.py +405 -0
  20. package/skills/anthropic-plan/scripts/validate_packet.py +397 -0
  21. package/skills/anthropic-plan/templates/README.md +20 -0
  22. package/skills/anthropic-plan/templates/build-prompt.md +9 -0
  23. package/skills/anthropic-plan/templates/source-map.md +5 -0
  24. package/skills/anthropic-plan/test_skill_contract.py +53 -0
  25. package/skills/anthropic-plan/workflow/plan-packet.contract.test.mjs +79 -0
  26. package/skills/anthropic-plan/workflow/plan-packet.mjs +299 -0
  27. package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +8 -1
  28. package/skills/autoconverge/workflow/converge.mjs +9 -1
@@ -0,0 +1,405 @@
1
+ """Tests for the plan packet validator."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib.util
6
+ import json
7
+ import subprocess
8
+ import sys
9
+ from pathlib import Path
10
+ from types import ModuleType
11
+
12
+ import pytest
13
+
14
+
15
+ SCRIPTS_DIRECTORY = Path(__file__).resolve().parent
16
+ VALIDATOR_PATH = SCRIPTS_DIRECTORY / "validate_packet.py"
17
+
18
+
19
+ def load_validator_module() -> ModuleType:
20
+ if str(SCRIPTS_DIRECTORY) not in sys.path:
21
+ sys.path.insert(0, str(SCRIPTS_DIRECTORY))
22
+ spec = importlib.util.spec_from_file_location("validate_packet", VALIDATOR_PATH)
23
+ assert spec is not None
24
+ assert spec.loader is not None
25
+ validator_module = importlib.util.module_from_spec(spec)
26
+ spec.loader.exec_module(validator_module)
27
+ return validator_module
28
+
29
+
30
+ def write_valid_packet(packet_directory: Path) -> None:
31
+ all_relative_paths = load_validator_module().required_relative_paths()
32
+ for each_relative_path in all_relative_paths:
33
+ target_path = packet_directory / each_relative_path
34
+ target_path.parent.mkdir(parents=True, exist_ok=True)
35
+ target_path.write_text(valid_markdown_for(each_relative_path), encoding="utf-8")
36
+
37
+ packet_payload = {
38
+ "schemaVersion": 1,
39
+ "slug": "add-login",
40
+ "repoRoot": str(packet_directory.parent.parent.parent),
41
+ "packetPath": str(packet_directory),
42
+ "sourceFiles": ["src/auth.py"],
43
+ "assumptions": ["No migration is needed."],
44
+ "validator": {"deterministic": "pending", "semantic": "pending"},
45
+ }
46
+ (packet_directory / "packet.json").write_text(
47
+ json.dumps(packet_payload, indent=2),
48
+ encoding="utf-8",
49
+ )
50
+
51
+
52
+ def valid_markdown_for(relative_path: str) -> str:
53
+ if relative_path == "context/source-map.md":
54
+ return (
55
+ "# Source Map\n\n"
56
+ "| Source | Why it matters | Facts extracted | Plan implication |\n"
57
+ "|---|---|---|---|\n"
58
+ "| src/auth.py | Login flow entrypoint | Existing authenticate_user function handles password checks. | Reuse authenticate_user in the implementation. |\n"
59
+ )
60
+ if relative_path == "implementation/tdd-plan.md":
61
+ return (
62
+ "# TDD Plan\n\n"
63
+ "1. Failing test: add test_auth_login_success before production edits.\n"
64
+ "2. Production code: wire the existing authenticate_user call.\n"
65
+ "3. Refactor after green: remove duplicated setup.\n"
66
+ )
67
+ if relative_path == "implementation/steps.md":
68
+ return (
69
+ "# Steps\n\n"
70
+ "1. Test first: add login success coverage.\n"
71
+ "2. Production change: add the route handler; covered by test_auth_login_success.\n"
72
+ )
73
+ if relative_path == "handoff/build-prompt.md":
74
+ return (
75
+ "# Build Prompt\n\n"
76
+ "Use only this packet. Read README.md, then context/source-map.md, then implementation/steps.md. "
77
+ "Do not rely on prior chat history."
78
+ )
79
+ return f"# {relative_path}\n\nGrounded implementation detail for this packet file.\n"
80
+
81
+
82
+ def run_validator(packet_directory: Path) -> subprocess.CompletedProcess[str]:
83
+ return subprocess.run(
84
+ [sys.executable, str(VALIDATOR_PATH), str(packet_directory)],
85
+ capture_output=True,
86
+ text=True,
87
+ check=False,
88
+ )
89
+
90
+
91
+ def test_valid_packet_passes(tmp_path: Path) -> None:
92
+ packet_directory = tmp_path / "docs" / "plans" / "add-login"
93
+ write_valid_packet(packet_directory)
94
+
95
+ validator_run = run_validator(packet_directory)
96
+
97
+ assert validator_run.returncode == 0, validator_run.stderr
98
+ assert "packet validation passed" in validator_run.stdout
99
+
100
+
101
+ def test_missing_required_file_fails(tmp_path: Path) -> None:
102
+ packet_directory = tmp_path / "docs" / "plans" / "add-login"
103
+ write_valid_packet(packet_directory)
104
+ (packet_directory / "spec" / "behavior.md").unlink()
105
+
106
+ validator_run = run_validator(packet_directory)
107
+
108
+ assert validator_run.returncode == 2
109
+ assert "missing required file: spec/behavior.md" in validator_run.stderr
110
+
111
+
112
+ def test_placeholder_text_fails(tmp_path: Path) -> None:
113
+ packet_directory = tmp_path / "docs" / "plans" / "add-login"
114
+ write_valid_packet(packet_directory)
115
+ (packet_directory / "spec" / "scope.md").write_text("TODO: fill this in", encoding="utf-8")
116
+
117
+ validator_run = run_validator(packet_directory)
118
+
119
+ assert validator_run.returncode == 2
120
+ assert "placeholder text" in validator_run.stderr
121
+
122
+
123
+ @pytest.mark.parametrize(
124
+ "template_placeholder_markdown",
125
+ [
126
+ "# <Plan Title>\n\nThis plan implements the feature.",
127
+ "This plan implements <feature name> for the <component> module.",
128
+ "| <path> | <reason> | <verified fact> | <implementation implication> |",
129
+ ],
130
+ )
131
+ def test_angle_bracket_placeholder_text_fails(
132
+ tmp_path: Path,
133
+ template_placeholder_markdown: str,
134
+ ) -> None:
135
+ packet_directory = tmp_path / "docs" / "plans" / "add-login"
136
+ write_valid_packet(packet_directory)
137
+ (packet_directory / "spec" / "scope.md").write_text(
138
+ template_placeholder_markdown,
139
+ encoding="utf-8",
140
+ )
141
+
142
+ validator_run = run_validator(packet_directory)
143
+
144
+ assert validator_run.returncode == 2
145
+ assert "spec/scope.md contains placeholder text" in validator_run.stderr
146
+
147
+
148
+ @pytest.mark.parametrize(
149
+ "non_placeholder_markdown",
150
+ [
151
+ "<details>\n<summary>Notes</summary>\nGrounded detail.\n</details>",
152
+ "Type the annotation as `list[str]` and call `<command>` from a code span.",
153
+ "Reach the endpoint with `curl <url>` inside the inline code span.",
154
+ "The comparison `attempt_count < threshold` must hold before the retry.",
155
+ "The handler accepts a List<Item> of records.",
156
+ "Wrap the field in Optional<User> when it may be absent.",
157
+ "The cache stores an Array<string> of identifiers.",
158
+ "The lookup uses a Map<Key, Value> keyed by request id.",
159
+ ],
160
+ )
161
+ def test_inline_html_and_code_does_not_flag_placeholder(
162
+ tmp_path: Path,
163
+ non_placeholder_markdown: str,
164
+ ) -> None:
165
+ packet_directory = tmp_path / "docs" / "plans" / "add-login"
166
+ write_valid_packet(packet_directory)
167
+ (packet_directory / "spec" / "scope.md").write_text(
168
+ non_placeholder_markdown,
169
+ encoding="utf-8",
170
+ )
171
+
172
+ validator_run = run_validator(packet_directory)
173
+
174
+ assert validator_run.returncode == 0, validator_run.stderr
175
+
176
+
177
+ def test_open_questions_heading_fails(tmp_path: Path) -> None:
178
+ packet_directory = tmp_path / "docs" / "plans" / "add-login"
179
+ write_valid_packet(packet_directory)
180
+ (packet_directory / "validation" / "unresolved-risks.md").write_text(
181
+ "## Open Questions\n- Which database?",
182
+ encoding="utf-8",
183
+ )
184
+
185
+ validator_run = run_validator(packet_directory)
186
+
187
+ assert validator_run.returncode == 2
188
+ assert "Open Questions" in validator_run.stderr
189
+
190
+
191
+ def test_weak_source_map_fails(tmp_path: Path) -> None:
192
+ packet_directory = tmp_path / "docs" / "plans" / "add-login"
193
+ write_valid_packet(packet_directory)
194
+ (packet_directory / "context" / "source-map.md").write_text(
195
+ "# Source Map\n\nNo sources yet.\n",
196
+ encoding="utf-8",
197
+ )
198
+
199
+ validator_run = run_validator(packet_directory)
200
+
201
+ assert validator_run.returncode == 2
202
+ assert "source-map.md must include source-grounded rows" in validator_run.stderr
203
+
204
+
205
+ def test_missing_tdd_plan_fails(tmp_path: Path) -> None:
206
+ packet_directory = tmp_path / "docs" / "plans" / "add-login"
207
+ write_valid_packet(packet_directory)
208
+ (packet_directory / "implementation" / "tdd-plan.md").write_text(
209
+ "# TDD Plan\n\nImplementation can start directly.",
210
+ encoding="utf-8",
211
+ )
212
+
213
+ validator_run = run_validator(packet_directory)
214
+
215
+ assert validator_run.returncode == 2
216
+ assert "tdd-plan.md must name failing tests" in validator_run.stderr
217
+
218
+
219
+ @pytest.mark.parametrize(
220
+ "forbidden_phrase",
221
+ ["as discussed above", "from our chat", "previous conversation", "earlier in this thread"],
222
+ )
223
+ def test_handoff_prompt_depending_on_chat_history_fails(
224
+ tmp_path: Path,
225
+ forbidden_phrase: str,
226
+ ) -> None:
227
+ packet_directory = tmp_path / "docs" / "plans" / "add-login"
228
+ write_valid_packet(packet_directory)
229
+ (packet_directory / "handoff" / "build-prompt.md").write_text(
230
+ f"# Build Prompt\n\nUse the details {forbidden_phrase}.",
231
+ encoding="utf-8",
232
+ )
233
+
234
+ validator_run = run_validator(packet_directory)
235
+
236
+ assert validator_run.returncode == 2
237
+ assert "build-prompt.md must stand alone" in validator_run.stderr
238
+
239
+
240
+ def test_tdd_plan_with_red_only_as_substring_fails(tmp_path: Path) -> None:
241
+ packet_directory = tmp_path / "docs" / "plans" / "add-login"
242
+ write_valid_packet(packet_directory)
243
+ (packet_directory / "implementation" / "tdd-plan.md").write_text(
244
+ "# TDD Plan\n\nImplementation is required. Wire production code directly.",
245
+ encoding="utf-8",
246
+ )
247
+
248
+ validator_run = run_validator(packet_directory)
249
+
250
+ assert validator_run.returncode == 2
251
+ assert "tdd-plan.md must name failing tests" in validator_run.stderr
252
+
253
+
254
+ def test_tdd_plan_naming_red_step_passes(tmp_path: Path) -> None:
255
+ packet_directory = tmp_path / "docs" / "plans" / "add-login"
256
+ write_valid_packet(packet_directory)
257
+ (packet_directory / "implementation" / "tdd-plan.md").write_text(
258
+ "# TDD Plan\n\n1. Red step: add the failing coverage.\n2. Green production code follows.",
259
+ encoding="utf-8",
260
+ )
261
+
262
+ validator_run = run_validator(packet_directory)
263
+
264
+ assert validator_run.returncode == 0, validator_run.stderr
265
+
266
+
267
+ def test_packet_path_with_forward_slashes_matches_native_directory(tmp_path: Path) -> None:
268
+ packet_directory = tmp_path / "docs" / "plans" / "add-login"
269
+ write_valid_packet(packet_directory)
270
+ packet_file = packet_directory / "packet.json"
271
+ packet_payload = json.loads(packet_file.read_text(encoding="utf-8"))
272
+ packet_payload["packetPath"] = packet_directory.as_posix()
273
+ packet_file.write_text(json.dumps(packet_payload, indent=2), encoding="utf-8")
274
+
275
+ validator_run = run_validator(packet_directory)
276
+
277
+ assert validator_run.returncode == 0, validator_run.stderr
278
+
279
+
280
+ def test_packet_path_with_trailing_separator_matches_directory(tmp_path: Path) -> None:
281
+ packet_directory = tmp_path / "docs" / "plans" / "add-login"
282
+ write_valid_packet(packet_directory)
283
+ packet_file = packet_directory / "packet.json"
284
+ packet_payload = json.loads(packet_file.read_text(encoding="utf-8"))
285
+ packet_payload["packetPath"] = str(packet_directory) + "/"
286
+ packet_file.write_text(json.dumps(packet_payload, indent=2), encoding="utf-8")
287
+
288
+ validator_run = run_validator(packet_directory)
289
+
290
+ assert validator_run.returncode == 0, validator_run.stderr
291
+
292
+
293
+ def test_packet_path_mismatch_still_fails(tmp_path: Path) -> None:
294
+ packet_directory = tmp_path / "docs" / "plans" / "add-login"
295
+ write_valid_packet(packet_directory)
296
+ packet_file = packet_directory / "packet.json"
297
+ packet_payload = json.loads(packet_file.read_text(encoding="utf-8"))
298
+ packet_payload["packetPath"] = str(packet_directory.parent / "different-slug")
299
+ packet_file.write_text(json.dumps(packet_payload, indent=2), encoding="utf-8")
300
+
301
+ validator_run = run_validator(packet_directory)
302
+
303
+ assert validator_run.returncode == 2
304
+ assert "packet.json packetPath must match the validated packet directory" in validator_run.stderr
305
+
306
+
307
+ def test_source_map_with_bare_non_python_source_passes(tmp_path: Path) -> None:
308
+ packet_directory = tmp_path / "docs" / "plans" / "add-login"
309
+ write_valid_packet(packet_directory)
310
+ (packet_directory / "context" / "source-map.md").write_text(
311
+ "# Source Map\n\n"
312
+ "| Source | Why it matters | Facts extracted | Plan implication |\n"
313
+ "|---|---|---|---|\n"
314
+ "| plan-packet.mjs | Workflow entry | Exports the run function. | Reuse the run shape. |\n",
315
+ encoding="utf-8",
316
+ )
317
+
318
+ validator_run = run_validator(packet_directory)
319
+
320
+ assert validator_run.returncode == 0, validator_run.stderr
321
+
322
+
323
+ def test_source_map_with_only_version_number_row_fails(tmp_path: Path) -> None:
324
+ packet_directory = tmp_path / "docs" / "plans" / "add-login"
325
+ write_valid_packet(packet_directory)
326
+ (packet_directory / "context" / "source-map.md").write_text(
327
+ "# Source Map\n\n"
328
+ "| Source | Why it matters | Facts extracted | Plan implication |\n"
329
+ "|---|---|---|---|\n"
330
+ "| The login subsystem | We reviewed version 2.0 of the design | No file path named here | Build accordingly |\n",
331
+ encoding="utf-8",
332
+ )
333
+
334
+ validator_run = run_validator(packet_directory)
335
+
336
+ assert validator_run.returncode == 2
337
+ assert "source-map.md must include source-grounded rows" in validator_run.stderr
338
+
339
+
340
+ def test_source_map_data_row_naming_source_and_facts_passes(tmp_path: Path) -> None:
341
+ packet_directory = tmp_path / "docs" / "plans" / "add-login"
342
+ write_valid_packet(packet_directory)
343
+ (packet_directory / "context" / "source-map.md").write_text(
344
+ "# Source Map\n\n"
345
+ "| Source | Why it matters | Facts extracted | Plan implication |\n"
346
+ "|---|---|---|---|\n"
347
+ "| src/auth.py | The source of truth for login | Key facts about hashing | Reuse it |\n",
348
+ encoding="utf-8",
349
+ )
350
+
351
+ validator_run = run_validator(packet_directory)
352
+
353
+ assert validator_run.returncode == 0, validator_run.stderr
354
+
355
+
356
+ def test_has_source_table_row_keeps_grounded_row_with_source_and_facts_prose() -> None:
357
+ validator_module = load_validator_module()
358
+ source_map_text = (
359
+ "| Source | Why it matters | Facts extracted | Plan implication |\n"
360
+ "|---|---|---|---|\n"
361
+ "| src/auth.py | The source of truth for login | Key facts about hashing | Reuse it |\n"
362
+ )
363
+
364
+ assert validator_module.has_source_table_row(source_map_text) is True
365
+
366
+
367
+ def test_has_source_table_row_skips_header_only_document() -> None:
368
+ validator_module = load_validator_module()
369
+ source_map_text = (
370
+ "| Source | Why it matters | Facts extracted | Plan implication |\n"
371
+ "|---|---|---|---|\n"
372
+ )
373
+
374
+ assert validator_module.has_source_table_row(source_map_text) is False
375
+
376
+
377
+ def test_bullet_list_steps_without_test_contract_fails(tmp_path: Path) -> None:
378
+ packet_directory = tmp_path / "docs" / "plans" / "add-login"
379
+ write_valid_packet(packet_directory)
380
+ (packet_directory / "implementation" / "steps.md").write_text(
381
+ "# Steps\n\n"
382
+ "- Add the route handler\n"
383
+ "- Wire the database call\n",
384
+ encoding="utf-8",
385
+ )
386
+
387
+ validator_run = run_validator(packet_directory)
388
+
389
+ assert validator_run.returncode == 2
390
+ assert "implementation/steps.md has steps without a test or non-code reason" in validator_run.stderr
391
+
392
+
393
+ def test_bullet_list_steps_naming_test_contract_passes(tmp_path: Path) -> None:
394
+ packet_directory = tmp_path / "docs" / "plans" / "add-login"
395
+ write_valid_packet(packet_directory)
396
+ (packet_directory / "implementation" / "steps.md").write_text(
397
+ "# Steps\n\n"
398
+ "- Test first: add login success coverage.\n"
399
+ "- Production change: add the route handler; covered by test_auth_login_success.\n",
400
+ encoding="utf-8",
401
+ )
402
+
403
+ validator_run = run_validator(packet_directory)
404
+
405
+ assert validator_run.returncode == 0, validator_run.stderr