Skip to content

Commit 425ec32

Browse files
committed
fix(init): generate root CLAUDE.md for --ai claude
Ensure `specify init --ai claude` creates a minimal root CLAUDE.md pointing to `.specify/memory/constitution.md`, and add a regression test for issue #1983. Made-with: Cursor
1 parent b22f381 commit 425ec32

2 files changed

Lines changed: 82 additions & 0 deletions

File tree

src/specify_cli/__init__.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1461,6 +1461,48 @@ def ensure_constitution_from_template(project_path: Path, tracker: StepTracker |
14611461
console.print(f"[yellow]Warning: Could not initialize constitution: {e}[/yellow]")
14621462

14631463

1464+
def ensure_claude_md(project_path: Path, tracker: StepTracker | None = None) -> None:
1465+
"""Create a minimal root `CLAUDE.md` for Claude Code if missing.
1466+
1467+
Claude Code expects `CLAUDE.md` at the project root; this file acts as a
1468+
bridge to `.specify/memory/constitution.md` (the source of truth).
1469+
"""
1470+
claude_file = project_path / "CLAUDE.md"
1471+
if claude_file.exists():
1472+
if tracker:
1473+
tracker.add("claude-md", "Claude Code role file")
1474+
tracker.skip("claude-md", "existing file preserved")
1475+
return
1476+
1477+
content = (
1478+
"## Claude's Role\n"
1479+
"Read `.specify/memory/constitution.md` first. It is the authoritative source of truth for this project. "
1480+
"Everything in it is non-negotiable.\n\n"
1481+
"## SpecKit Commands\n"
1482+
"- `/speckit.specify` — generate spec\n"
1483+
"- `/speckit.plan` — generate plan\n"
1484+
"- `/speckit.tasks` — generate task list\n"
1485+
"- `/speckit.implement` — execute plan\n\n"
1486+
"## On Ambiguity\n"
1487+
"If a spec is missing, incomplete, or conflicts with the constitution — stop and ask. "
1488+
"Do not infer. Do not proceed.\n\n"
1489+
)
1490+
1491+
try:
1492+
claude_file.write_text(content, encoding="utf-8")
1493+
if tracker:
1494+
tracker.add("claude-md", "Claude Code role file")
1495+
tracker.complete("claude-md", "created")
1496+
else:
1497+
console.print("[cyan]Initialized CLAUDE.md for Claude Code[/cyan]")
1498+
except Exception as e:
1499+
if tracker:
1500+
tracker.add("claude-md", "Claude Code role file")
1501+
tracker.error("claude-md", str(e))
1502+
else:
1503+
console.print(f"[yellow]Warning: Could not create CLAUDE.md: {e}[/yellow]")
1504+
1505+
14641506
INIT_OPTIONS_FILE = ".specify/init-options.json"
14651507

14661508

@@ -2071,6 +2113,8 @@ def init(
20712113
("constitution", "Constitution setup"),
20722114
]:
20732115
tracker.add(key, label)
2116+
if selected_ai == "claude":
2117+
tracker.add("claude-md", "Claude Code role file")
20742118
if ai_skills:
20752119
tracker.add("ai-skills", "Install agent skills")
20762120
for key, label in [
@@ -2137,6 +2181,9 @@ def init(
21372181

21382182
ensure_constitution_from_template(project_path, tracker=tracker)
21392183

2184+
if selected_ai == "claude":
2185+
ensure_claude_md(project_path, tracker=tracker)
2186+
21402187
# Determine skills directory and migrate any legacy Kimi dotted skills.
21412188
migrated_legacy_kimi_skills = 0
21422189
removed_legacy_kimi_skills = 0

tests/test_ai_skills.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -693,6 +693,41 @@ class TestNewProjectCommandSkip:
693693
download_and_extract_template patched to create local fixtures.
694694
"""
695695

696+
@pytest.mark.skipif(
697+
shutil.which("bash") is None or shutil.which("zip") is None,
698+
reason="offline scaffolding requires bash + zip",
699+
)
700+
def test_init_claude_creates_root_CLAUDE_md(self, tmp_path):
701+
from typer.testing import CliRunner
702+
703+
runner = CliRunner()
704+
target = tmp_path / "claude-proj"
705+
706+
result = runner.invoke(
707+
app,
708+
[
709+
"init",
710+
str(target),
711+
"--ai",
712+
"claude",
713+
"--offline",
714+
"--ignore-agent-tools",
715+
"--no-git",
716+
"--script",
717+
"sh",
718+
],
719+
)
720+
721+
assert result.exit_code == 0, result.output
722+
723+
claude_file = target / "CLAUDE.md"
724+
assert claude_file.exists()
725+
726+
content = claude_file.read_text(encoding="utf-8")
727+
assert "## Claude's Role" in content
728+
assert "`.specify/memory/constitution.md`" in content
729+
assert "/speckit.plan" in content
730+
696731
def _fake_extract(self, agent, project_path, **_kwargs):
697732
"""Simulate template extraction: create agent commands dir."""
698733
agent_cfg = AGENT_CONFIG.get(agent, {})

0 commit comments

Comments
 (0)