feat(playbook): auto-inject AGENTS.md into CLAUDE.md

When any sync action runs and the target project already has a CLAUDE.md,
automatically insert @AGENTS.md / @AGENT_RULES.md import block so
Claude Code picks up the same rules as Codex without manual setup.

- Add sync_claude_md() called at end of sync_agents_template()
- Uses <!-- playbook:claude:start/end --> block markers for idempotent updates
- Skips if no CLAUDE.md exists (Codex users unaffected)
- Skips if @AGENTS.md already present (manual setups respected)
- Add 4 tests covering: no file, append, update block, already-referenced

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
csh 2026-05-15 17:16:35 +08:00
parent e1dbf3cc45
commit 452c6f58f9
2 changed files with 143 additions and 0 deletions

View File

@ -721,9 +721,56 @@ def sync_agents_template(context: dict) -> int:
date_value,
playbook_scripts,
)
sync_claude_md(project_root)
return 0
_CLAUDE_BLOCK_START = "<!-- playbook:claude:start -->"
_CLAUDE_BLOCK_END = "<!-- playbook:claude:end -->"
def sync_claude_md(project_root: Path) -> None:
claude_md = project_root / "CLAUDE.md"
if not claude_md.exists():
return
block_lines = [
_CLAUDE_BLOCK_START,
"",
"@AGENTS.md",
"@AGENT_RULES.md",
"",
_CLAUDE_BLOCK_END,
]
text = claude_md.read_text(encoding="utf-8")
if _CLAUDE_BLOCK_START in text:
lines = text.splitlines()
updated: list[str] = []
in_block = False
replaced = False
for line in lines:
if not replaced and line.strip() == _CLAUDE_BLOCK_START:
updated.extend(block_lines)
in_block = True
replaced = True
continue
if in_block:
if line.strip() == _CLAUDE_BLOCK_END:
in_block = False
continue
updated.append(line)
claude_md.write_text("\n".join(updated) + "\n", encoding="utf-8")
log("Updated CLAUDE.md (playbook block).")
elif "@AGENTS.md" in text:
log("Skip: CLAUDE.md already references AGENTS.md")
else:
appended = text.rstrip("\n") + "\n\n" + "\n".join(block_lines) + "\n"
claude_md.write_text(appended, encoding="utf-8")
log("Appended playbook block to CLAUDE.md")
def should_sync_agents(config: dict) -> bool:
for key in ("sync_rules", "sync_memory_bank", "sync_prompts", "sync_standards"):
if key in config:

View File

@ -487,5 +487,101 @@ skills = ["style-cleanup"]
root, agents_home, f"{CUSTOM_DEPLOY_ROOT}/docs"
)
def test_sync_claude_md_skips_when_no_claude_md(self):
with tempfile.TemporaryDirectory() as tmp_dir:
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)
self.assertFalse((Path(tmp_dir) / "CLAUDE.md").exists())
def test_sync_claude_md_appends_block_to_existing_claude_md(self):
with tempfile.TemporaryDirectory() as tmp_dir:
claude_md = Path(tmp_dir) / "CLAUDE.md"
claude_md.write_text("# My project\n\nSome existing content.\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.assertIn("@AGENTS.md", text)
self.assertIn("@AGENT_RULES.md", text)
self.assertIn("<!-- playbook:claude:start -->", text)
self.assertIn("Some existing content.", text)
def test_sync_claude_md_updates_existing_block(self):
with tempfile.TemporaryDirectory() as tmp_dir:
claude_md = Path(tmp_dir) / "CLAUDE.md"
claude_md.write_text(
"# My project\n\n"
"<!-- playbook:claude:start -->\n"
"@AGENTS.md\n"
"<!-- playbook:claude:end -->\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.assertIn("@AGENTS.md", text)
self.assertIn("@AGENT_RULES.md", text)
self.assertEqual(text.count("<!-- playbook:claude:start -->"), 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"
original = "# My project\n\n@AGENTS.md\n"
claude_md.write_text(original, 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)
self.assertEqual(claude_md.read_text(encoding="utf-8"), original)
if __name__ == "__main__":
unittest.main()