Skip to content

Commit 89d2ab2

Browse files
committed
fix(init): gate CLAUDE.md on constitution presence
generate CLAUDE.md only when `.specify/memory/constitution.md` exists to avoid misleading guidance, and make the regression test deterministic by patching init scaffolding. Made-with: Cursor
1 parent 425ec32 commit 89d2ab2

2 files changed

Lines changed: 51 additions & 18 deletions

File tree

src/specify_cli/__init__.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1467,13 +1467,23 @@ def ensure_claude_md(project_path: Path, tracker: StepTracker | None = None) ->
14671467
Claude Code expects `CLAUDE.md` at the project root; this file acts as a
14681468
bridge to `.specify/memory/constitution.md` (the source of truth).
14691469
"""
1470+
memory_constitution = project_path / ".specify" / "memory" / "constitution.md"
14701471
claude_file = project_path / "CLAUDE.md"
14711472
if claude_file.exists():
14721473
if tracker:
14731474
tracker.add("claude-md", "Claude Code role file")
14741475
tracker.skip("claude-md", "existing file preserved")
14751476
return
14761477

1478+
if not memory_constitution.exists():
1479+
detail = "constitution missing"
1480+
if tracker:
1481+
tracker.add("claude-md", "Claude Code role file")
1482+
tracker.skip("claude-md", detail)
1483+
else:
1484+
console.print(f"[yellow]Warning:[/yellow] Not creating CLAUDE.md because {memory_constitution} is missing")
1485+
return
1486+
14771487
content = (
14781488
"## Claude's Role\n"
14791489
"Read `.specify/memory/constitution.md` first. It is the authoritative source of truth for this project. "

tests/test_ai_skills.py

Lines changed: 41 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@
2929
DEFAULT_SKILLS_DIR,
3030
SKILL_DESCRIPTIONS,
3131
AGENT_CONFIG,
32+
StepTracker,
3233
app,
34+
ensure_claude_md,
3335
)
3436

3537

@@ -693,30 +695,39 @@ class TestNewProjectCommandSkip:
693695
download_and_extract_template patched to create local fixtures.
694696
"""
695697

696-
@pytest.mark.skipif(
697-
shutil.which("bash") is None or shutil.which("zip") is None,
698-
reason="offline scaffolding requires bash + zip",
699-
)
700698
def test_init_claude_creates_root_CLAUDE_md(self, tmp_path):
701699
from typer.testing import CliRunner
702700

703701
runner = CliRunner()
704702
target = tmp_path / "claude-proj"
705703

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-
)
704+
def fake_download(project_path, *args, **kwargs):
705+
# Minimal scaffold required for ensure_constitution_from_template()
706+
# and ensure_claude_md() to succeed deterministically.
707+
templates_dir = project_path / ".specify" / "templates"
708+
templates_dir.mkdir(parents=True, exist_ok=True)
709+
(templates_dir / "constitution-template.md").write_text(
710+
"# Constitution\n\nNon-negotiable rules.\n",
711+
encoding="utf-8",
712+
)
713+
714+
with patch("specify_cli.download_and_extract_template", side_effect=fake_download), \
715+
patch("specify_cli.ensure_executable_scripts"), \
716+
patch("specify_cli.is_git_repo", return_value=False), \
717+
patch("specify_cli.shutil.which", return_value="/usr/bin/git"):
718+
result = runner.invoke(
719+
app,
720+
[
721+
"init",
722+
str(target),
723+
"--ai",
724+
"claude",
725+
"--ignore-agent-tools",
726+
"--no-git",
727+
"--script",
728+
"sh",
729+
],
730+
)
720731

721732
assert result.exit_code == 0, result.output
722733

@@ -728,6 +739,18 @@ def test_init_claude_creates_root_CLAUDE_md(self, tmp_path):
728739
assert "`.specify/memory/constitution.md`" in content
729740
assert "/speckit.plan" in content
730741

742+
def test_ensure_claude_md_skips_when_constitution_missing(self, tmp_path):
743+
project = tmp_path / "proj"
744+
project.mkdir()
745+
746+
tracker = StepTracker("t")
747+
ensure_claude_md(project, tracker=tracker)
748+
749+
assert not (project / "CLAUDE.md").exists()
750+
step = next(s for s in tracker.steps if s["key"] == "claude-md")
751+
assert step["status"] == "skipped"
752+
assert "constitution missing" in step["detail"]
753+
731754
def _fake_extract(self, agent, project_path, **_kwargs):
732755
"""Simulate template extraction: create agent commands dir."""
733756
agent_cfg = AGENT_CONFIG.get(agent, {})

0 commit comments

Comments
 (0)