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.
Files changed (74) hide show
  1. package/.agents/skills/pull-request/scripts/test_pull_request.py +1 -1
  2. package/.agents/skills-archived/pr-converge/scripts/test_check_convergence.py +4 -4
  3. package/_shared/advisor/reference/astra-rung.md +7 -7
  4. package/_shared/advisor/reference/third-party-bind.md +2 -2
  5. package/_shared/advisor/scripts/codex_astra_advisor.py +33 -26
  6. package/_shared/advisor/scripts/codex_astra_preflight.py +57 -55
  7. package/_shared/advisor/scripts/config/advisor_scripts_constants/astra_advisor_constants.py +8 -8
  8. package/_shared/advisor/scripts/tests/test_codex_astra_advisor.py +69 -23
  9. package/_shared/advisor/scripts/tests/test_codex_astra_preflight.py +4 -4
  10. package/_shared/pr-loop/scripts/check_convergence.py +54 -49
  11. package/_shared/pr-loop/scripts/codex_review_scripts_constants/run_constants.py +4 -0
  12. package/_shared/pr-loop/scripts/pr_converge_scripts_constants/convergence_gate_constants.py +10 -1
  13. package/_shared/pr-loop/scripts/run_codex_review.py +4 -6
  14. package/_shared/pr-loop/scripts/test_check_convergence.py +6 -6
  15. package/_shared/pr-loop/scripts/test_check_convergence_codex.py +73 -31
  16. package/_shared/pr-loop/scripts/test_check_convergence_contract.py +2 -2
  17. package/_shared/pr-loop/scripts/tests/test__claude_permissions_common.py +2 -2
  18. package/bin/git_hooks_installer.test.mjs +1 -1
  19. package/bin/install-constants.mjs +1 -6
  20. package/bin/install.cursor-rules.test.mjs +24 -51
  21. package/bin/install.mjs +16 -70
  22. package/docs/codex-accounts.md +79 -0
  23. package/hooks/blocking/test_duplicate_rmtree_helper_blocker.py +5 -5
  24. package/hooks/blocking/test_pii_scanner.py +1 -1
  25. package/hooks/blocking/test_pytest_testpaths_orphan_blocker.py +2 -2
  26. package/hooks/git-hooks/conftest.py +16 -0
  27. package/hooks/git-hooks/git_hooks_constants/verification_notice_constants.py +1 -1
  28. package/hooks/git-hooks/test_post_commit.py +1 -1
  29. package/hooks/git-hooks/test_pre_commit.py +1 -1
  30. package/hooks/git-hooks/test_pre_push.py +2 -2
  31. package/hooks/git-hooks/test_verification_start.py +1 -1
  32. package/hooks/git-hooks/verification_notice.py +2 -2
  33. package/hooks/git-hooks/verification_notice_context.py +21 -2
  34. package/hooks/git-hooks/verification_start.py +2 -3
  35. package/package.json +1 -1
  36. package/rules/durable-post-artifacts.md +4 -3
  37. package/scripts/claude_account_profile.py +21 -9
  38. package/scripts/codex_account_choice.py +396 -0
  39. package/scripts/codex_account_meters.py +372 -0
  40. package/scripts/dev_env_scripts_constants/codex_account_constants.py +141 -0
  41. package/scripts/dev_env_scripts_constants/private_term_constants.py +17 -2
  42. package/scripts/private_term_scan.py +6 -2
  43. package/scripts/repository_checks/config/constants.py +1 -0
  44. package/scripts/repository_checks/tracked_private_terms.py +7 -0
  45. package/scripts/test_claude_account_profile.py +12 -0
  46. package/scripts/test_codex_account_choice.py +276 -0
  47. package/scripts/test_codex_account_meters.py +205 -0
  48. package/scripts/tests/test_automatic_advisory.py +3 -3
  49. package/scripts/tests/test_automatic_advisory_polling.py +1 -1
  50. package/scripts/tests/test_automatic_advisory_recovery.py +1 -1
  51. package/scripts/tests/test_closed_pr_label.py +1 -1
  52. package/scripts/tests/test_private_term_scan.py +28 -0
  53. package/scripts/tests/test_review_closure.py +2 -2
  54. package/scripts/tests/test_review_closure_model.py +2 -2
  55. package/scripts/tests/test_rules.py +2 -83
  56. package/scripts/tests/test_tracked_private_terms.py +15 -0
  57. package/scripts/tests/test_verification_notice.py +29 -13
  58. package/.agents/skills/cert-classification-rule/SKILL.md +0 -224
  59. package/.agents/skills/cert-classification-rule/scripts/resort_cert_folders.py +0 -158
  60. package/_shared/pr-loop/scripts/codex_review_scripts_constants/codex_usage_probe_constants.py +0 -82
  61. package/_shared/pr-loop/scripts/codex_usage_probe.py +0 -573
  62. package/_shared/pr-loop/scripts/test_codex_usage_probe.py +0 -683
  63. package/scripts/sync_to_cursor/AGENTS.md +0 -1
  64. package/scripts/sync_to_cursor/CLAUDE.md +0 -1
  65. package/scripts/sync_to_cursor/__init__.py +0 -13
  66. package/scripts/sync_to_cursor/canonical_docs.py +0 -66
  67. package/scripts/sync_to_cursor/config/__init__.py +0 -8
  68. package/scripts/sync_to_cursor/engine.py +0 -219
  69. package/scripts/sync_to_cursor/hashing.py +0 -7
  70. package/scripts/sync_to_cursor/paths.py +0 -18
  71. package/scripts/sync_to_cursor/rules.py +0 -494
  72. package/scripts/sync_to_cursor.py +0 -22
  73. package/scripts/tests/test_engine.py +0 -101
  74. 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)])