Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions src/specify_cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1461,6 +1461,48 @@ def ensure_constitution_from_template(project_path: Path, tracker: StepTracker |
console.print(f"[yellow]Warning: Could not initialize constitution: {e}[/yellow]")


def ensure_claude_md(project_path: Path, tracker: StepTracker | None = None) -> None:
"""Create a minimal root `CLAUDE.md` for Claude Code if missing.

Claude Code expects `CLAUDE.md` at the project root; this file acts as a
bridge to `.specify/memory/constitution.md` (the source of truth).
"""
claude_file = project_path / "CLAUDE.md"
if claude_file.exists():
if tracker:
tracker.add("claude-md", "Claude Code role file")
tracker.skip("claude-md", "existing file preserved")
return

content = (
"## Claude's Role\n"
"Read `.specify/memory/constitution.md` first. It is the authoritative source of truth for this project. "
"Everything in it is non-negotiable.\n\n"
"## SpecKit Commands\n"
"- `/speckit.specify` — generate spec\n"
"- `/speckit.plan` — generate plan\n"
"- `/speckit.tasks` — generate task list\n"
"- `/speckit.implement` — execute plan\n\n"
"## On Ambiguity\n"
"If a spec is missing, incomplete, or conflicts with the constitution — stop and ask. "
"Do not infer. Do not proceed.\n\n"
Comment on lines +1487 to +1498
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ensure_claude_md() will create a root CLAUDE.md that instructs Claude to read .specify/memory/constitution.md even when that file does not exist (e.g., when ensure_constitution_from_template() failed because the template was missing). Consider gating CLAUDE.md creation on the constitution file existing (or on the template being present), and mark the tracker step as error/skipped otherwise to avoid generating misleading guidance.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated

)

try:
claude_file.write_text(content, encoding="utf-8")
if tracker:
tracker.add("claude-md", "Claude Code role file")
tracker.complete("claude-md", "created")
else:
console.print("[cyan]Initialized CLAUDE.md for Claude Code[/cyan]")
except Exception as e:
if tracker:
tracker.add("claude-md", "Claude Code role file")
tracker.error("claude-md", str(e))
else:
console.print(f"[yellow]Warning: Could not create CLAUDE.md: {e}[/yellow]")


INIT_OPTIONS_FILE = ".specify/init-options.json"


Expand Down Expand Up @@ -2071,6 +2113,8 @@ def init(
("constitution", "Constitution setup"),
]:
tracker.add(key, label)
if selected_ai == "claude":
tracker.add("claude-md", "Claude Code role file")
if ai_skills:
tracker.add("ai-skills", "Install agent skills")
for key, label in [
Expand Down Expand Up @@ -2137,6 +2181,9 @@ def init(

ensure_constitution_from_template(project_path, tracker=tracker)

if selected_ai == "claude":
ensure_claude_md(project_path, tracker=tracker)

# Determine skills directory and migrate any legacy Kimi dotted skills.
migrated_legacy_kimi_skills = 0
removed_legacy_kimi_skills = 0
Expand Down
35 changes: 35 additions & 0 deletions tests/test_ai_skills.py
Original file line number Diff line number Diff line change
Expand Up @@ -693,6 +693,41 @@ class TestNewProjectCommandSkip:
download_and_extract_template patched to create local fixtures.
"""

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

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

result = runner.invoke(
app,
[
"init",
str(target),
"--ai",
"claude",
"--offline",
"--ignore-agent-tools",
"--no-git",
"--script",
"sh",
],
)
Copy link

Copilot AI Mar 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test runs the full specify init --offline scaffolding path (requiring external bash + zip) instead of using the class’s existing approach of patching download_and_extract_template/scaffolding helpers. That makes the test slower and potentially skipped/flaky depending on the environment, reducing the chance the behavior is exercised in CI. Recommend patching the extraction/scaffold steps (and creating the minimal .specify/... structure needed) so the test is deterministic and does not depend on system tools.

Suggested change
result = runner.invoke(
app,
[
"init",
str(target),
"--ai",
"claude",
"--offline",
"--ignore-agent-tools",
"--no-git",
"--script",
"sh",
],
)
with patch.object(specify_cli, "download_and_extract_template", self._fake_extract):
result = runner.invoke(
app,
[
"init",
str(target),
"--ai",
"claude",
"--offline",
"--ignore-agent-tools",
"--no-git",
"--script",
"sh",
],
)

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated


assert result.exit_code == 0, result.output

claude_file = target / "CLAUDE.md"
assert claude_file.exists()

content = claude_file.read_text(encoding="utf-8")
assert "## Claude's Role" in content
assert "`.specify/memory/constitution.md`" in content
assert "/speckit.plan" in content

def _fake_extract(self, agent, project_path, **_kwargs):
"""Simulate template extraction: create agent commands dir."""
agent_cfg = AGENT_CONFIG.get(agent, {})
Expand Down
Loading