claude-dev-env 8.12.0 → 8.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/pull-request/scripts/test_pull_request.py +1 -1
- package/.agents/skills-archived/pr-converge/scripts/test_check_convergence.py +4 -4
- package/_shared/advisor/reference/astra-rung.md +7 -7
- package/_shared/advisor/reference/third-party-bind.md +2 -2
- package/_shared/advisor/scripts/codex_astra_advisor.py +33 -26
- package/_shared/advisor/scripts/codex_astra_preflight.py +57 -55
- package/_shared/advisor/scripts/config/advisor_scripts_constants/astra_advisor_constants.py +8 -8
- package/_shared/advisor/scripts/tests/test_codex_astra_advisor.py +69 -23
- package/_shared/advisor/scripts/tests/test_codex_astra_preflight.py +4 -4
- package/_shared/pr-loop/scripts/check_convergence.py +54 -49
- package/_shared/pr-loop/scripts/codex_review_scripts_constants/run_constants.py +4 -0
- package/_shared/pr-loop/scripts/pr_converge_scripts_constants/convergence_gate_constants.py +10 -1
- package/_shared/pr-loop/scripts/run_codex_review.py +4 -6
- package/_shared/pr-loop/scripts/test_check_convergence.py +6 -6
- package/_shared/pr-loop/scripts/test_check_convergence_codex.py +73 -31
- package/_shared/pr-loop/scripts/test_check_convergence_contract.py +2 -2
- package/_shared/pr-loop/scripts/tests/test__claude_permissions_common.py +2 -2
- package/bin/git_hooks_installer.test.mjs +1 -1
- package/bin/install-constants.mjs +1 -6
- package/bin/install.cursor-rules.test.mjs +24 -51
- package/bin/install.mjs +16 -70
- package/docs/codex-accounts.md +79 -0
- package/hooks/blocking/test_duplicate_rmtree_helper_blocker.py +5 -5
- package/hooks/blocking/test_pii_scanner.py +1 -1
- package/hooks/blocking/test_pytest_testpaths_orphan_blocker.py +2 -2
- package/hooks/git-hooks/conftest.py +16 -0
- package/hooks/git-hooks/git_hooks_constants/verification_notice_constants.py +1 -1
- package/hooks/git-hooks/test_post_commit.py +1 -1
- package/hooks/git-hooks/test_pre_commit.py +1 -1
- package/hooks/git-hooks/test_pre_push.py +2 -2
- package/hooks/git-hooks/test_verification_start.py +1 -1
- package/hooks/git-hooks/verification_notice.py +2 -2
- package/hooks/git-hooks/verification_notice_context.py +21 -2
- package/hooks/git-hooks/verification_start.py +2 -3
- package/package.json +1 -1
- package/rules/durable-post-artifacts.md +4 -3
- package/scripts/claude_account_profile.py +21 -9
- package/scripts/codex_account_choice.py +396 -0
- package/scripts/codex_account_meters.py +372 -0
- package/scripts/dev_env_scripts_constants/codex_account_constants.py +141 -0
- package/scripts/dev_env_scripts_constants/private_term_constants.py +17 -2
- package/scripts/private_term_scan.py +6 -2
- package/scripts/repository_checks/config/constants.py +1 -0
- package/scripts/repository_checks/tracked_private_terms.py +7 -0
- package/scripts/test_claude_account_profile.py +12 -0
- package/scripts/test_codex_account_choice.py +276 -0
- package/scripts/test_codex_account_meters.py +205 -0
- package/scripts/tests/test_automatic_advisory.py +3 -3
- package/scripts/tests/test_automatic_advisory_polling.py +1 -1
- package/scripts/tests/test_automatic_advisory_recovery.py +1 -1
- package/scripts/tests/test_closed_pr_label.py +1 -1
- package/scripts/tests/test_private_term_scan.py +28 -0
- package/scripts/tests/test_review_closure.py +2 -2
- package/scripts/tests/test_review_closure_model.py +2 -2
- package/scripts/tests/test_rules.py +2 -83
- package/scripts/tests/test_tracked_private_terms.py +15 -0
- package/scripts/tests/test_verification_notice.py +29 -13
- package/.agents/skills/cert-classification-rule/SKILL.md +0 -224
- package/.agents/skills/cert-classification-rule/scripts/resort_cert_folders.py +0 -158
- package/_shared/pr-loop/scripts/codex_review_scripts_constants/codex_usage_probe_constants.py +0 -82
- package/_shared/pr-loop/scripts/codex_usage_probe.py +0 -573
- package/_shared/pr-loop/scripts/test_codex_usage_probe.py +0 -683
- package/scripts/sync_to_cursor/AGENTS.md +0 -1
- package/scripts/sync_to_cursor/CLAUDE.md +0 -1
- package/scripts/sync_to_cursor/__init__.py +0 -13
- package/scripts/sync_to_cursor/canonical_docs.py +0 -66
- package/scripts/sync_to_cursor/config/__init__.py +0 -8
- package/scripts/sync_to_cursor/engine.py +0 -219
- package/scripts/sync_to_cursor/hashing.py +0 -7
- package/scripts/sync_to_cursor/paths.py +0 -18
- package/scripts/sync_to_cursor/rules.py +0 -494
- package/scripts/sync_to_cursor.py +0 -22
- package/scripts/tests/test_engine.py +0 -101
- package/scripts/tests/test_sync_to_cursor.py +0 -315
|
@@ -1,494 +0,0 @@
|
|
|
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 (
|
|
11
|
-
ALL_SKIPPED_RULE_FILE_NAMES,
|
|
12
|
-
CLAUDE_RULES_DIRECTORY_NAME,
|
|
13
|
-
MARKDOWN_SUFFIX,
|
|
14
|
-
MAX_RULE_BODY_LINES,
|
|
15
|
-
)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
def _parse_h2_sections(markdown: str) -> dict[str, str]:
|
|
19
|
-
parts = re.split(r"^## ", markdown, flags=re.MULTILINE)
|
|
20
|
-
sections: dict[str, str] = {}
|
|
21
|
-
for each_part in parts[1:]:
|
|
22
|
-
title_line, _, body = each_part.partition("\n")
|
|
23
|
-
sections[title_line.strip()] = body.strip()
|
|
24
|
-
return sections
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def _filter_core_principles(body: str) -> str:
|
|
28
|
-
lines = []
|
|
29
|
-
for each_line in body.splitlines():
|
|
30
|
-
if "readability standard" in each_line:
|
|
31
|
-
continue
|
|
32
|
-
lines.append(each_line)
|
|
33
|
-
return "\n".join(lines).strip()
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
def _limit_lines(text: str, max_lines: int) -> str:
|
|
37
|
-
lines = text.splitlines()
|
|
38
|
-
if len(lines) <= max_lines:
|
|
39
|
-
return text
|
|
40
|
-
return (
|
|
41
|
-
"\n".join(lines[:max_lines])
|
|
42
|
-
+ "\n\n_(Truncated for Cursor rule length; see the synced reference in `.cursor/docs/` "
|
|
43
|
-
"(`CODE_RULES.md` or `TEST_QUALITY.md` as applicable) and follow its canonical source "
|
|
44
|
-
"under `~/.claude/system-prompts/software-engineer.xml` when needed.)_"
|
|
45
|
-
)
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def _strip_code_standards_blockquote(markdown: str) -> str:
|
|
49
|
-
lines = markdown.splitlines()
|
|
50
|
-
out: list[str] = []
|
|
51
|
-
i = 0
|
|
52
|
-
while i < len(lines):
|
|
53
|
-
if lines[i].startswith(">") and "MANDATORY REFERENCE" in lines[i]:
|
|
54
|
-
while i < len(lines) and lines[i].startswith(">"):
|
|
55
|
-
i += 1
|
|
56
|
-
while i < len(lines) and lines[i].strip() == "":
|
|
57
|
-
i += 1
|
|
58
|
-
continue
|
|
59
|
-
out.append(lines[i])
|
|
60
|
-
i += 1
|
|
61
|
-
return "\n".join(out).strip()
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
_merged_mapping_key_order = (
|
|
65
|
-
"code-standards",
|
|
66
|
-
"tasklings-preferences",
|
|
67
|
-
"bdd",
|
|
68
|
-
"test-quality",
|
|
69
|
-
"research-mode",
|
|
70
|
-
"explore-thoroughly",
|
|
71
|
-
)
|
|
72
|
-
|
|
73
|
-
_code_standards_section_order = (
|
|
74
|
-
"COMMENT PRESERVATION",
|
|
75
|
-
"CORE PRINCIPLES",
|
|
76
|
-
"⚡ LINT-ENFORCED RULES",
|
|
77
|
-
"3. REUSE CONSTANTS / 4. CONFIG LOCATIONS",
|
|
78
|
-
"5. NO ABBREVIATIONS",
|
|
79
|
-
"6. COMPLETE TYPE HINTS",
|
|
80
|
-
"9. SELF-CONTAINED COMPONENTS",
|
|
81
|
-
)
|
|
82
|
-
|
|
83
|
-
_test_quality_section_order = (
|
|
84
|
-
"Delete Useless Tests",
|
|
85
|
-
"Test Dependencies MUST FAIL",
|
|
86
|
-
"Core Testing Principles",
|
|
87
|
-
"React Testing Patterns",
|
|
88
|
-
"Test File Organization",
|
|
89
|
-
)
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
def merge_code_standards(all_sources: tuple[Path, ...]) -> str:
|
|
93
|
-
"""Merge the code-standards rule and CODE_RULES doc into one Cursor rule body.
|
|
94
|
-
|
|
95
|
-
Args:
|
|
96
|
-
all_sources: The code-standards rule path then the CODE_RULES doc path.
|
|
97
|
-
|
|
98
|
-
Returns:
|
|
99
|
-
The merged Cursor rule body, truncated to the maximum rule body length.
|
|
100
|
-
"""
|
|
101
|
-
code_standards_markdown = _strip_code_standards_blockquote(
|
|
102
|
-
all_sources[0].read_text(encoding="utf-8")
|
|
103
|
-
)
|
|
104
|
-
code_standards_markdown = "\n".join(
|
|
105
|
-
each_line
|
|
106
|
-
for each_line in code_standards_markdown.splitlines()
|
|
107
|
-
if not each_line.strip().startswith("- TDD ")
|
|
108
|
-
)
|
|
109
|
-
code_rules_markdown = all_sources[1].read_text(encoding="utf-8")
|
|
110
|
-
sections_by_heading = _parse_h2_sections(code_rules_markdown)
|
|
111
|
-
if not sections_by_heading:
|
|
112
|
-
pointer_fallback_note = (
|
|
113
|
-
"_(Full code-quality rules: `~/.claude/system-prompts/software-engineer.xml`"
|
|
114
|
-
" under `<code_quality>`.)_"
|
|
115
|
-
)
|
|
116
|
-
chunks = [code_standards_markdown, "", pointer_fallback_note]
|
|
117
|
-
return _limit_lines("\n\n".join(chunks), MAX_RULE_BODY_LINES)
|
|
118
|
-
chunks = [
|
|
119
|
-
code_standards_markdown,
|
|
120
|
-
"",
|
|
121
|
-
"## Reference (full text: `.cursor/docs/CODE_RULES.md`)",
|
|
122
|
-
]
|
|
123
|
-
for each_title in _code_standards_section_order:
|
|
124
|
-
assert each_title in sections_by_heading, (
|
|
125
|
-
f"merge_code_standards: expected section absent from CODE_RULES.md: {each_title}"
|
|
126
|
-
)
|
|
127
|
-
body = sections_by_heading[each_title]
|
|
128
|
-
if each_title == "CORE PRINCIPLES":
|
|
129
|
-
body = _filter_core_principles(body)
|
|
130
|
-
if body:
|
|
131
|
-
chunks.append(f"## {each_title}\n\n{body}")
|
|
132
|
-
merged = "\n\n".join(chunks)
|
|
133
|
-
return _limit_lines(merged, MAX_RULE_BODY_LINES)
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
def merge_test_quality(all_sources: tuple[Path, ...]) -> str:
|
|
137
|
-
"""Merge the testing rule and TEST_QUALITY doc into one Cursor rule body.
|
|
138
|
-
|
|
139
|
-
Args:
|
|
140
|
-
all_sources: The testing rule path then the TEST_QUALITY doc path.
|
|
141
|
-
|
|
142
|
-
Returns:
|
|
143
|
-
The merged Cursor rule body, truncated to the maximum rule body length.
|
|
144
|
-
"""
|
|
145
|
-
testing = strip_leading_yaml_frontmatter(
|
|
146
|
-
all_sources[0].read_text(encoding="utf-8")
|
|
147
|
-
).strip()
|
|
148
|
-
test_quality_markdown = all_sources[1].read_text(encoding="utf-8")
|
|
149
|
-
sections_by_heading = _parse_h2_sections(test_quality_markdown)
|
|
150
|
-
chunks = [testing, "", "## Reference (full text: `.cursor/docs/TEST_QUALITY.md`)"]
|
|
151
|
-
for each_title in _test_quality_section_order:
|
|
152
|
-
assert each_title in sections_by_heading, (
|
|
153
|
-
f"merge_test_quality: expected section absent from TEST_QUALITY.md: {each_title}"
|
|
154
|
-
)
|
|
155
|
-
body = sections_by_heading[each_title]
|
|
156
|
-
if body:
|
|
157
|
-
chunks.append(f"## {each_title}\n\n{body}")
|
|
158
|
-
merged = "\n\n".join(chunks)
|
|
159
|
-
return _limit_lines(merged, MAX_RULE_BODY_LINES)
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
def strip_anthropic_refs(text: str) -> str:
|
|
163
|
-
"""Remove Anthropic source citations and the do-not-act wrapper from rule text.
|
|
164
|
-
|
|
165
|
-
Args:
|
|
166
|
-
text: The rule body to clean.
|
|
167
|
-
|
|
168
|
-
Returns:
|
|
169
|
-
The rule body without Anthropic citation lines or the wrapper tags.
|
|
170
|
-
"""
|
|
171
|
-
out_lines: list[str] = []
|
|
172
|
-
for each_line in text.splitlines():
|
|
173
|
-
stripped_line = each_line.strip()
|
|
174
|
-
if stripped_line.startswith("Source:") and (
|
|
175
|
-
"anthropic" in stripped_line.lower()
|
|
176
|
-
or "claude.com" in stripped_line.lower()
|
|
177
|
-
or "docs.anthropic" in stripped_line.lower()
|
|
178
|
-
):
|
|
179
|
-
continue
|
|
180
|
-
if "docs.anthropic.com" in each_line:
|
|
181
|
-
continue
|
|
182
|
-
out_lines.append(each_line)
|
|
183
|
-
text = "\n".join(out_lines)
|
|
184
|
-
text = re.sub(
|
|
185
|
-
r"<do_not_act_before_instructions>\s*",
|
|
186
|
-
"",
|
|
187
|
-
text,
|
|
188
|
-
flags=re.DOTALL,
|
|
189
|
-
)
|
|
190
|
-
text = re.sub(r"\s*</do_not_act_before_instructions>", "", text)
|
|
191
|
-
return text.strip()
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
def verbatim(text: str) -> str:
|
|
195
|
-
return text.strip()
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
def near_verbatim(text: str) -> str:
|
|
199
|
-
return strip_anthropic_refs(text)
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
def strip_leading_yaml_frontmatter(text: str) -> str:
|
|
203
|
-
"""Remove a leading `---` ... `---` block so Cursor `.mdc` keeps its own frontmatter.
|
|
204
|
-
|
|
205
|
-
Args:
|
|
206
|
-
text: The rule body that may open with a Claude YAML frontmatter block.
|
|
207
|
-
|
|
208
|
-
Returns:
|
|
209
|
-
The rule body with any leading frontmatter block removed.
|
|
210
|
-
"""
|
|
211
|
-
lines = text.splitlines()
|
|
212
|
-
if not lines or lines[0].strip() != "---":
|
|
213
|
-
return text
|
|
214
|
-
for i in range(1, len(lines)):
|
|
215
|
-
if lines[i].strip() == "---":
|
|
216
|
-
return "\n".join(lines[i + 1 :]).lstrip("\n")
|
|
217
|
-
return text
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
TransformName = Literal["verbatim", "near_verbatim", "merge_code_standards", "merge_test_quality"]
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
def apply_transform(
|
|
224
|
-
name: TransformName,
|
|
225
|
-
all_sources: tuple[Path, ...],
|
|
226
|
-
*,
|
|
227
|
-
strip_leading_frontmatter: bool = False,
|
|
228
|
-
) -> str:
|
|
229
|
-
"""Run the named transform over the source files and return the rule body.
|
|
230
|
-
|
|
231
|
-
Args:
|
|
232
|
-
name: The transform identifier selecting how the sources combine.
|
|
233
|
-
all_sources: The source files the transform reads.
|
|
234
|
-
strip_leading_frontmatter: When True, drop a leading YAML frontmatter
|
|
235
|
-
block from the raw concatenation before a verbatim transform.
|
|
236
|
-
|
|
237
|
-
Returns:
|
|
238
|
-
The transformed Cursor rule body.
|
|
239
|
-
|
|
240
|
-
Raises:
|
|
241
|
-
AssertionError: When *name* is not a recognized transform identifier.
|
|
242
|
-
"""
|
|
243
|
-
if name == "merge_code_standards":
|
|
244
|
-
return merge_code_standards(all_sources)
|
|
245
|
-
if name == "merge_test_quality":
|
|
246
|
-
return merge_test_quality(all_sources)
|
|
247
|
-
raw = "\n\n".join(each_source.read_text(encoding="utf-8") for each_source in all_sources)
|
|
248
|
-
if strip_leading_frontmatter:
|
|
249
|
-
raw = strip_leading_yaml_frontmatter(raw)
|
|
250
|
-
if name == "verbatim":
|
|
251
|
-
return verbatim(raw)
|
|
252
|
-
if name == "near_verbatim":
|
|
253
|
-
return near_verbatim(raw)
|
|
254
|
-
raise AssertionError(name)
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
@dataclass(frozen=True)
|
|
258
|
-
class RuleMapping:
|
|
259
|
-
key: str
|
|
260
|
-
sources: tuple[Path, ...]
|
|
261
|
-
output_name: str
|
|
262
|
-
always_apply: bool
|
|
263
|
-
globs: str | None
|
|
264
|
-
description: str
|
|
265
|
-
transform: TransformName
|
|
266
|
-
strip_leading_frontmatter: bool = False
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
def _frontmatter(description: str, always_apply: bool, globs: str | None) -> str:
|
|
270
|
-
escaped_description = description.replace('"', '\\"')
|
|
271
|
-
lines = ["---", f'description: "{escaped_description}"']
|
|
272
|
-
if globs:
|
|
273
|
-
escaped_globs = globs.replace('"', '\\"')
|
|
274
|
-
lines.append(f'globs: "{escaped_globs}"')
|
|
275
|
-
lines.append(f"alwaysApply: {'true' if always_apply else 'false'}")
|
|
276
|
-
lines.append("---")
|
|
277
|
-
return "\n".join(lines) + "\n"
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
def _full_mdc(mapping: RuleMapping, body: str) -> str:
|
|
281
|
-
generated_header = (
|
|
282
|
-
"<!-- Generated by sync_to_cursor.py — do not edit directly -->\n"
|
|
283
|
-
"<!-- Re-run: python ~/.claude/scripts/sync_to_cursor.py -->\n"
|
|
284
|
-
"<!-- Output: .cursor/rules/*.mdc, .cursor/docs/*.md"
|
|
285
|
-
" (see LLM_SETTINGS_ROOT in script docstring) -->\n"
|
|
286
|
-
)
|
|
287
|
-
return _frontmatter(mapping.description, mapping.always_apply, mapping.globs) + "\n" + generated_header + "\n" + body + "\n"
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
def _read_paths_glob(rule_file: Path) -> str | None:
|
|
291
|
-
"""Read the `paths:` list from a Claude rule's YAML frontmatter as a Cursor glob.
|
|
292
|
-
|
|
293
|
-
Args:
|
|
294
|
-
rule_file: The Claude rule file whose frontmatter may declare `paths:`.
|
|
295
|
-
|
|
296
|
-
Returns:
|
|
297
|
-
The comma-separated glob string, or None when the rule declares no paths.
|
|
298
|
-
"""
|
|
299
|
-
if not rule_file.is_file():
|
|
300
|
-
return None
|
|
301
|
-
lines = rule_file.read_text(encoding="utf-8").splitlines()
|
|
302
|
-
if not lines or lines[0].strip() != "---":
|
|
303
|
-
return None
|
|
304
|
-
is_in_paths = False
|
|
305
|
-
all_paths: list[str] = []
|
|
306
|
-
for each_line in lines[1:]:
|
|
307
|
-
if each_line.strip() == "---":
|
|
308
|
-
break
|
|
309
|
-
if each_line.startswith("paths:"):
|
|
310
|
-
is_in_paths = True
|
|
311
|
-
continue
|
|
312
|
-
if is_in_paths:
|
|
313
|
-
if each_line.startswith(" ") or each_line.startswith("\t"):
|
|
314
|
-
stripped_path = each_line.strip().lstrip("-").strip().strip('"').strip("'")
|
|
315
|
-
if stripped_path:
|
|
316
|
-
all_paths.append(stripped_path)
|
|
317
|
-
else:
|
|
318
|
-
is_in_paths = False
|
|
319
|
-
return ",".join(all_paths) if all_paths else None
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
def _require_paths_glob(rule_file: Path) -> str | None:
|
|
323
|
-
"""Return the path glob for an optional rule, requiring frontmatter when it exists.
|
|
324
|
-
|
|
325
|
-
Args:
|
|
326
|
-
rule_file: The Claude rule file whose frontmatter may declare `paths:`.
|
|
327
|
-
|
|
328
|
-
Returns:
|
|
329
|
-
The comma-separated glob string, or None when the rule file is absent.
|
|
330
|
-
|
|
331
|
-
Raises:
|
|
332
|
-
AssertionError: When the rule file exists but declares no `paths:`
|
|
333
|
-
frontmatter, which would silently disable the rule in Cursor.
|
|
334
|
-
"""
|
|
335
|
-
if not rule_file.is_file():
|
|
336
|
-
return None
|
|
337
|
-
paths_glob = _read_paths_glob(rule_file)
|
|
338
|
-
assert paths_glob is not None, (
|
|
339
|
-
f"{rule_file.name}: path-scoped rule exists but declares no `paths:` frontmatter; "
|
|
340
|
-
"add a `paths:` list or remove the file"
|
|
341
|
-
)
|
|
342
|
-
return paths_glob
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
def _always_apply_mappings(rules_directory: Path, docs_directory: Path) -> tuple[RuleMapping, ...]:
|
|
346
|
-
return (
|
|
347
|
-
RuleMapping(
|
|
348
|
-
"code-standards",
|
|
349
|
-
(rules_directory / "code-standards.md", docs_directory / "CODE_RULES.md"),
|
|
350
|
-
"code-standards.mdc",
|
|
351
|
-
True,
|
|
352
|
-
None,
|
|
353
|
-
"Core code standards: naming, types, config, hook-enforced rules",
|
|
354
|
-
"merge_code_standards",
|
|
355
|
-
),
|
|
356
|
-
RuleMapping(
|
|
357
|
-
"bdd",
|
|
358
|
-
(rules_directory / "bdd.md",),
|
|
359
|
-
"bdd.mdc",
|
|
360
|
-
True,
|
|
361
|
-
None,
|
|
362
|
-
"BDD: discovery-driven protocol (outside-in; Illustrate→Formulate→Automate)",
|
|
363
|
-
"verbatim",
|
|
364
|
-
),
|
|
365
|
-
RuleMapping(
|
|
366
|
-
"research-mode",
|
|
367
|
-
(rules_directory / "research-mode.md",),
|
|
368
|
-
"research-mode.mdc",
|
|
369
|
-
True,
|
|
370
|
-
None,
|
|
371
|
-
"Research mode: citations and I don't know",
|
|
372
|
-
"near_verbatim",
|
|
373
|
-
),
|
|
374
|
-
RuleMapping(
|
|
375
|
-
"explore-thoroughly",
|
|
376
|
-
(rules_directory / "explore-thoroughly.md",),
|
|
377
|
-
"explore-thoroughly.mdc",
|
|
378
|
-
True,
|
|
379
|
-
None,
|
|
380
|
-
"Explore codebase before committing to an approach",
|
|
381
|
-
"near_verbatim",
|
|
382
|
-
),
|
|
383
|
-
)
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
def _path_scoped_mappings(rules_directory: Path, docs_directory: Path) -> tuple[RuleMapping, ...]:
|
|
387
|
-
return (
|
|
388
|
-
RuleMapping(
|
|
389
|
-
"tasklings-preferences",
|
|
390
|
-
(rules_directory / "tasklings-preferences.md",),
|
|
391
|
-
"tasklings-preferences.mdc",
|
|
392
|
-
False,
|
|
393
|
-
_require_paths_glob(rules_directory / "tasklings-preferences.md"),
|
|
394
|
-
"Tasklings: Prefer / Do / Always engineering preferences (scoped path)",
|
|
395
|
-
"verbatim",
|
|
396
|
-
True,
|
|
397
|
-
),
|
|
398
|
-
RuleMapping(
|
|
399
|
-
"test-quality",
|
|
400
|
-
(rules_directory / "testing.md", docs_directory / "TEST_QUALITY.md"),
|
|
401
|
-
"test-quality.mdc",
|
|
402
|
-
False,
|
|
403
|
-
_require_paths_glob(rules_directory / "testing.md"),
|
|
404
|
-
"Testing quality for test files",
|
|
405
|
-
"merge_test_quality",
|
|
406
|
-
),
|
|
407
|
-
)
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
def _description_from_rule_file(rule_file: Path, fallback_key: str) -> str:
|
|
411
|
-
"""Return the first markdown heading, or the rule key with spaces.
|
|
412
|
-
|
|
413
|
-
Args:
|
|
414
|
-
rule_file: Claude rule markdown to read.
|
|
415
|
-
fallback_key: Stem used when the file has no heading.
|
|
416
|
-
|
|
417
|
-
Returns:
|
|
418
|
-
A one-line Cursor `description` value.
|
|
419
|
-
"""
|
|
420
|
-
for each_line in rule_file.read_text(encoding="utf-8").splitlines():
|
|
421
|
-
stripped = each_line.strip()
|
|
422
|
-
if stripped.startswith("# "):
|
|
423
|
-
return stripped[2:].strip()
|
|
424
|
-
return fallback_key.replace("-", " ")
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
def _discovered_mappings(
|
|
428
|
-
rules_directory: Path, all_covered_source_names: set[str]
|
|
429
|
-
) -> tuple[RuleMapping, ...]:
|
|
430
|
-
"""Map remaining `*.md` rule files to `<stem>.mdc` Cursor rules.
|
|
431
|
-
|
|
432
|
-
Args:
|
|
433
|
-
rules_directory: Claude `rules/` directory.
|
|
434
|
-
all_covered_source_names: Rule filenames already claimed by curated mappings.
|
|
435
|
-
|
|
436
|
-
Returns:
|
|
437
|
-
One mapping per remaining markdown rule, sorted by filename.
|
|
438
|
-
"""
|
|
439
|
-
if not rules_directory.is_dir():
|
|
440
|
-
return ()
|
|
441
|
-
all_discovered: list[RuleMapping] = []
|
|
442
|
-
for each_rule_file in sorted(rules_directory.glob("*.md")):
|
|
443
|
-
if each_rule_file.name in ALL_SKIPPED_RULE_FILE_NAMES:
|
|
444
|
-
continue
|
|
445
|
-
if each_rule_file.name in all_covered_source_names:
|
|
446
|
-
continue
|
|
447
|
-
key = each_rule_file.stem
|
|
448
|
-
paths_glob = _read_paths_glob(each_rule_file)
|
|
449
|
-
has_paths = paths_glob is not None
|
|
450
|
-
all_discovered.append(
|
|
451
|
-
RuleMapping(
|
|
452
|
-
key,
|
|
453
|
-
(each_rule_file,),
|
|
454
|
-
f"{key}.mdc",
|
|
455
|
-
not has_paths,
|
|
456
|
-
paths_glob,
|
|
457
|
-
_description_from_rule_file(each_rule_file, key),
|
|
458
|
-
"verbatim",
|
|
459
|
-
strip_leading_frontmatter=has_paths,
|
|
460
|
-
)
|
|
461
|
-
)
|
|
462
|
-
return tuple(all_discovered)
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
def build_mappings(claude: Path) -> tuple[RuleMapping, ...]:
|
|
466
|
-
"""Resolve every rule into a concrete Cursor mapping against a Claude layout.
|
|
467
|
-
|
|
468
|
-
Args:
|
|
469
|
-
claude: The Claude layout root holding the `rules` and `docs` directories.
|
|
470
|
-
|
|
471
|
-
Returns:
|
|
472
|
-
One RuleMapping per rule, each path-scoped rule carrying a glob derived
|
|
473
|
-
from its source rule's `paths:` frontmatter. Curated mappings come first,
|
|
474
|
-
then one generated mapping per remaining `rules/*.md` file.
|
|
475
|
-
"""
|
|
476
|
-
rules_directory = claude / "rules"
|
|
477
|
-
docs_directory = claude / "docs"
|
|
478
|
-
all_curated = (
|
|
479
|
-
*_always_apply_mappings(rules_directory, docs_directory),
|
|
480
|
-
*_path_scoped_mappings(rules_directory, docs_directory),
|
|
481
|
-
)
|
|
482
|
-
mapping_by_key = {each_mapping.key: each_mapping for each_mapping in all_curated}
|
|
483
|
-
assert set(mapping_by_key) == set(_merged_mapping_key_order)
|
|
484
|
-
ordered_curated = tuple(mapping_by_key[each_key] for each_key in _merged_mapping_key_order)
|
|
485
|
-
all_covered_source_names = {
|
|
486
|
-
each_source.name
|
|
487
|
-
for each_mapping in ordered_curated
|
|
488
|
-
for each_source in each_mapping.sources
|
|
489
|
-
if each_source.suffix == MARKDOWN_SUFFIX
|
|
490
|
-
and each_source.parent.name == CLAUDE_RULES_DIRECTORY_NAME
|
|
491
|
-
}
|
|
492
|
-
return ordered_curated + _discovered_mappings(
|
|
493
|
-
rules_directory, all_covered_source_names
|
|
494
|
-
)
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""Generate Cursor rules from ~/.claude/rules and docs.
|
|
3
|
-
|
|
4
|
-
Writes to <profile or repo>/.cursor/rules/*.mdc, .cursor/docs/*.md (byte copies of
|
|
5
|
-
CODE_RULES.md and TEST_QUALITY.md when present), and .cursor/.sync-manifest.json.
|
|
6
|
-
If LLM_SETTINGS_ROOT is set to the llm-settings repo root, uses <root>/.claude and
|
|
7
|
-
<root>/.cursor. Otherwise uses ~/.claude and ~/.cursor (after junction install).
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
|
-
from __future__ import annotations
|
|
11
|
-
|
|
12
|
-
import sys
|
|
13
|
-
from pathlib import Path
|
|
14
|
-
|
|
15
|
-
_SCRIPTS_DIR = Path(__file__).resolve().parent
|
|
16
|
-
if str(_SCRIPTS_DIR) not in sys.path:
|
|
17
|
-
sys.path.insert(0, str(_SCRIPTS_DIR))
|
|
18
|
-
|
|
19
|
-
from sync_to_cursor.engine import main
|
|
20
|
-
|
|
21
|
-
if __name__ == "__main__":
|
|
22
|
-
sys.exit(main())
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
"""Tests for sync_to_cursor engine CLI roots."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import sys
|
|
6
|
-
from pathlib import Path
|
|
7
|
-
|
|
8
|
-
import pytest
|
|
9
|
-
|
|
10
|
-
_SCRIPTS_DIR = Path(__file__).resolve().parent.parent
|
|
11
|
-
if str(_SCRIPTS_DIR) not in sys.path:
|
|
12
|
-
sys.path.insert(0, str(_SCRIPTS_DIR))
|
|
13
|
-
|
|
14
|
-
from sync_to_cursor.engine import run as run_sync_to_cursor
|
|
15
|
-
|
|
16
|
-
_CODE_STANDARDS_SECTION_ORDER = (
|
|
17
|
-
"COMMENT PRESERVATION",
|
|
18
|
-
"CORE PRINCIPLES",
|
|
19
|
-
"⚡ LINT-ENFORCED RULES",
|
|
20
|
-
"3. REUSE CONSTANTS / 4. CONFIG LOCATIONS",
|
|
21
|
-
"5. NO ABBREVIATIONS",
|
|
22
|
-
"6. COMPLETE TYPE HINTS",
|
|
23
|
-
"9. SELF-CONTAINED COMPONENTS",
|
|
24
|
-
)
|
|
25
|
-
_TEST_QUALITY_SECTION_ORDER = (
|
|
26
|
-
"Delete Useless Tests",
|
|
27
|
-
"Test Dependencies MUST FAIL",
|
|
28
|
-
"Core Testing Principles",
|
|
29
|
-
"React Testing Patterns",
|
|
30
|
-
"Test File Organization",
|
|
31
|
-
)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def _write_minimal_curated_rules(rules_directory: Path) -> None:
|
|
35
|
-
rules_directory.mkdir(parents=True, exist_ok=True)
|
|
36
|
-
(rules_directory / "code-standards.md").write_text(
|
|
37
|
-
"# Code standards stub\n", encoding="utf-8"
|
|
38
|
-
)
|
|
39
|
-
(rules_directory / "tasklings-preferences.md").write_text(
|
|
40
|
-
'---\npaths:\n - "Y:/x/**"\n---\n\n# Tasklings\n',
|
|
41
|
-
encoding="utf-8",
|
|
42
|
-
)
|
|
43
|
-
(rules_directory / "bdd.md").write_text("# BDD\n", encoding="utf-8")
|
|
44
|
-
(rules_directory / "testing.md").write_text(
|
|
45
|
-
'---\npaths:\n - "**/test_*.py"\n---\n\n# Testing\n',
|
|
46
|
-
encoding="utf-8",
|
|
47
|
-
)
|
|
48
|
-
(rules_directory / "research-mode.md").write_text("# RM\n", encoding="utf-8")
|
|
49
|
-
(rules_directory / "explore-thoroughly.md").write_text("# ET\n", encoding="utf-8")
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
def _write_minimal_docs(docs_directory: Path) -> None:
|
|
53
|
-
docs_directory.mkdir(parents=True, exist_ok=True)
|
|
54
|
-
(docs_directory / "CODE_RULES.md").write_text(
|
|
55
|
-
"\n\n".join(f"## {title}\n\nalpha" for title in _CODE_STANDARDS_SECTION_ORDER)
|
|
56
|
-
+ "\n",
|
|
57
|
-
encoding="utf-8",
|
|
58
|
-
)
|
|
59
|
-
(docs_directory / "TEST_QUALITY.md").write_text(
|
|
60
|
-
"\n\n".join(f"## {title}\n\nbeta" for title in _TEST_QUALITY_SECTION_ORDER)
|
|
61
|
-
+ "\n",
|
|
62
|
-
encoding="utf-8",
|
|
63
|
-
)
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
def test_explicit_roots_write_stem_named_mdc(
|
|
67
|
-
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
68
|
-
) -> None:
|
|
69
|
-
claude = tmp_path / "claude-home"
|
|
70
|
-
cursor = tmp_path / "cursor-home"
|
|
71
|
-
_write_minimal_curated_rules(claude / "rules")
|
|
72
|
-
_write_minimal_docs(claude / "docs")
|
|
73
|
-
(claude / "rules" / "asd-ste100-language.md").write_text(
|
|
74
|
-
"# ASD-STE100 Language Policy\n\nUse short sentences.\n",
|
|
75
|
-
encoding="utf-8",
|
|
76
|
-
)
|
|
77
|
-
(claude / "rules" / "CLAUDE.md").write_text("# Inventory\n", encoding="utf-8")
|
|
78
|
-
monkeypatch.delenv("LLM_SETTINGS_ROOT", raising=False)
|
|
79
|
-
assert cursor.exists() is False
|
|
80
|
-
assert (
|
|
81
|
-
run_sync_to_cursor(
|
|
82
|
-
[
|
|
83
|
-
"--force",
|
|
84
|
-
"--claude-root",
|
|
85
|
-
str(claude),
|
|
86
|
-
"--cursor-root",
|
|
87
|
-
str(cursor),
|
|
88
|
-
]
|
|
89
|
-
)
|
|
90
|
-
== 0
|
|
91
|
-
)
|
|
92
|
-
generated = (cursor / "rules" / "asd-ste100-language.mdc").read_text(encoding="utf-8")
|
|
93
|
-
assert 'description: "ASD-STE100 Language Policy"' in generated
|
|
94
|
-
assert "alwaysApply: true" in generated
|
|
95
|
-
assert "Use short sentences." in generated
|
|
96
|
-
assert not (cursor / "rules" / "CLAUDE.mdc").is_file()
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
def test_explicit_roots_require_both_flags(tmp_path: Path) -> None:
|
|
100
|
-
with pytest.raises(SystemExit):
|
|
101
|
-
run_sync_to_cursor(["--force", "--claude-root", str(tmp_path)])
|