Skip to content

Commit b55d00b

Browse files
fsilvaortizclaude
andauthored
fix: prepend YAML frontmatter to Cursor .mdc files (#1699)
* fix: prepend YAML frontmatter to Cursor .mdc files for auto-inclusion Cursor IDE requires YAML frontmatter with `alwaysApply: true` in .mdc rule files for them to be automatically loaded. Without this frontmatter, users must manually configure glob patterns for the rules to take effect. This fix adds frontmatter generation to both the bash and PowerShell update-agent-context scripts, handling three scenarios: - New .mdc file creation (frontmatter prepended after template processing) - Existing .mdc file update without frontmatter (frontmatter added) - Existing .mdc file with frontmatter (no duplication) Closes #669 🤖 Generated with [Claude Code](https://claude.com/code) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: address Copilot review suggestions - Handle CRLF line endings in frontmatter detection (grep '^---' instead of '^---$') - Fix double blank line after frontmatter in PowerShell New-AgentFile - Remove unused tempfile import from tests - Add encoding="utf-8" to all open() calls for cross-platform safety Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 525eae7 commit b55d00b

File tree

3 files changed

+298
-3
lines changed

3 files changed

+298
-3
lines changed

scripts/bash/update-agent-context.sh

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -351,10 +351,19 @@ create_new_agent_file() {
351351
# Convert \n sequences to actual newlines
352352
newline=$(printf '\n')
353353
sed -i.bak2 "s/\\\\n/${newline}/g" "$temp_file"
354-
354+
355355
# Clean up backup files
356356
rm -f "$temp_file.bak" "$temp_file.bak2"
357-
357+
358+
# Prepend Cursor frontmatter for .mdc files so rules are auto-included
359+
if [[ "$target_file" == *.mdc ]]; then
360+
local frontmatter_file
361+
frontmatter_file=$(mktemp) || return 1
362+
printf '%s\n' "---" "description: Project Development Guidelines" "globs: [\"**/*\"]" "alwaysApply: true" "---" "" > "$frontmatter_file"
363+
cat "$temp_file" >> "$frontmatter_file"
364+
mv "$frontmatter_file" "$temp_file"
365+
fi
366+
358367
return 0
359368
}
360369

@@ -492,13 +501,24 @@ update_existing_agent_file() {
492501
changes_entries_added=true
493502
fi
494503

504+
# Ensure Cursor .mdc files have YAML frontmatter for auto-inclusion
505+
if [[ "$target_file" == *.mdc ]]; then
506+
if ! head -1 "$temp_file" | grep -q '^---'; then
507+
local frontmatter_file
508+
frontmatter_file=$(mktemp) || { rm -f "$temp_file"; return 1; }
509+
printf '%s\n' "---" "description: Project Development Guidelines" "globs: [\"**/*\"]" "alwaysApply: true" "---" "" > "$frontmatter_file"
510+
cat "$temp_file" >> "$frontmatter_file"
511+
mv "$frontmatter_file" "$temp_file"
512+
fi
513+
fi
514+
495515
# Move temp file to target atomically
496516
if ! mv "$temp_file" "$target_file"; then
497517
log_error "Failed to update target file"
498518
rm -f "$temp_file"
499519
return 1
500520
fi
501-
521+
502522
return 0
503523
}
504524
#==============================================================================

scripts/powershell/update-agent-context.ps1

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,12 @@ function New-AgentFile {
258258
# Convert literal \n sequences introduced by Escape to real newlines
259259
$content = $content -replace '\\n',[Environment]::NewLine
260260

261+
# Prepend Cursor frontmatter for .mdc files so rules are auto-included
262+
if ($TargetFile -match '\.mdc$') {
263+
$frontmatter = @('---','description: Project Development Guidelines','globs: ["**/*"]','alwaysApply: true','---','') -join [Environment]::NewLine
264+
$content = $frontmatter + $content
265+
}
266+
261267
$parent = Split-Path -Parent $TargetFile
262268
if (-not (Test-Path $parent)) { New-Item -ItemType Directory -Path $parent | Out-Null }
263269
Set-Content -LiteralPath $TargetFile -Value $content -NoNewline -Encoding utf8
@@ -334,6 +340,12 @@ function Update-ExistingAgentFile {
334340
$newTechEntries | ForEach-Object { $output.Add($_) }
335341
}
336342

343+
# Ensure Cursor .mdc files have YAML frontmatter for auto-inclusion
344+
if ($TargetFile -match '\.mdc$' -and $output.Count -gt 0 -and $output[0] -ne '---') {
345+
$frontmatter = @('---','description: Project Development Guidelines','globs: ["**/*"]','alwaysApply: true','---','')
346+
$output.InsertRange(0, $frontmatter)
347+
}
348+
337349
Set-Content -LiteralPath $TargetFile -Value ($output -join [Environment]::NewLine) -Encoding utf8
338350
return $true
339351
}

tests/test_cursor_frontmatter.py

Lines changed: 263 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,263 @@
1+
"""
2+
Tests for Cursor .mdc frontmatter generation (issue #669).
3+
4+
Verifies that update-agent-context.sh properly prepends YAML frontmatter
5+
to .mdc files so that Cursor IDE auto-includes the rules.
6+
"""
7+
8+
import os
9+
import shutil
10+
import subprocess
11+
import textwrap
12+
13+
import pytest
14+
15+
SCRIPT_PATH = os.path.join(
16+
os.path.dirname(__file__),
17+
os.pardir,
18+
"scripts",
19+
"bash",
20+
"update-agent-context.sh",
21+
)
22+
23+
EXPECTED_FRONTMATTER_LINES = [
24+
"---",
25+
"description: Project Development Guidelines",
26+
'globs: ["**/*"]',
27+
"alwaysApply: true",
28+
"---",
29+
]
30+
31+
requires_git = pytest.mark.skipif(
32+
shutil.which("git") is None,
33+
reason="git is not installed",
34+
)
35+
36+
37+
class TestScriptFrontmatterPattern:
38+
"""Static analysis — no git required."""
39+
40+
def test_create_new_has_mdc_frontmatter_logic(self):
41+
"""create_new_agent_file() must contain .mdc frontmatter logic."""
42+
with open(SCRIPT_PATH, encoding="utf-8") as f:
43+
content = f.read()
44+
assert 'if [[ "$target_file" == *.mdc ]]' in content
45+
assert "alwaysApply: true" in content
46+
47+
def test_update_existing_has_mdc_frontmatter_logic(self):
48+
"""update_existing_agent_file() must also handle .mdc frontmatter."""
49+
with open(SCRIPT_PATH, encoding="utf-8") as f:
50+
content = f.read()
51+
# There should be two occurrences of the .mdc check — one per function
52+
occurrences = content.count('if [[ "$target_file" == *.mdc ]]')
53+
assert occurrences >= 2, (
54+
f"Expected at least 2 .mdc frontmatter checks, found {occurrences}"
55+
)
56+
57+
def test_powershell_script_has_mdc_frontmatter_logic(self):
58+
"""PowerShell script must also handle .mdc frontmatter."""
59+
ps_path = os.path.join(
60+
os.path.dirname(__file__),
61+
os.pardir,
62+
"scripts",
63+
"powershell",
64+
"update-agent-context.ps1",
65+
)
66+
with open(ps_path, encoding="utf-8") as f:
67+
content = f.read()
68+
assert "alwaysApply: true" in content
69+
occurrences = content.count(r"\.mdc$")
70+
assert occurrences >= 2, (
71+
f"Expected at least 2 .mdc frontmatter checks in PS script, found {occurrences}"
72+
)
73+
74+
75+
@requires_git
76+
class TestCursorFrontmatterIntegration:
77+
"""Integration tests using a real git repo."""
78+
79+
@pytest.fixture
80+
def git_repo(self, tmp_path):
81+
"""Create a minimal git repo with the spec-kit structure."""
82+
repo = tmp_path / "repo"
83+
repo.mkdir()
84+
85+
# Init git repo
86+
subprocess.run(
87+
["git", "init"], cwd=str(repo), capture_output=True, check=True
88+
)
89+
subprocess.run(
90+
["git", "config", "user.email", "test@test.com"],
91+
cwd=str(repo),
92+
capture_output=True,
93+
check=True,
94+
)
95+
subprocess.run(
96+
["git", "config", "user.name", "Test"],
97+
cwd=str(repo),
98+
capture_output=True,
99+
check=True,
100+
)
101+
102+
# Create .specify dir with config
103+
specify_dir = repo / ".specify"
104+
specify_dir.mkdir()
105+
(specify_dir / "config.yaml").write_text(
106+
textwrap.dedent("""\
107+
project_type: webapp
108+
language: python
109+
framework: fastapi
110+
database: N/A
111+
""")
112+
)
113+
114+
# Create template
115+
templates_dir = specify_dir / "templates"
116+
templates_dir.mkdir()
117+
(templates_dir / "agent-file-template.md").write_text(
118+
"# [PROJECT NAME] Development Guidelines\n\n"
119+
"Auto-generated from all feature plans. Last updated: [DATE]\n\n"
120+
"## Active Technologies\n\n"
121+
"[EXTRACTED FROM ALL PLAN.MD FILES]\n\n"
122+
"## Project Structure\n\n"
123+
"[ACTUAL STRUCTURE FROM PLANS]\n\n"
124+
"## Development Commands\n\n"
125+
"[ONLY COMMANDS FOR ACTIVE TECHNOLOGIES]\n\n"
126+
"## Coding Conventions\n\n"
127+
"[LANGUAGE-SPECIFIC, ONLY FOR LANGUAGES IN USE]\n\n"
128+
"## Recent Changes\n\n"
129+
"[LAST 3 FEATURES AND WHAT THEY ADDED]\n"
130+
)
131+
132+
# Create initial commit
133+
subprocess.run(
134+
["git", "add", "-A"], cwd=str(repo), capture_output=True, check=True
135+
)
136+
subprocess.run(
137+
["git", "commit", "-m", "init"],
138+
cwd=str(repo),
139+
capture_output=True,
140+
check=True,
141+
)
142+
143+
# Create a feature branch so CURRENT_BRANCH detection works
144+
subprocess.run(
145+
["git", "checkout", "-b", "001-test-feature"],
146+
cwd=str(repo),
147+
capture_output=True,
148+
check=True,
149+
)
150+
151+
# Create a spec so the script detects the feature
152+
spec_dir = repo / "specs" / "001-test-feature"
153+
spec_dir.mkdir(parents=True)
154+
(spec_dir / "plan.md").write_text(
155+
"# Test Feature Plan\n\n"
156+
"## Technology Stack\n\n"
157+
"- Language: Python\n"
158+
"- Framework: FastAPI\n"
159+
)
160+
161+
return repo
162+
163+
def _run_update(self, repo, agent_type="cursor-agent"):
164+
"""Run update-agent-context.sh for a specific agent type."""
165+
script = os.path.abspath(SCRIPT_PATH)
166+
result = subprocess.run(
167+
["bash", script, agent_type],
168+
cwd=str(repo),
169+
capture_output=True,
170+
text=True,
171+
timeout=30,
172+
)
173+
return result
174+
175+
def test_new_mdc_file_has_frontmatter(self, git_repo):
176+
"""Creating a new .mdc file must include YAML frontmatter."""
177+
result = self._run_update(git_repo)
178+
assert result.returncode == 0, f"Script failed: {result.stderr}"
179+
180+
mdc_file = git_repo / ".cursor" / "rules" / "specify-rules.mdc"
181+
assert mdc_file.exists(), "Cursor .mdc file was not created"
182+
183+
content = mdc_file.read_text()
184+
lines = content.splitlines()
185+
186+
# First line must be the opening ---
187+
assert lines[0] == "---", f"Expected frontmatter start, got: {lines[0]}"
188+
189+
# Check all frontmatter lines are present
190+
for expected in EXPECTED_FRONTMATTER_LINES:
191+
assert expected in content, f"Missing frontmatter line: {expected}"
192+
193+
# Content after frontmatter should be the template content
194+
assert "Development Guidelines" in content
195+
196+
def test_existing_mdc_without_frontmatter_gets_it_added(self, git_repo):
197+
"""Updating an existing .mdc file that lacks frontmatter must add it."""
198+
# First, create the file WITHOUT frontmatter (simulating pre-fix state)
199+
cursor_dir = git_repo / ".cursor" / "rules"
200+
cursor_dir.mkdir(parents=True, exist_ok=True)
201+
mdc_file = cursor_dir / "specify-rules.mdc"
202+
mdc_file.write_text(
203+
"# repo Development Guidelines\n\n"
204+
"Auto-generated from all feature plans. Last updated: 2025-01-01\n\n"
205+
"## Active Technologies\n\n"
206+
"- Python + FastAPI (main)\n\n"
207+
"## Recent Changes\n\n"
208+
"- main: Added Python + FastAPI\n"
209+
)
210+
211+
result = self._run_update(git_repo)
212+
assert result.returncode == 0, f"Script failed: {result.stderr}"
213+
214+
content = mdc_file.read_text()
215+
lines = content.splitlines()
216+
217+
assert lines[0] == "---", f"Expected frontmatter start, got: {lines[0]}"
218+
for expected in EXPECTED_FRONTMATTER_LINES:
219+
assert expected in content, f"Missing frontmatter line: {expected}"
220+
221+
def test_existing_mdc_with_frontmatter_not_duplicated(self, git_repo):
222+
"""Updating an .mdc file that already has frontmatter must not duplicate it."""
223+
cursor_dir = git_repo / ".cursor" / "rules"
224+
cursor_dir.mkdir(parents=True, exist_ok=True)
225+
mdc_file = cursor_dir / "specify-rules.mdc"
226+
227+
frontmatter = (
228+
"---\n"
229+
"description: Project Development Guidelines\n"
230+
'globs: ["**/*"]\n'
231+
"alwaysApply: true\n"
232+
"---\n\n"
233+
)
234+
body = (
235+
"# repo Development Guidelines\n\n"
236+
"Auto-generated from all feature plans. Last updated: 2025-01-01\n\n"
237+
"## Active Technologies\n\n"
238+
"- Python + FastAPI (main)\n\n"
239+
"## Recent Changes\n\n"
240+
"- main: Added Python + FastAPI\n"
241+
)
242+
mdc_file.write_text(frontmatter + body)
243+
244+
result = self._run_update(git_repo)
245+
assert result.returncode == 0, f"Script failed: {result.stderr}"
246+
247+
content = mdc_file.read_text()
248+
# Count occurrences of the frontmatter delimiter
249+
assert content.count("alwaysApply: true") == 1, (
250+
"Frontmatter was duplicated"
251+
)
252+
253+
def test_non_mdc_file_has_no_frontmatter(self, git_repo):
254+
"""Non-.mdc agent files (e.g., Claude) must NOT get frontmatter."""
255+
result = self._run_update(git_repo, agent_type="claude")
256+
assert result.returncode == 0, f"Script failed: {result.stderr}"
257+
258+
claude_file = git_repo / ".claude" / "CLAUDE.md"
259+
if claude_file.exists():
260+
content = claude_file.read_text()
261+
assert not content.startswith("---"), (
262+
"Non-mdc file should not have frontmatter"
263+
)

0 commit comments

Comments
 (0)