From 547fd507ad4aeab0b89fdfadf799b6b3e6917674 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabi=C3=A1n=20Silva=20Ortiz?= Date: Wed, 25 Feb 2026 23:09:55 -0300 Subject: [PATCH] fix: prepend YAML frontmatter to Cursor .mdc files for auto-inclusion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- scripts/bash/update-agent-context.sh | 26 +- scripts/powershell/update-agent-context.ps1 | 12 + tests/test_cursor_frontmatter.py | 264 ++++++++++++++++++++ 3 files changed, 299 insertions(+), 3 deletions(-) create mode 100644 tests/test_cursor_frontmatter.py diff --git a/scripts/bash/update-agent-context.sh b/scripts/bash/update-agent-context.sh index a33ea5cde..e0392dbb1 100644 --- a/scripts/bash/update-agent-context.sh +++ b/scripts/bash/update-agent-context.sh @@ -351,10 +351,19 @@ create_new_agent_file() { # Convert \n sequences to actual newlines newline=$(printf '\n') sed -i.bak2 "s/\\\\n/${newline}/g" "$temp_file" - + # Clean up backup files rm -f "$temp_file.bak" "$temp_file.bak2" - + + # Prepend Cursor frontmatter for .mdc files so rules are auto-included + if [[ "$target_file" == *.mdc ]]; then + local frontmatter_file + frontmatter_file=$(mktemp) || return 1 + printf '%s\n' "---" "description: Project Development Guidelines" "globs: [\"**/*\"]" "alwaysApply: true" "---" "" > "$frontmatter_file" + cat "$temp_file" >> "$frontmatter_file" + mv "$frontmatter_file" "$temp_file" + fi + return 0 } @@ -492,13 +501,24 @@ update_existing_agent_file() { changes_entries_added=true fi + # Ensure Cursor .mdc files have YAML frontmatter for auto-inclusion + if [[ "$target_file" == *.mdc ]]; then + if ! head -1 "$temp_file" | grep -q '^---$'; then + local frontmatter_file + frontmatter_file=$(mktemp) || { rm -f "$temp_file"; return 1; } + printf '%s\n' "---" "description: Project Development Guidelines" "globs: [\"**/*\"]" "alwaysApply: true" "---" "" > "$frontmatter_file" + cat "$temp_file" >> "$frontmatter_file" + mv "$frontmatter_file" "$temp_file" + fi + fi + # Move temp file to target atomically if ! mv "$temp_file" "$target_file"; then log_error "Failed to update target file" rm -f "$temp_file" return 1 fi - + return 0 } #============================================================================== diff --git a/scripts/powershell/update-agent-context.ps1 b/scripts/powershell/update-agent-context.ps1 index 61718e96c..c293e6831 100644 --- a/scripts/powershell/update-agent-context.ps1 +++ b/scripts/powershell/update-agent-context.ps1 @@ -258,6 +258,12 @@ function New-AgentFile { # Convert literal \n sequences introduced by Escape to real newlines $content = $content -replace '\\n',[Environment]::NewLine + # Prepend Cursor frontmatter for .mdc files so rules are auto-included + if ($TargetFile -match '\.mdc$') { + $frontmatter = @('---','description: Project Development Guidelines','globs: ["**/*"]','alwaysApply: true','---','') -join [Environment]::NewLine + $content = $frontmatter + [Environment]::NewLine + $content + } + $parent = Split-Path -Parent $TargetFile if (-not (Test-Path $parent)) { New-Item -ItemType Directory -Path $parent | Out-Null } Set-Content -LiteralPath $TargetFile -Value $content -NoNewline -Encoding utf8 @@ -334,6 +340,12 @@ function Update-ExistingAgentFile { $newTechEntries | ForEach-Object { $output.Add($_) } } + # Ensure Cursor .mdc files have YAML frontmatter for auto-inclusion + if ($TargetFile -match '\.mdc$' -and $output.Count -gt 0 -and $output[0] -ne '---') { + $frontmatter = @('---','description: Project Development Guidelines','globs: ["**/*"]','alwaysApply: true','---','') + $output.InsertRange(0, $frontmatter) + } + Set-Content -LiteralPath $TargetFile -Value ($output -join [Environment]::NewLine) -Encoding utf8 return $true } diff --git a/tests/test_cursor_frontmatter.py b/tests/test_cursor_frontmatter.py new file mode 100644 index 000000000..c01c65313 --- /dev/null +++ b/tests/test_cursor_frontmatter.py @@ -0,0 +1,264 @@ +""" +Tests for Cursor .mdc frontmatter generation (issue #669). + +Verifies that update-agent-context.sh properly prepends YAML frontmatter +to .mdc files so that Cursor IDE auto-includes the rules. +""" + +import os +import shutil +import subprocess +import tempfile +import textwrap + +import pytest + +SCRIPT_PATH = os.path.join( + os.path.dirname(__file__), + os.pardir, + "scripts", + "bash", + "update-agent-context.sh", +) + +EXPECTED_FRONTMATTER_LINES = [ + "---", + "description: Project Development Guidelines", + 'globs: ["**/*"]', + "alwaysApply: true", + "---", +] + +requires_git = pytest.mark.skipif( + shutil.which("git") is None, + reason="git is not installed", +) + + +class TestScriptFrontmatterPattern: + """Static analysis — no git required.""" + + def test_create_new_has_mdc_frontmatter_logic(self): + """create_new_agent_file() must contain .mdc frontmatter logic.""" + with open(SCRIPT_PATH) as f: + content = f.read() + assert 'if [[ "$target_file" == *.mdc ]]' in content + assert "alwaysApply: true" in content + + def test_update_existing_has_mdc_frontmatter_logic(self): + """update_existing_agent_file() must also handle .mdc frontmatter.""" + with open(SCRIPT_PATH) as f: + content = f.read() + # There should be two occurrences of the .mdc check — one per function + occurrences = content.count('if [[ "$target_file" == *.mdc ]]') + assert occurrences >= 2, ( + f"Expected at least 2 .mdc frontmatter checks, found {occurrences}" + ) + + def test_powershell_script_has_mdc_frontmatter_logic(self): + """PowerShell script must also handle .mdc frontmatter.""" + ps_path = os.path.join( + os.path.dirname(__file__), + os.pardir, + "scripts", + "powershell", + "update-agent-context.ps1", + ) + with open(ps_path) as f: + content = f.read() + assert "alwaysApply: true" in content + occurrences = content.count(r"\.mdc$") + assert occurrences >= 2, ( + f"Expected at least 2 .mdc frontmatter checks in PS script, found {occurrences}" + ) + + +@requires_git +class TestCursorFrontmatterIntegration: + """Integration tests using a real git repo.""" + + @pytest.fixture + def git_repo(self, tmp_path): + """Create a minimal git repo with the spec-kit structure.""" + repo = tmp_path / "repo" + repo.mkdir() + + # Init git repo + subprocess.run( + ["git", "init"], cwd=str(repo), capture_output=True, check=True + ) + subprocess.run( + ["git", "config", "user.email", "test@test.com"], + cwd=str(repo), + capture_output=True, + check=True, + ) + subprocess.run( + ["git", "config", "user.name", "Test"], + cwd=str(repo), + capture_output=True, + check=True, + ) + + # Create .specify dir with config + specify_dir = repo / ".specify" + specify_dir.mkdir() + (specify_dir / "config.yaml").write_text( + textwrap.dedent("""\ + project_type: webapp + language: python + framework: fastapi + database: N/A + """) + ) + + # Create template + templates_dir = specify_dir / "templates" + templates_dir.mkdir() + (templates_dir / "agent-file-template.md").write_text( + "# [PROJECT NAME] Development Guidelines\n\n" + "Auto-generated from all feature plans. Last updated: [DATE]\n\n" + "## Active Technologies\n\n" + "[EXTRACTED FROM ALL PLAN.MD FILES]\n\n" + "## Project Structure\n\n" + "[ACTUAL STRUCTURE FROM PLANS]\n\n" + "## Development Commands\n\n" + "[ONLY COMMANDS FOR ACTIVE TECHNOLOGIES]\n\n" + "## Coding Conventions\n\n" + "[LANGUAGE-SPECIFIC, ONLY FOR LANGUAGES IN USE]\n\n" + "## Recent Changes\n\n" + "[LAST 3 FEATURES AND WHAT THEY ADDED]\n" + ) + + # Create initial commit + subprocess.run( + ["git", "add", "-A"], cwd=str(repo), capture_output=True, check=True + ) + subprocess.run( + ["git", "commit", "-m", "init"], + cwd=str(repo), + capture_output=True, + check=True, + ) + + # Create a feature branch so CURRENT_BRANCH detection works + subprocess.run( + ["git", "checkout", "-b", "001-test-feature"], + cwd=str(repo), + capture_output=True, + check=True, + ) + + # Create a spec so the script detects the feature + spec_dir = repo / "specs" / "001-test-feature" + spec_dir.mkdir(parents=True) + (spec_dir / "plan.md").write_text( + "# Test Feature Plan\n\n" + "## Technology Stack\n\n" + "- Language: Python\n" + "- Framework: FastAPI\n" + ) + + return repo + + def _run_update(self, repo, agent_type="cursor-agent"): + """Run update-agent-context.sh for a specific agent type.""" + script = os.path.abspath(SCRIPT_PATH) + result = subprocess.run( + ["bash", script, agent_type], + cwd=str(repo), + capture_output=True, + text=True, + timeout=30, + ) + return result + + def test_new_mdc_file_has_frontmatter(self, git_repo): + """Creating a new .mdc file must include YAML frontmatter.""" + result = self._run_update(git_repo) + assert result.returncode == 0, f"Script failed: {result.stderr}" + + mdc_file = git_repo / ".cursor" / "rules" / "specify-rules.mdc" + assert mdc_file.exists(), "Cursor .mdc file was not created" + + content = mdc_file.read_text() + lines = content.splitlines() + + # First line must be the opening --- + assert lines[0] == "---", f"Expected frontmatter start, got: {lines[0]}" + + # Check all frontmatter lines are present + for expected in EXPECTED_FRONTMATTER_LINES: + assert expected in content, f"Missing frontmatter line: {expected}" + + # Content after frontmatter should be the template content + assert "Development Guidelines" in content + + def test_existing_mdc_without_frontmatter_gets_it_added(self, git_repo): + """Updating an existing .mdc file that lacks frontmatter must add it.""" + # First, create the file WITHOUT frontmatter (simulating pre-fix state) + cursor_dir = git_repo / ".cursor" / "rules" + cursor_dir.mkdir(parents=True, exist_ok=True) + mdc_file = cursor_dir / "specify-rules.mdc" + mdc_file.write_text( + "# repo Development Guidelines\n\n" + "Auto-generated from all feature plans. Last updated: 2025-01-01\n\n" + "## Active Technologies\n\n" + "- Python + FastAPI (main)\n\n" + "## Recent Changes\n\n" + "- main: Added Python + FastAPI\n" + ) + + result = self._run_update(git_repo) + assert result.returncode == 0, f"Script failed: {result.stderr}" + + content = mdc_file.read_text() + lines = content.splitlines() + + assert lines[0] == "---", f"Expected frontmatter start, got: {lines[0]}" + for expected in EXPECTED_FRONTMATTER_LINES: + assert expected in content, f"Missing frontmatter line: {expected}" + + def test_existing_mdc_with_frontmatter_not_duplicated(self, git_repo): + """Updating an .mdc file that already has frontmatter must not duplicate it.""" + cursor_dir = git_repo / ".cursor" / "rules" + cursor_dir.mkdir(parents=True, exist_ok=True) + mdc_file = cursor_dir / "specify-rules.mdc" + + frontmatter = ( + "---\n" + "description: Project Development Guidelines\n" + 'globs: ["**/*"]\n' + "alwaysApply: true\n" + "---\n\n" + ) + body = ( + "# repo Development Guidelines\n\n" + "Auto-generated from all feature plans. Last updated: 2025-01-01\n\n" + "## Active Technologies\n\n" + "- Python + FastAPI (main)\n\n" + "## Recent Changes\n\n" + "- main: Added Python + FastAPI\n" + ) + mdc_file.write_text(frontmatter + body) + + result = self._run_update(git_repo) + assert result.returncode == 0, f"Script failed: {result.stderr}" + + content = mdc_file.read_text() + # Count occurrences of the frontmatter delimiter + assert content.count("alwaysApply: true") == 1, ( + "Frontmatter was duplicated" + ) + + def test_non_mdc_file_has_no_frontmatter(self, git_repo): + """Non-.mdc agent files (e.g., Claude) must NOT get frontmatter.""" + result = self._run_update(git_repo, agent_type="claude") + assert result.returncode == 0, f"Script failed: {result.stderr}" + + claude_file = git_repo / ".claude" / "CLAUDE.md" + if claude_file.exists(): + content = claude_file.read_text() + assert not content.startswith("---"), ( + "Non-mdc file should not have frontmatter" + )