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
package/rules/code-standards.md
CHANGED
|
@@ -1,43 +1 @@
|
|
|
1
|
-
# Code
|
|
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
|
|
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
|
|
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>`.
|
package/rules/git-workflow.md
CHANGED
|
@@ -1,42 +1 @@
|
|
|
1
|
-
# Git
|
|
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>`.
|
package/rules/parallel-tools.md
CHANGED
|
@@ -1,23 +1 @@
|
|
|
1
|
-
# Parallel
|
|
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>`.
|
package/rules/research-mode.md
CHANGED
|
@@ -1,23 +1 @@
|
|
|
1
|
-
# Research
|
|
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-
|
|
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,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,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"
|