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.
Files changed (39) hide show
  1. package/CLAUDE.md +16 -0
  2. package/bin/install.mjs +34 -1
  3. package/docs/BDD_DISCOVERY_PROTOCOL.md +53 -0
  4. package/docs/BDD_SCENARIO_QUALITY.md +89 -0
  5. package/docs/BDD_TEST_LAYOUT.md +71 -0
  6. package/docs/CODE_RULES.md +1 -208
  7. package/hooks/blocking/tdd-enforcer.py +3 -3
  8. package/package.json +5 -2
  9. package/rules/agent-spawn-protocol.md +1 -47
  10. package/rules/bdd.md +28 -0
  11. package/rules/cleanup-temp-files.md +1 -27
  12. package/rules/code-reviews.md +1 -11
  13. package/rules/code-standards.md +1 -43
  14. package/rules/conservative-action.md +1 -20
  15. package/rules/context7.md +1 -12
  16. package/rules/explore-thoroughly.md +1 -27
  17. package/rules/git-workflow.md +1 -42
  18. package/rules/parallel-tools.md +1 -23
  19. package/rules/research-mode.md +1 -23
  20. package/rules/right-sized-engineering.md +1 -28
  21. package/rules/self-contained-docs.md +1 -0
  22. package/rules/vault-context.md +1 -0
  23. package/rules/verify-before-asking.md +1 -0
  24. package/scripts/sync-to-cursor.py +22 -0
  25. package/scripts/sync_to_cursor/__init__.py +13 -0
  26. package/scripts/sync_to_cursor/canonical_docs.py +66 -0
  27. package/scripts/sync_to_cursor/config.py +5 -0
  28. package/scripts/sync_to_cursor/engine.py +194 -0
  29. package/scripts/sync_to_cursor/hashing.py +7 -0
  30. package/scripts/sync_to_cursor/paths.py +18 -0
  31. package/scripts/sync_to_cursor/rules.py +321 -0
  32. package/scripts/tests/test_sync_to_cursor.py +255 -0
  33. package/skills/bdd-protocol/SKILL.md +31 -0
  34. package/skills/bdd-protocol/references/anti-patterns.md +26 -0
  35. package/skills/bdd-protocol/references/example-mapping.md +23 -0
  36. package/skills/npm-creator/SKILL.md +3 -3
  37. package/skills/rule-audit/SKILL.md +2 -2
  38. package/system-prompts/software-engineer.xml +387 -0
  39. package/rules/tdd.md +0 -7
@@ -1,43 +1 @@
1
- # Code Standards
2
-
3
- > **MANDATORY REFERENCE:** CODE_RULES.md - Load for ALL code generation.
4
- > This is the single source of truth for code standards. Non-negotiable.
5
-
6
- @${CLAUDE_PLUGIN_ROOT}/docs/CODE_RULES.md
7
-
8
- **Key principles (see CODE_RULES.md for complete reference):**
9
- - Self-documenting code (no comments)
10
- - Centralized configuration (one source of truth)
11
- - Reuse constants (search before creating)
12
- - No magic values (everything named)
13
- - No abbreviations (full words)
14
- - Complete type hints
15
- - TDD (test first)
16
-
17
- ## Function Parameters - Required vs Optional
18
-
19
- **Use required parameters when no valid use case exists for optional.**
20
- **Remove unused parameters.**
21
-
22
- ## Encapsulation - Logic Belongs in Models
23
-
24
- **NEVER scatter construction logic in calling code.**
25
-
26
- Path/URL building, formatting, transformations -> Put in model methods.
27
- If you find yourself building the same string pattern in multiple places, it belongs in the model.
28
-
29
- ## Document Temporary Code
30
-
31
- **Scaffolding/placeholder code MUST have TODO comments.**
32
-
33
- When code exists only to enable testing before full implementation:
34
- - Add `// TODO: Replace with...` explaining what will replace it
35
- - Explain WHY it's temporary, not just WHAT it does
36
-
37
- ## Naming Reflects Behavior
38
-
39
- **Name components after what they ARE, not abstract concepts.**
40
-
41
- If it overlays the viewport -> "Overlay" not "Screen"
42
- If it validates input -> "Validator" not "Handler"
43
- Names should describe observable behavior or visual appearance.
1
+ # Code-standards pointer: canonical policy lives in `~/.claude/system-prompts/software-engineer.xml` under `<code_quality>`.
@@ -1,20 +1 @@
1
- # Conservative Action
2
-
3
- Source: [Anthropic - Tool Usage](https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/claude-prompting-best-practices#tool-usage)
4
-
5
- <do_not_act_before_instructions>
6
- When the user's intent is ambiguous, default to research and recommendations rather than taking action. Provide information, explain options, and surface tradeoffs — then let the user decide before making changes.
7
-
8
- Proceed with edits, file modifications, or implementations only when the user explicitly requests them.
9
- </do_not_act_before_instructions>
10
-
11
- ## Deciding whether to act
12
-
13
- - If the user asks a question, answer the question. Do not also fix the thing they asked about.
14
- - If the user describes a problem, investigate and recommend. Do not jump to implementation.
15
- - If the user says "do it", "go ahead", "make the change", or similarly explicit language, proceed with action.
16
- - When in doubt, ask: "Would you like me to make this change, or just show you the approach?"
17
-
18
- ## Why
19
-
20
- Acting prematurely wastes effort and round-trips when the user wanted a different approach. Exploring first produces better outcomes than committing early. This is especially important with models that have a strong action bias.
1
+ # Conservative-action pointer: canonical policy lives in `~/.claude/system-prompts/software-engineer.xml` under `<task_scope>`.
package/rules/context7.md CHANGED
@@ -1,12 +1 @@
1
- ---
2
- alwaysApply: true
3
- ---
4
-
5
- When working with libraries, frameworks, or APIs — use Context7 MCP to fetch current documentation instead of relying on training data. This includes setup questions, code generation, API references, and anything involving specific packages.
6
-
7
- ## Steps
8
-
9
- 1. Call `resolve-library-id` with the library name and the user's question
10
- 2. Pick the best match — prefer exact names and version-specific IDs when a version is mentioned
11
- 3. Call `query-docs` with the selected library ID and the user's question
12
- 4. Answer using the fetched docs — include code examples and cite the version
1
+ # Context7 pointer: canonical policy lives in `~/.claude/system-prompts/software-engineer.xml` under `<context7>`.
@@ -1,27 +1 @@
1
- # Explore Thoroughly
2
-
3
- Source: [Anthropic - Overthinking and Excessive Thoroughness](https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/claude-prompting-best-practices#overthinking-and-excessive-thoroughness)
4
-
5
- Note: This deliberately chooses exploration depth over the "commit and execute quickly" pattern from the same source. Thorough upfront exploration is preferred for the intended workflow.
6
-
7
- ## Before committing to an approach
8
-
9
- - Read the relevant files. Understand what exists before proposing what to change.
10
- - Map the existing patterns: naming conventions, file organization, architectural decisions.
11
- - Identify constraints that could invalidate an approach before investing effort in it.
12
- - For unfamiliar codebases or high-stakes changes, invest more time exploring than feels necessary.
13
-
14
- ## Exploration scales with risk
15
-
16
- - Small change to a familiar file: a quick read of the file and its immediate neighbors is sufficient.
17
- - New feature or cross-cutting change: read broadly across the codebase to understand how similar things are done.
18
- - Architectural decision: explore the full landscape before recommending a direction.
19
-
20
- ## Relationship to other rules
21
-
22
- - **conservative-action.md** gates *whether* to act. This rule governs *how deeply* to investigate.
23
- - **research-mode.md** ensures factual claims are grounded. This rule ensures implementation plans are grounded in the actual codebase.
24
-
25
- ## Why
26
-
27
- Premature commitment leads to wasted effort when the chosen approach conflicts with existing patterns or misses important context. Thorough exploration surfaces constraints early and produces better-informed solutions.
1
+ # Explore-thoroughly pointer: canonical policy lives in `~/.claude/system-prompts/software-engineer.xml` under `<investigation>`.
@@ -1,42 +1 @@
1
- # Git Workflow
2
-
3
- User-level rule: applies to **every** git repo that uses GitHub with `gh` (no exceptions for “small” or non-primary repos unless the user says otherwise in the session).
4
-
5
- ## Workflow Decision Tree
6
-
7
- **When to use stacked PRs:** Feature B depends on Feature A's implementation
8
-
9
- **When to extract shared infrastructure first:** Multiple features need same utilities/helpers
10
-
11
- **Extract Shared Infrastructure Pattern:**
12
- 1. Create infrastructure PR with only shared code
13
- 2. Get reviewed and MERGE infrastructure first
14
- 3. Launch parallel feature PRs that use merged infrastructure
15
-
16
- ## PR Submission Rules
17
-
18
- **ALWAYS create PRs as DRAFT:** Use `gh pr create --draft` for ALL PRs
19
-
20
- ## Git Golden Rules (NON-NEGOTIABLE)
21
-
22
- 1. **DRAFT BEFORE PUSH**: When pushing ANYTHING to a PR, it MUST be in draft state first
23
- - Before push: `gh pr ready --undo`
24
- - After review approved: `gh pr ready`
25
-
26
- 2. **ONE COMMIT PER REVIEW STAGE**: Each review round gets exactly ONE commit
27
- - Initial feature: 1 commit
28
- - After review #1: 2 commits (initial + review #1 fixes)
29
- - After review #2: 3 commits (initial + review #1 fixes + review #2 fixes)
30
- - NEVER squash multiple review stages into one commit
31
- - NEVER have multiple commits for the same review stage
32
-
33
- ## Never Commit Working Documents or Images
34
-
35
- **NEVER commit these files to the repo:**
36
-
37
- | Pattern | Reason |
38
- |---------|--------|
39
- | `docs/plans/*.md` | Working documents for planning, not repo content |
40
- | `*.plan.md` | Temporary planning files |
41
- | `SESSION_STATE.md` | Local session state |
42
- | `*.png *.jpg *.jpeg *.gif *.webp *.avif *.svg *.ico` | Images go to external storage, not GitHub |
1
+ # Git-workflow pointer: canonical git workflow policy lives in `~/.claude/system-prompts/software-engineer.xml` under `<git_workflow>`.
@@ -1,23 +1 @@
1
- # Parallel Tool Calls
2
-
3
- Source: [Anthropic - Parallel Tool Calling](https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/claude-prompting-best-practices#optimize-parallel-tool-calling)
4
-
5
- <use_parallel_tool_calls>
6
- When multiple tool calls have no dependencies between them, make all independent calls in a single response. Only sequence calls when a later call needs an earlier call's result.
7
- </use_parallel_tool_calls>
8
-
9
- ## Examples
10
-
11
- - Reading 3 files: call all 3 Read operations at once.
12
- - Running independent searches: launch all Grep/Glob calls simultaneously.
13
- - Checking git status + reading a config file: both in one response.
14
- - Reading a file, then editing based on its content: sequential (edit depends on read result).
15
-
16
- ## Guard rails
17
-
18
- - Use real parameter values only. Do not guess or use placeholders to force parallelism.
19
- - If you are unsure whether calls are independent, run them sequentially.
20
-
21
- ## Why
22
-
23
- Explicit reinforcement of parallel calling boosts compliance to near 100%. Sequential calls for independent operations waste time and round-trips for the user.
1
+ # Parallel-tools pointer: canonical policy lives in `~/.claude/system-prompts/software-engineer.xml` under `<agent_workflow>`.
@@ -1,23 +1 @@
1
- # Research Mode (Global)
2
-
3
- Three anti-hallucination constraints are ALWAYS active.
4
-
5
- Source: [Anthropic - Reduce Hallucinations](https://docs.anthropic.com/en/docs/test-and-evaluate/strengthen-guardrails/reduce-hallucinations)
6
-
7
- ## 1. Say "I don't know"
8
- If you don't have a credible source for a claim, say so. Don't guess. Don't infer. "I don't have data on this" is always a valid answer.
9
-
10
- ## 2. Verify with citations
11
- Every recommendation, claim, or piece of advice must cite a specific source:
12
- - A file in the current project
13
- - An external source found via web search (with URL)
14
- - A named expert, paper, or researcher
15
- - Official documentation
16
-
17
- If you generate a claim and cannot find a supporting source, retract it. Do not present it.
18
-
19
- ## 3. Direct quotes for factual grounding
20
- When working from documents, extract the actual text first before analyzing. Ground your response in word-for-word quotes, not paraphrased summaries. Reference the quote when making your point.
21
-
22
- ## Exceptions
23
- Creative thinking, brainstorming, and novel ideas don't require citation. You can synthesize across sources to reach new conclusions, but the inputs must be grounded.
1
+ # Research-mode pointer: canonical policy lives in `~/.claude/system-prompts/software-engineer.xml` under `<investigation>`.
@@ -1,28 +1 @@
1
- # Right-Sized Engineering
2
-
3
- **Build it right, but build it simple.** Good engineering principles at the appropriate scale.
4
-
5
- ## Always Do
6
- - Extract constants and configuration (no hardcoding)
7
- - Create reusable functions (no copy-paste)
8
- - Use proper error handling
9
- - Follow DRY from the start
10
- - Single responsibility per function
11
-
12
- ## Never Do (Solo Scale)
13
- - Abstract base classes for single implementations
14
- - Dependency injection frameworks
15
- - Complex patterns (CQRS, microservices)
16
- - Multiple inheritance hierarchies
17
- - Over-abstracted interfaces
18
-
19
- ## Complexity Budget
20
-
21
- **State BEFORE implementation:** Files (target 1-2, max 3), Lines (~50-300), Checkpoints ("Is this MINIMUM?", "Fewer files?", "Functions vs classes?")
22
-
23
- ## YAGNI for API Surface
24
-
25
- **Don't expose optional parameters until they're actually used.**
26
-
27
- If a value will always be a constant for now, use the constant internally.
28
- Only add the parameter when callers actually need to vary it.
1
+ # Right-sized-engineering pointer: canonical policy lives in `~/.claude/system-prompts/software-engineer.xml` under `<scope_discipline>`.
@@ -0,0 +1 @@
1
+ # Self-contained-docs pointer: canonical policy lives in `~/.claude/system-prompts/software-engineer.xml` under `<documentation>`.
@@ -0,0 +1 @@
1
+ # Vault-context pointer: canonical policy lives in `~/.claude/system-prompts/software-engineer.xml` under `<obsidian_vault>`.
@@ -0,0 +1 @@
1
+ # Verify-before-asking pointer: canonical policy lives in `~/.claude/system-prompts/software-engineer.xml` under `<investigation>`.
@@ -0,0 +1,22 @@
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())
@@ -0,0 +1,13 @@
1
+ """Sync Claude ~/.claude rules and docs into Cursor .cursor layout."""
2
+
3
+ from sync_to_cursor.config import MAX_RULE_BODY_LINES
4
+ from sync_to_cursor.canonical_docs import sync_canonical_docs as _sync_canonical_docs
5
+ from sync_to_cursor.rules import _limit_lines, merge_code_standards, merge_test_quality
6
+
7
+ __all__ = [
8
+ "MAX_RULE_BODY_LINES",
9
+ "_limit_lines",
10
+ "_sync_canonical_docs",
11
+ "merge_code_standards",
12
+ "merge_test_quality",
13
+ ]
@@ -0,0 +1,66 @@
1
+ """Copy and verify canonical docs under .cursor/docs/."""
2
+
3
+ import shutil
4
+ from pathlib import Path
5
+
6
+ from sync_to_cursor.config import CANONICAL_DOC_FILES
7
+ from sync_to_cursor.hashing import sha256_bytes
8
+
9
+
10
+ def sync_canonical_docs(
11
+ claude: Path,
12
+ cursor: Path,
13
+ dry_run: bool,
14
+ quiet: bool,
15
+ ) -> dict:
16
+ docs_out = cursor / "docs"
17
+ if not dry_run:
18
+ docs_out.mkdir(parents=True, exist_ok=True)
19
+ new_docs: dict = {}
20
+ for name in CANONICAL_DOC_FILES:
21
+ src = claude / "docs" / name
22
+ dst = docs_out / name
23
+ if not src.is_file():
24
+ if dst.is_file():
25
+ if not dry_run:
26
+ dst.unlink()
27
+ if not quiet:
28
+ print(f"WARN docs/{name} (source removed — deleted stale copy at {dst})")
29
+ elif not quiet:
30
+ print(f"WARN docs/{name} (missing source: {src})")
31
+ continue
32
+ key = f"docs/{name}"
33
+ src_hash = sha256_bytes(src.read_bytes())
34
+ if dry_run:
35
+ if dst.is_file():
36
+ out_hash = sha256_bytes(dst.read_bytes())
37
+ else:
38
+ out_hash = ""
39
+ else:
40
+ shutil.copy2(src, dst)
41
+ out_hash = sha256_bytes(dst.read_bytes())
42
+ new_docs[key] = {"sources_hash": src_hash, "output_hash": out_hash}
43
+ return new_docs
44
+
45
+
46
+ def check_canonical_docs(claude: Path, cursor: Path, docs_entries: dict) -> bool:
47
+ for name in CANONICAL_DOC_FILES:
48
+ key = f"docs/{name}"
49
+ src = claude / "docs" / name
50
+ dst = cursor / "docs" / name
51
+ if not src.is_file():
52
+ if key in docs_entries:
53
+ return False
54
+ continue
55
+ if not dst.is_file():
56
+ return False
57
+ src_hash = sha256_bytes(src.read_bytes())
58
+ dst_hash = sha256_bytes(dst.read_bytes())
59
+ prev = docs_entries.get(key)
60
+ if not prev:
61
+ return False
62
+ if prev.get("sources_hash") != src_hash:
63
+ return False
64
+ if prev.get("output_hash") != dst_hash:
65
+ return False
66
+ return True
@@ -0,0 +1,5 @@
1
+ """Shared configuration for the sync-to-cursor package."""
2
+
3
+ GENERATOR_VERSION: str = "1.2.3"
4
+ CANONICAL_DOC_FILES: tuple[str, ...] = ("CODE_RULES.md", "TEST_QUALITY.md")
5
+ MAX_RULE_BODY_LINES: int = 50
@@ -0,0 +1,194 @@
1
+ """Sync Claude rules to Cursor .mdc files and manifest."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import sys
8
+ from datetime import datetime, timezone
9
+ from pathlib import Path
10
+
11
+ from sync_to_cursor.canonical_docs import check_canonical_docs, sync_canonical_docs
12
+ from sync_to_cursor.config import GENERATOR_VERSION
13
+ from sync_to_cursor.hashing import sha256_bytes
14
+ from sync_to_cursor.paths import llm_layout_paths
15
+ from sync_to_cursor.rules import RuleMapping, _full_mdc, apply_transform, build_mappings
16
+
17
+
18
+ def _load_manifest(manifest_path: Path) -> dict:
19
+ if not manifest_path.is_file():
20
+ return {}
21
+ try:
22
+ return json.loads(manifest_path.read_text(encoding="utf-8"))
23
+ except json.JSONDecodeError:
24
+ return {}
25
+
26
+
27
+ def _sources_hash(paths: tuple[Path, ...]) -> str:
28
+ return sha256_bytes(b"\x00".join(p.read_bytes() for p in paths))
29
+
30
+
31
+ def _run_check(
32
+ mappings: tuple[RuleMapping, ...],
33
+ out_dir: Path,
34
+ entries_meta: dict,
35
+ claude: Path,
36
+ cursor: Path,
37
+ docs_entries_meta: dict,
38
+ ) -> int:
39
+ for each_mapping in mappings:
40
+ key = f"rules/{each_mapping.output_name}"
41
+ out_path = out_dir / each_mapping.output_name
42
+ missing = [source for source in each_mapping.sources if not source.is_file()]
43
+ if missing:
44
+ if each_mapping.always_apply:
45
+ return 1
46
+ continue
47
+ src_hash = _sources_hash(each_mapping.sources)
48
+ prev = entries_meta.get(key, {})
49
+ if src_hash != prev.get("sources_hash"):
50
+ return 1
51
+ prev_out = prev.get("output_hash", "")
52
+ if out_path.is_file() and prev_out:
53
+ if sha256_bytes(out_path.read_bytes()) != prev_out:
54
+ return 1
55
+ elif not out_path.is_file():
56
+ return 1
57
+ if out_dir.is_dir():
58
+ for each_path in out_dir.glob("*.mdc"):
59
+ if each_path.name not in {x.output_name for x in mappings}:
60
+ return 1
61
+ if not check_canonical_docs(claude, cursor, docs_entries_meta):
62
+ return 1
63
+ return 0
64
+
65
+
66
+ def _sync_rules(
67
+ mappings: tuple[RuleMapping, ...],
68
+ out_dir: Path,
69
+ entries_meta: dict,
70
+ *,
71
+ force: bool,
72
+ dry_run: bool,
73
+ quiet: bool,
74
+ cursor: Path,
75
+ ) -> tuple[dict, dict]:
76
+ summary: dict = {"skip": 0, "update": 0, "tampered": 0, "warn": 0, "orphan": 0}
77
+ new_entries: dict = {}
78
+ write_allowed = not dry_run
79
+
80
+ for each_mapping in mappings:
81
+ key = f"rules/{each_mapping.output_name}"
82
+ out_path = out_dir / each_mapping.output_name
83
+ missing = [source for source in each_mapping.sources if not source.is_file()]
84
+ if missing:
85
+ summary["warn"] += 1
86
+ if not quiet:
87
+ print(f"WARN rules/{each_mapping.output_name} (missing source: {missing})")
88
+ continue
89
+
90
+ src_hash = _sources_hash(each_mapping.sources)
91
+ prev = entries_meta.get(key, {})
92
+ prev_src = prev.get("sources_hash", "")
93
+ prev_out = prev.get("output_hash", "")
94
+
95
+ if (
96
+ not force
97
+ and out_path.is_file()
98
+ and prev_src == src_hash
99
+ and prev_out
100
+ and sha256_bytes(out_path.read_bytes()) == prev_out
101
+ ):
102
+ summary["skip"] += 1
103
+ new_entries[key] = {"sources_hash": src_hash, "output_hash": prev_out}
104
+ continue
105
+
106
+ if (
107
+ not force
108
+ and out_path.is_file()
109
+ and prev_src == src_hash
110
+ and prev_out
111
+ and sha256_bytes(out_path.read_bytes()) != prev_out
112
+ ):
113
+ summary["tampered"] += 1
114
+ if not quiet:
115
+ print(f"TAMPERED rules/{each_mapping.output_name} (manual edit; regenerating)")
116
+
117
+ summary["update"] += 1
118
+ if not quiet:
119
+ print(f"UPDATE rules/{each_mapping.output_name}")
120
+
121
+ body = apply_transform(
122
+ each_mapping.transform,
123
+ each_mapping.sources,
124
+ strip_leading_frontmatter=each_mapping.strip_leading_frontmatter,
125
+ )
126
+ full = _full_mdc(each_mapping, body)
127
+ out_hash = sha256_bytes(full.encode("utf-8"))
128
+ new_entries[key] = {"sources_hash": src_hash, "output_hash": out_hash}
129
+
130
+ if write_allowed:
131
+ out_path.write_text(full, encoding="utf-8", newline="\n")
132
+
133
+ expected = {each_mapping.output_name for each_mapping in mappings}
134
+ for each_path in out_dir.glob("*.mdc"):
135
+ if each_path.name not in expected:
136
+ summary["orphan"] += 1
137
+ if not quiet:
138
+ print(f"WARN {each_path.relative_to(cursor)} (orphan — not generated by this tool)")
139
+
140
+ return summary, new_entries
141
+
142
+
143
+ def run(argv: list[str] | None = None) -> int:
144
+ argument_parser = argparse.ArgumentParser(description="Sync Claude rules to Cursor .mdc files")
145
+ argument_parser.add_argument("--force", action="store_true", help="Regenerate all outputs")
146
+ argument_parser.add_argument("--dry-run", action="store_true", help="Print actions only")
147
+ argument_parser.add_argument("--check", action="store_true", help="Exit 1 if anything stale")
148
+ argument_parser.add_argument("--quiet", action="store_true", help="Minimal output when up to date")
149
+ args = argument_parser.parse_args(argv)
150
+
151
+ claude, cursor, out_dir, manifest_path = llm_layout_paths()
152
+ mappings = build_mappings(claude)
153
+ old_manifest = _load_manifest(manifest_path)
154
+ entries_meta: dict = old_manifest.get("entries", {})
155
+ docs_entries_meta: dict = old_manifest.get("docs_entries", {})
156
+
157
+ if args.check:
158
+ return _run_check(mappings, out_dir, entries_meta, claude, cursor, docs_entries_meta)
159
+
160
+ if not args.dry_run:
161
+ out_dir.mkdir(parents=True, exist_ok=True)
162
+ new_docs_entries = sync_canonical_docs(claude, cursor, args.dry_run, args.quiet)
163
+
164
+ summary, new_entries = _sync_rules(
165
+ mappings,
166
+ out_dir,
167
+ entries_meta,
168
+ force=args.force,
169
+ dry_run=args.dry_run,
170
+ quiet=args.quiet,
171
+ cursor=cursor,
172
+ )
173
+
174
+ manifest_out = {
175
+ "generated_at": datetime.now(timezone.utc).isoformat(),
176
+ "generator_version": GENERATOR_VERSION,
177
+ "entries": new_entries,
178
+ "docs_entries": new_docs_entries,
179
+ }
180
+ if not args.dry_run:
181
+ manifest_path.write_text(json.dumps(manifest_out, indent=2) + "\n", encoding="utf-8")
182
+
183
+ if (
184
+ not args.quiet
185
+ and summary["skip"]
186
+ and not any((summary["update"], summary["tampered"], summary["warn"], summary["orphan"]))
187
+ ):
188
+ print(f"SKIP all {summary['skip']} rule(s) unchanged")
189
+
190
+ return 0
191
+
192
+
193
+ def main() -> int:
194
+ return run(sys.argv[1:])
@@ -0,0 +1,7 @@
1
+ """Content hashing for manifest entries."""
2
+
3
+ import hashlib
4
+
5
+
6
+ def sha256_bytes(content_bytes: bytes) -> str:
7
+ return hashlib.sha256(content_bytes).hexdigest()
@@ -0,0 +1,18 @@
1
+ """Resolve Claude / Cursor layout paths (LLM_SETTINGS_ROOT or home)."""
2
+
3
+ import os
4
+ from pathlib import Path
5
+
6
+
7
+ def llm_layout_paths() -> tuple[Path, Path, Path, Path]:
8
+ """Return (claude_dir, cursor_dir, rules_out_dir, manifest_path)."""
9
+ raw = os.environ.get("LLM_SETTINGS_ROOT", "").strip()
10
+ if raw:
11
+ base = Path(raw).expanduser().resolve()
12
+ claude = base / ".claude"
13
+ cursor = base / ".cursor"
14
+ else:
15
+ home = Path.home()
16
+ claude = home / ".claude"
17
+ cursor = home / ".cursor"
18
+ return claude, cursor, cursor / "rules", cursor / ".sync-manifest.json"