diff --git a/scripts/playbook.py b/scripts/playbook.py index e486baac..08eb25e9 100644 --- a/scripts/playbook.py +++ b/scripts/playbook.py @@ -760,6 +760,10 @@ _CLAUDE_BLOCK_END = "" _CLAUDE_MD_CANDIDATES = ["CLAUDE.md", ".claude/CLAUDE.md"] +def _claude_block_needs_heading(text: str) -> bool: + return text.lstrip().startswith(_CLAUDE_BLOCK_START) + + def sync_claude_md(project_root: Path, config: dict) -> None: claude_md_config = config.get("playbook", {}).get("claude_md") @@ -796,7 +800,9 @@ def sync_claude_md(project_root: Path, config: dict) -> None: if not claude_md.exists(): ensure_dir(claude_md.parent) claude_md.write_text( - "\n".join(block_lines) + "\n", encoding="utf-8", newline="\n" + "# CLAUDE.md\n\n" + "\n".join(block_lines) + "\n", + encoding="utf-8", + newline="\n", ) log(f"Created {claude_md.relative_to(project_root)} with playbook block.") return @@ -819,9 +825,10 @@ def sync_claude_md(project_root: Path, config: dict) -> None: in_block = False continue updated.append(line) - claude_md.write_text( - "\n".join(updated) + "\n", encoding="utf-8", newline="\n" - ) + updated_text = "\n".join(updated) + "\n" + if _claude_block_needs_heading(updated_text): + updated_text = "# CLAUDE.md\n\n" + updated_text.lstrip() + claude_md.write_text(updated_text, encoding="utf-8", newline="\n") log("Updated CLAUDE.md (playbook block).") elif "@AGENTS.md" in text: log("Skip: CLAUDE.md already references AGENTS.md") diff --git a/tests/cli/test_playbook_cli.py b/tests/cli/test_playbook_cli.py index 474273c5..92d69bfd 100644 --- a/tests/cli/test_playbook_cli.py +++ b/tests/cli/test_playbook_cli.py @@ -607,6 +607,7 @@ project_name = "Demo" claude_md = Path(tmp_dir) / "CLAUDE.md" self.assertTrue(claude_md.exists()) text = claude_md.read_text(encoding="utf-8") + self.assertTrue(text.startswith("# CLAUDE.md\n\n")) self.assertIn("@AGENTS.md", text) self.assertIn("", text) @@ -665,6 +666,39 @@ project_name = "Demo" self.assertIn("@AGENT_RULES.md", text) self.assertEqual(text.count(""), 1) + def test_sync_claude_md_adds_heading_to_generated_block(self): + with tempfile.TemporaryDirectory() as tmp_dir: + claude_md = Path(tmp_dir) / "CLAUDE.md" + claude_md.write_text( + "\n" + "\n" + "@AGENTS.md\n" + "@AGENT_RULES.md\n" + "\n" + "\n", + encoding="utf-8", + ) + + config_body = f""" +[playbook] +project_root = "{tmp_dir}" +deploy_root = "{CUSTOM_DEPLOY_ROOT}" + +[sync_memory_bank] +project_name = "Demo" +""" + config_path = Path(tmp_dir) / "playbook.toml" + config_path.write_text(config_body, encoding="utf-8") + + result = run_cli("-config", str(config_path)) + + self.assertEqual(result.returncode, 0) + text = claude_md.read_text(encoding="utf-8") + self.assertTrue(text.startswith("# CLAUDE.md\n\n")) + self.assertIn("@AGENTS.md", text) + self.assertIn("@AGENT_RULES.md", text) + self.assertEqual(text.count(""), 1) + def test_sync_claude_md_skips_when_already_references_agents(self): with tempfile.TemporaryDirectory() as tmp_dir: claude_md = Path(tmp_dir) / "CLAUDE.md"