claude-dev-env 1.19.3 → 1.20.1
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/CLAUDE.md +16 -0
- package/bin/install.mjs +34 -1
- package/docs/BDD_DISCOVERY_PROTOCOL.md +53 -0
- package/docs/BDD_SCENARIO_QUALITY.md +89 -0
- package/docs/BDD_TEST_LAYOUT.md +71 -0
- package/docs/CODE_RULES.md +1 -208
- package/hooks/blocking/tdd-enforcer.py +3 -3
- package/package.json +5 -2
- package/rules/agent-spawn-protocol.md +1 -47
- package/rules/bdd.md +28 -0
- package/rules/cleanup-temp-files.md +1 -27
- package/rules/code-reviews.md +1 -11
- package/rules/code-standards.md +1 -43
- package/rules/conservative-action.md +1 -20
- package/rules/context7.md +1 -12
- package/rules/explore-thoroughly.md +1 -27
- package/rules/git-workflow.md +1 -42
- package/rules/parallel-tools.md +1 -23
- package/rules/research-mode.md +1 -23
- package/rules/right-sized-engineering.md +1 -28
- package/rules/self-contained-docs.md +1 -0
- package/rules/vault-context.md +1 -0
- package/rules/verify-before-asking.md +1 -0
- package/scripts/sync-to-cursor.py +22 -0
- package/scripts/sync_to_cursor/__init__.py +13 -0
- package/scripts/sync_to_cursor/canonical_docs.py +66 -0
- package/scripts/sync_to_cursor/config.py +5 -0
- package/scripts/sync_to_cursor/engine.py +194 -0
- package/scripts/sync_to_cursor/hashing.py +7 -0
- package/scripts/sync_to_cursor/paths.py +18 -0
- package/scripts/sync_to_cursor/rules.py +321 -0
- package/scripts/tests/test_sync_to_cursor.py +255 -0
- package/skills/bdd-protocol/SKILL.md +31 -0
- package/skills/bdd-protocol/references/anti-patterns.md +26 -0
- package/skills/bdd-protocol/references/example-mapping.md +23 -0
- package/skills/npm-creator/SKILL.md +3 -3
- package/skills/rule-audit/SKILL.md +2 -2
- package/system-prompts/software-engineer.xml +387 -0
- package/rules/tdd.md +0 -7
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
"""Build Cursor .mdc bodies from Claude rules and docs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Literal
|
|
9
|
+
|
|
10
|
+
from sync_to_cursor.config import MAX_RULE_BODY_LINES
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _parse_h2_sections(markdown: str) -> dict[str, str]:
|
|
14
|
+
parts = re.split(r"^## ", markdown, flags=re.MULTILINE)
|
|
15
|
+
sections: dict[str, str] = {}
|
|
16
|
+
for part in parts[1:]:
|
|
17
|
+
title_line, _, body = part.partition("\n")
|
|
18
|
+
sections[title_line.strip()] = body.strip()
|
|
19
|
+
return sections
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _filter_core_principles(body: str) -> str:
|
|
23
|
+
lines = []
|
|
24
|
+
for line in body.splitlines():
|
|
25
|
+
if "readability-review" in line or "readability standard" in line:
|
|
26
|
+
continue
|
|
27
|
+
lines.append(line)
|
|
28
|
+
return "\n".join(lines).strip()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _limit_lines(text: str, max_lines: int) -> str:
|
|
32
|
+
lines = text.splitlines()
|
|
33
|
+
if len(lines) <= max_lines:
|
|
34
|
+
return text
|
|
35
|
+
return (
|
|
36
|
+
"\n".join(lines[:max_lines])
|
|
37
|
+
+ "\n\n_(Truncated for Cursor rule length; see the synced reference in `.cursor/docs/` "
|
|
38
|
+
"(`CODE_RULES.md` or `TEST_QUALITY.md` as applicable) and follow its canonical source "
|
|
39
|
+
"under `~/.claude/system-prompts/software-engineer.xml` when needed.)_"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _strip_code_standards_blockquote(markdown: str) -> str:
|
|
44
|
+
lines = markdown.splitlines()
|
|
45
|
+
out: list[str] = []
|
|
46
|
+
i = 0
|
|
47
|
+
while i < len(lines):
|
|
48
|
+
if lines[i].startswith(">") and "MANDATORY REFERENCE" in lines[i]:
|
|
49
|
+
while i < len(lines) and lines[i].startswith(">"):
|
|
50
|
+
i += 1
|
|
51
|
+
while i < len(lines) and lines[i].strip() == "":
|
|
52
|
+
i += 1
|
|
53
|
+
continue
|
|
54
|
+
out.append(lines[i])
|
|
55
|
+
i += 1
|
|
56
|
+
return "\n".join(out).strip()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def merge_code_standards(sources: tuple[Path, ...]) -> str:
|
|
60
|
+
code_standards_markdown = _strip_code_standards_blockquote(
|
|
61
|
+
sources[0].read_text(encoding="utf-8")
|
|
62
|
+
)
|
|
63
|
+
code_standards_markdown = "\n".join(
|
|
64
|
+
line for line in code_standards_markdown.splitlines() if not line.strip().startswith("- TDD ")
|
|
65
|
+
)
|
|
66
|
+
code_rules_markdown = sources[1].read_text(encoding="utf-8")
|
|
67
|
+
sections_by_heading = _parse_h2_sections(code_rules_markdown)
|
|
68
|
+
if not sections_by_heading:
|
|
69
|
+
pointer_fallback_note = (
|
|
70
|
+
"_(Full code-quality rules: `~/.claude/system-prompts/software-engineer.xml`"
|
|
71
|
+
" under `<code_quality>`.)_"
|
|
72
|
+
)
|
|
73
|
+
chunks = [code_standards_markdown, "", pointer_fallback_note]
|
|
74
|
+
return _limit_lines("\n\n".join(chunks), MAX_RULE_BODY_LINES)
|
|
75
|
+
include_order = [
|
|
76
|
+
"COMMENT PRESERVATION (ABSOLUTE RULE)",
|
|
77
|
+
"CORE PRINCIPLES",
|
|
78
|
+
"⚡ HOOK-ENFORCED RULES",
|
|
79
|
+
"4. CONFIG LOCATIONS",
|
|
80
|
+
"5. NO ABBREVIATIONS",
|
|
81
|
+
"6. COMPLETE TYPE HINTS",
|
|
82
|
+
"9. SELF-CONTAINED COMPONENTS",
|
|
83
|
+
]
|
|
84
|
+
chunks = [
|
|
85
|
+
code_standards_markdown,
|
|
86
|
+
"",
|
|
87
|
+
"## Reference (full text: `.cursor/docs/CODE_RULES.md`)",
|
|
88
|
+
]
|
|
89
|
+
for title in include_order:
|
|
90
|
+
body = sections_by_heading.get(title, "")
|
|
91
|
+
if title == "CORE PRINCIPLES":
|
|
92
|
+
body = _filter_core_principles(body)
|
|
93
|
+
if body:
|
|
94
|
+
chunks.append(f"## {title}\n\n{body}")
|
|
95
|
+
merged = "\n\n".join(chunks)
|
|
96
|
+
return _limit_lines(merged, MAX_RULE_BODY_LINES)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def merge_test_quality(sources: tuple[Path, ...]) -> str:
|
|
100
|
+
testing = sources[0].read_text(encoding="utf-8").strip()
|
|
101
|
+
test_quality_markdown = sources[1].read_text(encoding="utf-8")
|
|
102
|
+
sections_by_heading = _parse_h2_sections(test_quality_markdown)
|
|
103
|
+
include_order = [
|
|
104
|
+
"Delete Useless Tests",
|
|
105
|
+
"Test Dependencies MUST FAIL",
|
|
106
|
+
"Core Testing Principles",
|
|
107
|
+
"React Testing Patterns",
|
|
108
|
+
"Test File Organization",
|
|
109
|
+
]
|
|
110
|
+
chunks = [testing, "", "## Reference (full text: `.cursor/docs/TEST_QUALITY.md`)"]
|
|
111
|
+
for title in include_order:
|
|
112
|
+
body = sections_by_heading.get(title, "")
|
|
113
|
+
if body:
|
|
114
|
+
chunks.append(f"## {title}\n\n{body}")
|
|
115
|
+
merged = "\n\n".join(chunks)
|
|
116
|
+
return _limit_lines(merged, MAX_RULE_BODY_LINES)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def strip_anthropic_refs(text: str) -> str:
|
|
120
|
+
out_lines: list[str] = []
|
|
121
|
+
for line in text.splitlines():
|
|
122
|
+
s = line.strip()
|
|
123
|
+
if s.startswith("Source:") and (
|
|
124
|
+
"anthropic" in s.lower()
|
|
125
|
+
or "claude.com" in s.lower()
|
|
126
|
+
or "docs.anthropic" in s.lower()
|
|
127
|
+
):
|
|
128
|
+
continue
|
|
129
|
+
if "docs.anthropic.com" in line:
|
|
130
|
+
continue
|
|
131
|
+
out_lines.append(line)
|
|
132
|
+
text = "\n".join(out_lines)
|
|
133
|
+
text = re.sub(
|
|
134
|
+
r"<do_not_act_before_instructions>\s*",
|
|
135
|
+
"",
|
|
136
|
+
text,
|
|
137
|
+
flags=re.DOTALL,
|
|
138
|
+
)
|
|
139
|
+
text = re.sub(r"\s*</do_not_act_before_instructions>", "", text)
|
|
140
|
+
return text.strip()
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def verbatim(text: str) -> str:
|
|
144
|
+
return text.strip()
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def near_verbatim(text: str) -> str:
|
|
148
|
+
return strip_anthropic_refs(text)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def strip_leading_yaml_frontmatter(text: str) -> str:
|
|
152
|
+
"""Remove leading `---` ... `---` block (e.g. Claude `paths:`) so Cursor `.mdc` uses its own frontmatter."""
|
|
153
|
+
lines = text.splitlines()
|
|
154
|
+
if not lines or lines[0].strip() != "---":
|
|
155
|
+
return text
|
|
156
|
+
for i in range(1, len(lines)):
|
|
157
|
+
if lines[i].strip() == "---":
|
|
158
|
+
return "\n".join(lines[i + 1 :]).lstrip("\n")
|
|
159
|
+
return text
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
TransformName = Literal["verbatim", "near_verbatim", "merge_code_standards", "merge_test_quality"]
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def apply_transform(
|
|
166
|
+
name: TransformName,
|
|
167
|
+
sources: tuple[Path, ...],
|
|
168
|
+
*,
|
|
169
|
+
strip_leading_frontmatter: bool = False,
|
|
170
|
+
) -> str:
|
|
171
|
+
if name == "merge_code_standards":
|
|
172
|
+
return merge_code_standards(sources)
|
|
173
|
+
if name == "merge_test_quality":
|
|
174
|
+
return merge_test_quality(sources)
|
|
175
|
+
raw = "\n\n".join(p.read_text(encoding="utf-8") for p in sources)
|
|
176
|
+
if strip_leading_frontmatter:
|
|
177
|
+
raw = strip_leading_yaml_frontmatter(raw)
|
|
178
|
+
if name == "verbatim":
|
|
179
|
+
return verbatim(raw)
|
|
180
|
+
if name == "near_verbatim":
|
|
181
|
+
return near_verbatim(raw)
|
|
182
|
+
raise AssertionError(name)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@dataclass(frozen=True)
|
|
186
|
+
class RuleMapping:
|
|
187
|
+
key: str
|
|
188
|
+
sources: tuple[Path, ...]
|
|
189
|
+
output_name: str
|
|
190
|
+
always_apply: bool
|
|
191
|
+
globs: str | None
|
|
192
|
+
description: str
|
|
193
|
+
transform: TransformName
|
|
194
|
+
strip_leading_frontmatter: bool = False
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _frontmatter(description: str, always_apply: bool, globs: str | None) -> str:
|
|
198
|
+
escaped_description = description.replace('"', '\\"')
|
|
199
|
+
lines = ["---", f'description: "{escaped_description}"']
|
|
200
|
+
if globs:
|
|
201
|
+
escaped_globs = globs.replace('"', '\\"')
|
|
202
|
+
lines.append(f'globs: "{escaped_globs}"')
|
|
203
|
+
lines.append(f"alwaysApply: {'true' if always_apply else 'false'}")
|
|
204
|
+
lines.append("---")
|
|
205
|
+
return "\n".join(lines) + "\n"
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _full_mdc(mapping: RuleMapping, body: str) -> str:
|
|
209
|
+
generated_header = (
|
|
210
|
+
"<!-- Generated by sync-to-cursor.py — do not edit directly -->\n"
|
|
211
|
+
"<!-- Re-run: python ~/.claude/scripts/sync-to-cursor.py -->\n"
|
|
212
|
+
"<!-- Output: .cursor/rules/*.mdc, .cursor/docs/*.md"
|
|
213
|
+
" (see LLM_SETTINGS_ROOT in script docstring) -->\n"
|
|
214
|
+
)
|
|
215
|
+
return _frontmatter(mapping.description, mapping.always_apply, mapping.globs) + "\n" + generated_header + "\n" + body + "\n"
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _read_paths_glob(rule_file: Path) -> str | None:
|
|
219
|
+
"""Read `paths:` list from a Claude rule's YAML frontmatter; return as comma-separated Cursor glob string."""
|
|
220
|
+
if not rule_file.is_file():
|
|
221
|
+
return None
|
|
222
|
+
lines = rule_file.read_text(encoding="utf-8").splitlines()
|
|
223
|
+
if not lines or lines[0].strip() != "---":
|
|
224
|
+
return None
|
|
225
|
+
is_in_paths = False
|
|
226
|
+
all_paths: list[str] = []
|
|
227
|
+
for line in lines[1:]:
|
|
228
|
+
if line.strip() == "---":
|
|
229
|
+
break
|
|
230
|
+
if line.startswith("paths:"):
|
|
231
|
+
is_in_paths = True
|
|
232
|
+
continue
|
|
233
|
+
if is_in_paths:
|
|
234
|
+
if line.startswith(" ") or line.startswith("\t"):
|
|
235
|
+
path_value = line.strip().lstrip("-").strip().strip('"').strip("'")
|
|
236
|
+
if path_value:
|
|
237
|
+
all_paths.append(path_value)
|
|
238
|
+
else:
|
|
239
|
+
is_in_paths = False
|
|
240
|
+
return ",".join(all_paths) if all_paths else None
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def build_mappings(claude: Path) -> tuple[RuleMapping, ...]:
|
|
244
|
+
rules_directory = claude / "rules"
|
|
245
|
+
docs_directory = claude / "docs"
|
|
246
|
+
test_globs = "**/*.test.*,**/*.spec.*,**/test_*,**/*_test.*"
|
|
247
|
+
return (
|
|
248
|
+
RuleMapping(
|
|
249
|
+
"code-standards",
|
|
250
|
+
(rules_directory / "code-standards.md", docs_directory / "CODE_RULES.md"),
|
|
251
|
+
"code-standards.mdc",
|
|
252
|
+
True,
|
|
253
|
+
None,
|
|
254
|
+
"Core code standards: naming, types, config, hook-enforced rules",
|
|
255
|
+
"merge_code_standards",
|
|
256
|
+
),
|
|
257
|
+
RuleMapping(
|
|
258
|
+
"tasklings-preferences",
|
|
259
|
+
(rules_directory / "tasklings-preferences.md",),
|
|
260
|
+
"tasklings-preferences.mdc",
|
|
261
|
+
False,
|
|
262
|
+
_read_paths_glob(rules_directory / "tasklings-preferences.md"),
|
|
263
|
+
"Tasklings: Prefer / Do / Always engineering preferences (scoped path)",
|
|
264
|
+
"verbatim",
|
|
265
|
+
True,
|
|
266
|
+
),
|
|
267
|
+
RuleMapping(
|
|
268
|
+
"right-sized-engineering",
|
|
269
|
+
(rules_directory / "right-sized-engineering.md",),
|
|
270
|
+
"right-sized-engineering.mdc",
|
|
271
|
+
True,
|
|
272
|
+
None,
|
|
273
|
+
"Right-sized engineering and complexity budget",
|
|
274
|
+
"verbatim",
|
|
275
|
+
),
|
|
276
|
+
RuleMapping(
|
|
277
|
+
"bdd",
|
|
278
|
+
(rules_directory / "bdd.md",),
|
|
279
|
+
"bdd.mdc",
|
|
280
|
+
True,
|
|
281
|
+
None,
|
|
282
|
+
"BDD: discovery-driven protocol (outside-in; Illustrate→Formulate→Automate)",
|
|
283
|
+
"verbatim",
|
|
284
|
+
),
|
|
285
|
+
RuleMapping(
|
|
286
|
+
"test-quality",
|
|
287
|
+
(rules_directory / "testing.md", docs_directory / "TEST_QUALITY.md"),
|
|
288
|
+
"test-quality.mdc",
|
|
289
|
+
False,
|
|
290
|
+
test_globs,
|
|
291
|
+
"Testing quality for test files",
|
|
292
|
+
"merge_test_quality",
|
|
293
|
+
),
|
|
294
|
+
RuleMapping(
|
|
295
|
+
"research-mode",
|
|
296
|
+
(rules_directory / "research-mode.md",),
|
|
297
|
+
"research-mode.mdc",
|
|
298
|
+
True,
|
|
299
|
+
None,
|
|
300
|
+
"Research mode: citations and I don't know",
|
|
301
|
+
"near_verbatim",
|
|
302
|
+
),
|
|
303
|
+
RuleMapping(
|
|
304
|
+
"conservative-action",
|
|
305
|
+
(rules_directory / "conservative-action.md",),
|
|
306
|
+
"conservative-action.mdc",
|
|
307
|
+
True,
|
|
308
|
+
None,
|
|
309
|
+
"Prefer research over action when intent is unclear",
|
|
310
|
+
"near_verbatim",
|
|
311
|
+
),
|
|
312
|
+
RuleMapping(
|
|
313
|
+
"explore-thoroughly",
|
|
314
|
+
(rules_directory / "explore-thoroughly.md",),
|
|
315
|
+
"explore-thoroughly.mdc",
|
|
316
|
+
True,
|
|
317
|
+
None,
|
|
318
|
+
"Explore codebase before committing to an approach",
|
|
319
|
+
"near_verbatim",
|
|
320
|
+
),
|
|
321
|
+
)
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
"""Tests for sync-to-cursor.py: canonical docs copy, manifest, and truncation footer."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
_SCRIPTS_DIR = Path(__file__).resolve().parent.parent
|
|
12
|
+
if str(_SCRIPTS_DIR) not in sys.path:
|
|
13
|
+
sys.path.insert(0, str(_SCRIPTS_DIR))
|
|
14
|
+
|
|
15
|
+
import sync_to_cursor as mod
|
|
16
|
+
from sync_to_cursor.rules import _read_paths_glob
|
|
17
|
+
|
|
18
|
+
_SYNC_SCRIPT = _SCRIPTS_DIR / "sync-to-cursor.py"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _minimal_rule_files(claude_rules: Path) -> None:
|
|
22
|
+
claude_rules.mkdir(parents=True, exist_ok=True)
|
|
23
|
+
(claude_rules / "code-standards.md").write_text(
|
|
24
|
+
"# Code standards stub\n", encoding="utf-8"
|
|
25
|
+
)
|
|
26
|
+
(claude_rules / "tasklings-preferences.md").write_text(
|
|
27
|
+
"---\npaths:\n - \"Y:/x/**\"\n---\n\n# Tasklings\n",
|
|
28
|
+
encoding="utf-8",
|
|
29
|
+
)
|
|
30
|
+
(claude_rules / "right-sized-engineering.md").write_text("# RSE\n", encoding="utf-8")
|
|
31
|
+
(claude_rules / "bdd.md").write_text("# BDD\n", encoding="utf-8")
|
|
32
|
+
(claude_rules / "testing.md").write_text("# Testing\n", encoding="utf-8")
|
|
33
|
+
(claude_rules / "research-mode.md").write_text("# RM\n", encoding="utf-8")
|
|
34
|
+
(claude_rules / "conservative-action.md").write_text("# CA\n", encoding="utf-8")
|
|
35
|
+
(claude_rules / "explore-thoroughly.md").write_text("# ET\n", encoding="utf-8")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _minimal_code_rules_and_test_quality(claude_docs: Path) -> tuple[bytes, bytes]:
|
|
39
|
+
claude_docs.mkdir(parents=True, exist_ok=True)
|
|
40
|
+
cr = b"## CORE PRINCIPLES\n\nalpha\n"
|
|
41
|
+
tq = b"## Core Testing Principles\n\nbeta\n"
|
|
42
|
+
(claude_docs / "CODE_RULES.md").write_bytes(cr)
|
|
43
|
+
(claude_docs / "TEST_QUALITY.md").write_bytes(tq)
|
|
44
|
+
return cr, tq
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def test_limit_lines_footer_mentions_cursor_docs() -> None:
|
|
48
|
+
long_body = "\n".join(f"line {index}" for index in range(mod.MAX_RULE_BODY_LINES + 5))
|
|
49
|
+
out = mod._limit_lines(long_body, mod.MAX_RULE_BODY_LINES)
|
|
50
|
+
assert ".cursor/docs" in out
|
|
51
|
+
assert "synced reference" in out
|
|
52
|
+
assert "~/.claude/system-prompts/software-engineer.xml" in out
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_sync_canonical_docs_copies_byte_identical(tmp_path: Path) -> None:
|
|
56
|
+
claude = tmp_path / ".claude"
|
|
57
|
+
cursor = tmp_path / ".cursor"
|
|
58
|
+
docs = claude / "docs"
|
|
59
|
+
cr, tq = _minimal_code_rules_and_test_quality(docs)
|
|
60
|
+
mod._sync_canonical_docs(claude, cursor, dry_run=False, quiet=True)
|
|
61
|
+
assert (cursor / "docs" / "CODE_RULES.md").read_bytes() == cr
|
|
62
|
+
assert (cursor / "docs" / "TEST_QUALITY.md").read_bytes() == tq
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_sync_canonical_docs_skips_missing_with_no_dst(tmp_path: Path) -> None:
|
|
66
|
+
claude = tmp_path / ".claude"
|
|
67
|
+
cursor = tmp_path / ".cursor"
|
|
68
|
+
(claude / "docs").mkdir(parents=True)
|
|
69
|
+
(claude / "docs" / "CODE_RULES.md").write_text("x", encoding="utf-8")
|
|
70
|
+
mod._sync_canonical_docs(claude, cursor, dry_run=False, quiet=True)
|
|
71
|
+
assert (cursor / "docs" / "CODE_RULES.md").read_text(encoding="utf-8") == "x"
|
|
72
|
+
assert not (cursor / "docs" / "TEST_QUALITY.md").is_file()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_check_fails_when_doc_source_changes_without_resync(tmp_path: Path) -> None:
|
|
76
|
+
claude = tmp_path / ".claude"
|
|
77
|
+
cursor = tmp_path / ".cursor"
|
|
78
|
+
_minimal_rule_files(claude / "rules")
|
|
79
|
+
_minimal_code_rules_and_test_quality(claude / "docs")
|
|
80
|
+
env = {**os.environ, "LLM_SETTINGS_ROOT": str(tmp_path)}
|
|
81
|
+
subprocess.run(
|
|
82
|
+
[sys.executable, str(_SYNC_SCRIPT), "--force"],
|
|
83
|
+
env=env,
|
|
84
|
+
check=True,
|
|
85
|
+
cwd=str(_SCRIPTS_DIR),
|
|
86
|
+
)
|
|
87
|
+
(claude / "docs" / "CODE_RULES.md").write_bytes(b"changed\n")
|
|
88
|
+
subprocess_result = subprocess.run(
|
|
89
|
+
[sys.executable, str(_SYNC_SCRIPT), "--check"],
|
|
90
|
+
env=env,
|
|
91
|
+
cwd=str(_SCRIPTS_DIR),
|
|
92
|
+
)
|
|
93
|
+
assert subprocess_result.returncode != 0
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_check_passes_after_resync(tmp_path: Path) -> None:
|
|
97
|
+
claude = tmp_path / ".claude"
|
|
98
|
+
cursor = tmp_path / ".cursor"
|
|
99
|
+
_minimal_rule_files(claude / "rules")
|
|
100
|
+
_minimal_code_rules_and_test_quality(claude / "docs")
|
|
101
|
+
env = {**os.environ, "LLM_SETTINGS_ROOT": str(tmp_path)}
|
|
102
|
+
subprocess.run(
|
|
103
|
+
[sys.executable, str(_SYNC_SCRIPT), "--force"],
|
|
104
|
+
env=env,
|
|
105
|
+
check=True,
|
|
106
|
+
cwd=str(_SCRIPTS_DIR),
|
|
107
|
+
)
|
|
108
|
+
subprocess_result = subprocess.run(
|
|
109
|
+
[sys.executable, str(_SYNC_SCRIPT), "--check"],
|
|
110
|
+
env=env,
|
|
111
|
+
cwd=str(_SCRIPTS_DIR),
|
|
112
|
+
)
|
|
113
|
+
assert subprocess_result.returncode == 0
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def test_manifest_includes_docs_entries(tmp_path: Path) -> None:
|
|
117
|
+
claude = tmp_path / ".claude"
|
|
118
|
+
cursor = tmp_path / ".cursor"
|
|
119
|
+
_minimal_rule_files(claude / "rules")
|
|
120
|
+
_minimal_code_rules_and_test_quality(claude / "docs")
|
|
121
|
+
env = {**os.environ, "LLM_SETTINGS_ROOT": str(tmp_path)}
|
|
122
|
+
subprocess.run(
|
|
123
|
+
[sys.executable, str(_SYNC_SCRIPT), "--force"],
|
|
124
|
+
env=env,
|
|
125
|
+
check=True,
|
|
126
|
+
cwd=str(_SCRIPTS_DIR),
|
|
127
|
+
)
|
|
128
|
+
manifest = json.loads((cursor / ".sync-manifest.json").read_text(encoding="utf-8"))
|
|
129
|
+
assert "docs_entries" in manifest
|
|
130
|
+
de = manifest["docs_entries"]
|
|
131
|
+
assert "docs/CODE_RULES.md" in de
|
|
132
|
+
assert "docs/TEST_QUALITY.md" in de
|
|
133
|
+
assert "sources_hash" in de["docs/CODE_RULES.md"]
|
|
134
|
+
assert "output_hash" in de["docs/CODE_RULES.md"]
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def test_merge_code_standards_with_pointer_style_code_rules(tmp_path: Path) -> None:
|
|
138
|
+
rules_directory = tmp_path / "rules"
|
|
139
|
+
docs_directory = tmp_path / "docs"
|
|
140
|
+
rules_directory.mkdir(parents=True, exist_ok=True)
|
|
141
|
+
docs_directory.mkdir(parents=True, exist_ok=True)
|
|
142
|
+
(rules_directory / "code-standards.md").write_text(
|
|
143
|
+
"# Code standards stub\n\n- Use full words\n", encoding="utf-8"
|
|
144
|
+
)
|
|
145
|
+
(docs_directory / "CODE_RULES.md").write_text(
|
|
146
|
+
"# CODE_RULES pointer: canonical code-quality policy lives in"
|
|
147
|
+
" `~/.claude/system-prompts/software-engineer.xml` under `<code_quality>`.\n",
|
|
148
|
+
encoding="utf-8",
|
|
149
|
+
)
|
|
150
|
+
merged = mod.merge_code_standards(
|
|
151
|
+
(rules_directory / "code-standards.md", docs_directory / "CODE_RULES.md")
|
|
152
|
+
)
|
|
153
|
+
assert merged.strip(), "output must not be empty for pointer-style CODE_RULES.md"
|
|
154
|
+
assert "software-engineer.xml" in merged
|
|
155
|
+
assert "code_quality" in merged
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def test_sync_canonical_docs_deletes_stale_copy_when_source_removed(tmp_path: Path) -> None:
|
|
159
|
+
claude = tmp_path / ".claude"
|
|
160
|
+
cursor = tmp_path / ".cursor"
|
|
161
|
+
_minimal_code_rules_and_test_quality(claude / "docs")
|
|
162
|
+
mod._sync_canonical_docs(claude, cursor, dry_run=False, quiet=True)
|
|
163
|
+
assert (cursor / "docs" / "CODE_RULES.md").is_file()
|
|
164
|
+
(claude / "docs" / "CODE_RULES.md").unlink()
|
|
165
|
+
mod._sync_canonical_docs(claude, cursor, dry_run=False, quiet=True)
|
|
166
|
+
assert not (cursor / "docs" / "CODE_RULES.md").is_file()
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def test_dry_run_does_not_create_output_directory(tmp_path: Path) -> None:
|
|
170
|
+
claude = tmp_path / ".claude"
|
|
171
|
+
cursor = tmp_path / ".cursor"
|
|
172
|
+
_minimal_rule_files(claude / "rules")
|
|
173
|
+
_minimal_code_rules_and_test_quality(claude / "docs")
|
|
174
|
+
env = {**os.environ, "LLM_SETTINGS_ROOT": str(tmp_path)}
|
|
175
|
+
subprocess.run(
|
|
176
|
+
[sys.executable, str(_SYNC_SCRIPT), "--dry-run", "--quiet"],
|
|
177
|
+
env=env,
|
|
178
|
+
check=True,
|
|
179
|
+
cwd=str(_SCRIPTS_DIR),
|
|
180
|
+
)
|
|
181
|
+
assert not (cursor / "rules").exists(), "--dry-run must not create the output directory"
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def test_check_skips_optional_mapping_when_source_missing(tmp_path: Path) -> None:
|
|
185
|
+
claude = tmp_path / ".claude"
|
|
186
|
+
cursor = tmp_path / ".cursor"
|
|
187
|
+
_minimal_rule_files(claude / "rules")
|
|
188
|
+
_minimal_code_rules_and_test_quality(claude / "docs")
|
|
189
|
+
env = {**os.environ, "LLM_SETTINGS_ROOT": str(tmp_path)}
|
|
190
|
+
subprocess.run(
|
|
191
|
+
[sys.executable, str(_SYNC_SCRIPT), "--force"],
|
|
192
|
+
env=env,
|
|
193
|
+
check=True,
|
|
194
|
+
cwd=str(_SCRIPTS_DIR),
|
|
195
|
+
)
|
|
196
|
+
(claude / "rules" / "tasklings-preferences.md").unlink()
|
|
197
|
+
subprocess_result = subprocess.run(
|
|
198
|
+
[sys.executable, str(_SYNC_SCRIPT), "--check"],
|
|
199
|
+
env=env,
|
|
200
|
+
cwd=str(_SCRIPTS_DIR),
|
|
201
|
+
)
|
|
202
|
+
assert subprocess_result.returncode == 0, "--check must pass when only optional sources are missing"
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def test_tasklings_glob_derived_from_frontmatter(tmp_path: Path) -> None:
|
|
206
|
+
rules_directory = tmp_path / "rules"
|
|
207
|
+
rules_directory.mkdir(parents=True)
|
|
208
|
+
(rules_directory / "tasklings-preferences.md").write_text(
|
|
209
|
+
'---\npaths:\n - "Y:/MyProject/**"\n - "Z:/Other/**"\n---\n\n# Tasklings\n',
|
|
210
|
+
encoding="utf-8",
|
|
211
|
+
)
|
|
212
|
+
glob_value = _read_paths_glob(rules_directory / "tasklings-preferences.md")
|
|
213
|
+
assert glob_value == "Y:/MyProject/**,Z:/Other/**"
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def test_merge_reference_headers_point_at_cursor_docs(tmp_path: Path) -> None:
|
|
217
|
+
rules_directory = tmp_path / "rules"
|
|
218
|
+
docs_directory = tmp_path / "docs"
|
|
219
|
+
rules_directory.mkdir(parents=True, exist_ok=True)
|
|
220
|
+
docs_directory.mkdir(parents=True, exist_ok=True)
|
|
221
|
+
(rules_directory / "code-standards.md").write_text("# CS\n", encoding="utf-8")
|
|
222
|
+
cr_body = "\n\n".join(
|
|
223
|
+
f"## {t}\n\nbody"
|
|
224
|
+
for t in [
|
|
225
|
+
"COMMENT PRESERVATION (ABSOLUTE RULE)",
|
|
226
|
+
"CORE PRINCIPLES",
|
|
227
|
+
"⚡ HOOK-ENFORCED RULES",
|
|
228
|
+
"4. CONFIG LOCATIONS",
|
|
229
|
+
"5. NO ABBREVIATIONS",
|
|
230
|
+
"6. COMPLETE TYPE HINTS",
|
|
231
|
+
"9. SELF-CONTAINED COMPONENTS",
|
|
232
|
+
]
|
|
233
|
+
)
|
|
234
|
+
(docs_directory / "CODE_RULES.md").write_text(cr_body, encoding="utf-8")
|
|
235
|
+
merged = mod.merge_code_standards(
|
|
236
|
+
(rules_directory / "code-standards.md", docs_directory / "CODE_RULES.md")
|
|
237
|
+
)
|
|
238
|
+
assert ".cursor/docs/CODE_RULES.md" in merged
|
|
239
|
+
|
|
240
|
+
(rules_directory / "testing.md").write_text("# T\n", encoding="utf-8")
|
|
241
|
+
tq_body = "\n\n".join(
|
|
242
|
+
f"## {t}\n\nbody"
|
|
243
|
+
for t in [
|
|
244
|
+
"Delete Useless Tests",
|
|
245
|
+
"Test Dependencies MUST FAIL",
|
|
246
|
+
"Core Testing Principles",
|
|
247
|
+
"React Testing Patterns",
|
|
248
|
+
"Test File Organization",
|
|
249
|
+
]
|
|
250
|
+
)
|
|
251
|
+
(docs_directory / "TEST_QUALITY.md").write_text(tq_body, encoding="utf-8")
|
|
252
|
+
merged_tq = mod.merge_test_quality(
|
|
253
|
+
(rules_directory / "testing.md", docs_directory / "TEST_QUALITY.md")
|
|
254
|
+
)
|
|
255
|
+
assert ".cursor/docs/TEST_QUALITY.md" in merged_tq
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: bdd-protocol
|
|
3
|
+
description: >-
|
|
4
|
+
On-demand BDD depth aligned with jl-cmd/claude-code-config#82: Example Mapping (Smart &
|
|
5
|
+
Molak §6.4), scenario quality and anti-patterns (§7.6), solo minimal BDD, outside-in test
|
|
6
|
+
layout. Use when expanding `<behavior_protocol>` in the system prompt, writing executable
|
|
7
|
+
specifications, or when the user asks for discovery, "the one where" examples, or BDD
|
|
8
|
+
scenario quality. Triggers: bdd-protocol, Example Mapping, BDD anti-patterns, §7.6.
|
|
9
|
+
---
|
|
10
|
+
@~/.claude/skills/bdd-protocol/references/example-mapping.md
|
|
11
|
+
@~/.claude/skills/bdd-protocol/references/anti-patterns.md
|
|
12
|
+
|
|
13
|
+
# BDD protocol (on-demand)
|
|
14
|
+
|
|
15
|
+
The always-on sequence lives in `~/.claude/system-prompts/software-engineer.xml` under `<behavior_protocol>` (Deliberate Discovery → Illustrate → Formulate → Automate). This skill adds **depth** you load when you need algorithms, catalogs, or layout guidance.
|
|
16
|
+
|
|
17
|
+
## When to use this skill
|
|
18
|
+
|
|
19
|
+
- The user or task needs **Example Mapping** steps, parking-lot questions, or "the one where …" phrasing.
|
|
20
|
+
- You are writing or reviewing **scenarios** and need the **§7.6** quality bar and anti-patterns.
|
|
21
|
+
- You are organizing **tests by behavior** (describe / when / should) or using **soap-opera personas** for solo work.
|
|
22
|
+
|
|
23
|
+
## Authority
|
|
24
|
+
|
|
25
|
+
- John Ferguson Smart & Jan Molak, *BDD in Action* 2e (Manning, 2023) — §2.3.7 outside-in, §5.4 Deliberate Discovery, §6.4 Example Mapping, §7.6 scenario quality, §16.5.5 test layout.
|
|
26
|
+
- Dan North, "Introducing BDD" (2006).
|
|
27
|
+
- John Ferguson Smart, Minimal BDD (learnbdd.com; Wayback-cited in tracking issue).
|
|
28
|
+
|
|
29
|
+
## What stays in the system prompt
|
|
30
|
+
|
|
31
|
+
Do not duplicate the four-phase sequence here; keep a single source of truth in `<behavior_protocol>`.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Scenario quality and anti-patterns (§7.6 and related)
|
|
2
|
+
|
|
3
|
+
Use this as a **checklist** when writing or reviewing scenarios and developer-facing specifications. Phrasing follows *BDD in Action* 2e and the catalog summarized in [claude-code-config#82](https://github.com/jl-cmd/claude-code-config/issues/82).
|
|
4
|
+
|
|
5
|
+
## Positive targets (what "good" looks like)
|
|
6
|
+
|
|
7
|
+
- **Declarative focus (§7.6.3):** Scenarios describe **business behavior** and user goals, not low-level UI click scripts.
|
|
8
|
+
- **Single-rule focus (§7.6.4):** One **business rule** per scenario; split when a scenario grows hard to read.
|
|
9
|
+
- **Meaningful actors (§7.6.5):** Personas can be **lightweight** (e.g. soap-opera introductions) when full UX research is unavailable — name + role, grow detail as scenarios demand.
|
|
10
|
+
- **Essential detail (§7.6.6):** Include data and columns that **change outcomes**; omit neutral or redundant columns.
|
|
11
|
+
- **State clarity (§7.6.6):** Make **initial** and **final** state explicit when data illustrates behavior.
|
|
12
|
+
- **Outcome description (§7.6.7):** Outcomes are **observable and measurable** — not hidden behind vague "verify" steps.
|
|
13
|
+
- **Independence (§7.6.8):** Each scenario sets up **its own** data and state so it can run alone.
|
|
14
|
+
|
|
15
|
+
## Anti-patterns to refuse or rewrite
|
|
16
|
+
|
|
17
|
+
- **Imperative scenarios** that read like automation scripts (especially pure UI step lists).
|
|
18
|
+
- **Multi-concern** scenarios that mix unrelated rules.
|
|
19
|
+
- **Incidental detail** that does not serve the rule under test.
|
|
20
|
+
- **Test scripts in disguise** — heavy use of **verify** / **check** without stating the business outcome.
|
|
21
|
+
- **Dependent scenarios** that only pass in order.
|
|
22
|
+
- **Gherkin without conversation** (BAs or testers writing scenarios in isolation) — structured specs are an **output** of discovery, not a substitute for it (Minimal BDD).
|
|
23
|
+
|
|
24
|
+
## Executable specs at the code level
|
|
25
|
+
|
|
26
|
+
Prefer **"should …"** phrasing in names (Dan North, 2006). Nest by **behavior context** (e.g. when / given groupings) rather than mirroring production file layout when that obscures intent (§16.5.5 attitude: tests as documentation).
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Example Mapping (Smart & Molak §6.4)
|
|
2
|
+
|
|
3
|
+
Lightweight, breadth-first discovery before code. Sessions are typically **25–30 minutes**.
|
|
4
|
+
|
|
5
|
+
## Core moves
|
|
6
|
+
|
|
7
|
+
- State **rules**, then ask for an **example of each** rule. Examples often use **"The one where …"** phrasing (Daniel Terhorst-North; sometimes called "Friends episode notation").
|
|
8
|
+
- For each example, probe with **"What if …?"**, **"Is this always the case?"**, **"Are there examples where this rule behaves differently?"** Probes can surface **new rules**; add examples for those rules.
|
|
9
|
+
- **Pink cards**: questions that cannot be answered yet — **park** them; do not pretend they are specifications.
|
|
10
|
+
|
|
11
|
+
## Chat algorithm (solo-friendly)
|
|
12
|
+
|
|
13
|
+
1. **Restate the rule or feature** until it is clearly defined.
|
|
14
|
+
2. Generate **3–5** "the one where …" examples from simple to complex.
|
|
15
|
+
3. For each example, run the **three probes** above.
|
|
16
|
+
4. If probes reveal a **new rule** or materially different behavior, add **2–3** examples for that rule and continue probing.
|
|
17
|
+
5. Repeat until examples for each rule are probed.
|
|
18
|
+
6. Compile **rules + examples + parked questions**; confirm with the user before automating tests.
|
|
19
|
+
7. **Time-box** discovery; proceed to failing tests only after the map is agreed.
|
|
20
|
+
|
|
21
|
+
## Parking lot discipline
|
|
22
|
+
|
|
23
|
+
Unanswered questions stay **out of automated tests** until resolved or explicitly accepted as follow-up work.
|
|
@@ -74,8 +74,8 @@ For each detected directory from Step 1:
|
|
|
74
74
|
- Copy files from package to CLAUDE_HOME, preserving directory structure
|
|
75
75
|
- For skills/, copy recursively (each skill is a subdirectory)
|
|
76
76
|
- For each file, log with status icon:
|
|
77
|
-
- New file: ` ✓ rules/
|
|
78
|
-
- Existing file: ` ↻ rules/
|
|
77
|
+
- New file: ` ✓ rules/bdd.md (new)`
|
|
78
|
+
- Existing file: ` ↻ rules/bdd.md (updated)`
|
|
79
79
|
|
|
80
80
|
If CLAUDE.md generation is enabled:
|
|
81
81
|
- Concatenate all rules/*.md under `# Project Rules (from <package-name> plugin)`
|
|
@@ -112,7 +112,7 @@ Installed <package-name>:
|
|
|
112
112
|
|
|
113
113
|
**Uninstall mode (`--uninstall` flag):**
|
|
114
114
|
- Remove only files matching the package's file list (never delete user-created files)
|
|
115
|
-
- Log each removal: ` ✗ rules/
|
|
115
|
+
- Log each removal: ` ✗ rules/bdd.md (removed)`
|
|
116
116
|
- Remove hook entries from settings.json by matching command paths
|
|
117
117
|
- Remove generated CLAUDE.md if it exists
|
|
118
118
|
- Print removal summary
|