From 73d5c261b1fedf93588ff2be80437c63b500edc6 Mon Sep 17 00:00:00 2001 From: csh Date: Tue, 27 Jan 2026 08:49:40 +0800 Subject: [PATCH] :wrench: chore(playbook): split sync_templates into sections --- README.md | 19 +- playbook.toml.example | 17 +- scripts/playbook.py | 259 +++++++++++++++------- templates/README.md | 69 ++++-- tests/README.md | 4 +- tests/cli/test_playbook_cli.py | 4 +- tests/test_sync_templates_placeholders.py | 4 +- 7 files changed, 263 insertions(+), 113 deletions(-) diff --git a/README.md b/README.md index 75030bf..7f88b44 100644 --- a/README.md +++ b/README.md @@ -64,17 +64,20 @@ python scripts/playbook.py -config playbook.toml [playbook] project_root = "/path/to/project" -[sync_templates] +[sync_rules] +# force = true # 可选 + +[sync_memory_bank] project_name = "MyProject" -full = false + +[sync_prompts] ``` **部署行为**: -- **新项目**:创建完整的 `AGENTS.md`、`AGENT_RULES.md`、`memory-bank/`、`docs/prompts/` -- **已有 AGENTS.md**:追加路由链接(使用 `` 标记) -- **full = true**:追加完整框架(规则优先级 + 新会话开始时)到已有 AGENTS.md -- **其他文件**:如果已存在则跳过(使用 `force = true` 覆盖) +- **配置节存在即启用**:只写需要同步的配置节 +- **AGENTS.md**:始终按区块更新(``) +- **force**:默认 false,已存在则跳过;设为 true 时强制覆盖(会先备份) 详见:`templates/README.md` @@ -226,7 +229,9 @@ git commit -m ":package: deps(playbook): add tsl standards" [sync_standards] langs = ["tsl", "cpp"] - [sync_templates] + [sync_rules] + + [sync_memory_bank] project_name = "MyProject" ``` diff --git a/playbook.toml.example b/playbook.toml.example index 0f63c5b..30edfa0 100644 --- a/playbook.toml.example +++ b/playbook.toml.example @@ -11,13 +11,20 @@ # langs = ["tsl"] # 可选:默认仅 tsl # target_dir = "docs/standards/playbook" -[sync_templates] +[sync_rules] +# 同步 AGENT_RULES.md(配置节存在即启用) +# force = false # 可选:覆盖已有文件 + +[sync_memory_bank] +# 同步 memory-bank/(配置节存在即启用) # project_name = "MyProject" # 可选:替换 {{PROJECT_NAME}} -# main_language = "tsl" # 可选:替换 {{MAIN_LANGUAGE}}(未配置时取 sync_standards.langs[0],否则 tsl) -# date = "2026-01-23" # 可选:替换 {{DATE}},默认今天 -# force = false # 可选:覆盖已有目录 +# force = false # 可选:覆盖已有目录(会先备份) +# no_backup = false # 可选:跳过备份 + +[sync_prompts] +# 同步 docs/prompts/(配置节存在即启用) +# force = false # 可选:覆盖已有目录(会先备份) # no_backup = false # 可选:跳过备份 -# full = false # 可选:写入 framework 区块 [sync_standards] # langs = ["tsl", "cpp"] # 必填:要同步的语言 diff --git a/scripts/playbook.py b/scripts/playbook.py index 39b012e..7eda6f1 100644 --- a/scripts/playbook.py +++ b/scripts/playbook.py @@ -10,7 +10,15 @@ try: except ModuleNotFoundError: # Python < 3.11 tomllib = None -ORDER = ["vendor", "sync_templates", "sync_standards", "install_skills", "format_md"] +ORDER = [ + "vendor", + "sync_rules", + "sync_memory_bank", + "sync_prompts", + "sync_standards", + "install_skills", + "format_md", +] SCRIPT_DIR = Path(__file__).resolve().parent PLAYBOOK_ROOT = SCRIPT_DIR.parent @@ -503,15 +511,125 @@ def update_agents_section( log("Appended: AGENTS.md (section)") -def sync_templates_action(config: dict, context: dict) -> int: +def resolve_project_name(context: dict) -> str | None: + config = context.get("config", {}) + if not isinstance(config, dict): + return None + memory_conf = config.get("sync_memory_bank") + if isinstance(memory_conf, dict): + raw = memory_conf.get("project_name") + if raw is not None and str(raw).strip(): + return str(raw).strip() + return None + + +def resolve_template_date(context: dict) -> str: + config = context.get("config", {}) + if isinstance(config, dict): + for key in ("sync_rules", "sync_memory_bank", "sync_prompts"): + section = config.get(key) + if isinstance(section, dict): + value = section.get("date") + if value: + return str(value) + return datetime.now().strftime("%Y-%m-%d") + + +def sync_agents_template(context: dict) -> int: project_root: Path = context["project_root"] if project_root.resolve() == PLAYBOOK_ROOT.resolve(): log("Skip: playbook root equals project root.") return 0 templates_dir = PLAYBOOK_ROOT / "templates" - if not templates_dir.is_dir(): - print(f"ERROR: templates not found: {templates_dir}", file=sys.stderr) + agents_src = templates_dir / "AGENTS.template.md" + if not agents_src.is_file(): + return 0 + + project_name = resolve_project_name(context) + main_language = resolve_main_language({}, context) + playbook_scripts = resolve_playbook_scripts(project_root, context) + date_value = resolve_template_date(context) + + agents_dst = project_root / "AGENTS.md" + if agents_dst.exists(): + agents_text = agents_dst.read_text(encoding="utf-8") + if "" in agents_text: + start_marker = "" + end_marker = "" + elif "" in agents_text: + start_marker = "" + end_marker = "" + else: + start_marker = "" + end_marker = "" + else: + start_marker = "" + end_marker = "" + + update_agents_section( + agents_dst, + agents_src, + start_marker, + end_marker, + project_name, + date_value, + main_language, + playbook_scripts, + ) + return 0 + + +def should_sync_agents(config: dict) -> bool: + for key in ("sync_rules", "sync_memory_bank", "sync_prompts", "sync_standards"): + if key in config: + return True + return False + + +def sync_rules_action(config: dict, context: dict) -> int: + project_root: Path = context["project_root"] + if project_root.resolve() == PLAYBOOK_ROOT.resolve(): + log("Skip: playbook root equals project root.") + return 0 + + templates_dir = PLAYBOOK_ROOT / "templates" + rules_src = templates_dir / "AGENT_RULES.template.md" + if not rules_src.is_file(): + print(f"ERROR: template not found: {rules_src}", file=sys.stderr) + return 2 + + rules_dst = project_root / "AGENT_RULES.md" + force = bool(config.get("force", False)) + if rules_dst.exists() and not force: + log("AGENT_RULES.md already exists. Use force to overwrite.") + return 0 + + project_name = resolve_project_name(context) + main_language = resolve_main_language(config, context) + playbook_scripts = resolve_playbook_scripts(project_root, context) + date_value = config.get("date") or datetime.now().strftime("%Y-%m-%d") + + backup_path(rules_dst, False) + text = rules_src.read_text(encoding="utf-8") + text = replace_placeholders( + text, project_name, date_value, main_language, playbook_scripts + ) + rules_dst.write_text(text + "\n", encoding="utf-8") + log("Synced: AGENT_RULES.md") + return 0 + + +def sync_memory_bank_action(config: dict, context: dict) -> int: + project_root: Path = context["project_root"] + if project_root.resolve() == PLAYBOOK_ROOT.resolve(): + log("Skip: playbook root equals project root.") + return 0 + + templates_dir = PLAYBOOK_ROOT / "templates" + memory_src = templates_dir / "memory-bank" + if not memory_src.is_dir(): + print(f"ERROR: templates not found: {memory_src}", file=sys.stderr) return 2 project_name = config.get("project_name") @@ -520,80 +638,62 @@ def sync_templates_action(config: dict, context: dict) -> int: date_value = config.get("date") or datetime.now().strftime("%Y-%m-%d") force = bool(config.get("force", False)) no_backup = bool(config.get("no_backup", False)) - full = bool(config.get("full", False)) - memory_src = templates_dir / "memory-bank" + memory_dst = project_root / "memory-bank" + if memory_dst.exists() and not force: + log("memory-bank/ already exists. Use force to overwrite.") + return 0 + + backup_path(memory_dst, no_backup) + copytree(memory_src, memory_dst) + rename_template_files(memory_dst) + replace_placeholders_in_dir( + memory_dst, + project_name, + date_value, + main_language, + playbook_scripts, + ) + log("Synced: memory-bank/") + return 0 + + +def sync_prompts_action(config: dict, context: dict) -> int: + project_root: Path = context["project_root"] + if project_root.resolve() == PLAYBOOK_ROOT.resolve(): + log("Skip: playbook root equals project root.") + return 0 + + templates_dir = PLAYBOOK_ROOT / "templates" prompts_src = templates_dir / "prompts" - agents_src = templates_dir / "AGENTS.template.md" - rules_src = templates_dir / "AGENT_RULES.template.md" + if not prompts_src.is_dir(): + print(f"ERROR: templates not found: {prompts_src}", file=sys.stderr) + return 2 - if memory_src.is_dir(): - memory_dst = project_root / "memory-bank" - if memory_dst.exists() and not force: - log("memory-bank/ already exists. Use force to overwrite.") - else: - backup_path(memory_dst, no_backup) - copytree(memory_src, memory_dst) - rename_template_files(memory_dst) - replace_placeholders_in_dir( - memory_dst, - project_name, - date_value, - main_language, - playbook_scripts, - ) - log("Synced: memory-bank/") + project_name = resolve_project_name(context) + main_language = resolve_main_language(config, context) + playbook_scripts = resolve_playbook_scripts(project_root, context) + date_value = config.get("date") or datetime.now().strftime("%Y-%m-%d") + force = bool(config.get("force", False)) + no_backup = bool(config.get("no_backup", False)) - if prompts_src.is_dir(): - prompts_dst = project_root / "docs/prompts" - if prompts_dst.exists() and not force: - log("docs/prompts/ already exists. Use force to overwrite.") - else: - backup_path(prompts_dst, no_backup) - ensure_dir(prompts_dst.parent) - copytree(prompts_src, prompts_dst) - rename_template_files(prompts_dst) - replace_placeholders_in_dir( - prompts_dst, - project_name, - date_value, - main_language, - playbook_scripts, - ) - log("Synced: docs/prompts/") - - if agents_src.is_file(): - agents_dst = project_root / "AGENTS.md" - if full: - start_marker = "" - end_marker = "" - else: - start_marker = "" - end_marker = "" - update_agents_section( - agents_dst, - agents_src, - start_marker, - end_marker, - project_name, - date_value, - main_language, - playbook_scripts, - ) - - if rules_src.is_file(): - rules_dst = project_root / "AGENT_RULES.md" - if rules_dst.exists() and not force: - log("AGENT_RULES.md already exists. Use force to overwrite.") - else: - backup_path(rules_dst, no_backup) - text = rules_src.read_text(encoding="utf-8") - text = replace_placeholders( - text, project_name, date_value, main_language, playbook_scripts - ) - rules_dst.write_text(text + "\n", encoding="utf-8") - log("Synced: AGENT_RULES.md") + prompts_dst = project_root / "docs/prompts" + if prompts_dst.exists() and not force: + log("docs/prompts/ already exists. Use force to overwrite.") + return 0 + backup_path(prompts_dst, no_backup) + ensure_dir(prompts_dst.parent) + copytree(prompts_src, prompts_dst) + rename_template_files(prompts_dst) + replace_placeholders_in_dir( + prompts_dst, + project_name, + date_value, + main_language, + playbook_scripts, + ) + log("Synced: docs/prompts/") return 0 @@ -959,8 +1059,12 @@ def run_action(name: str, config: dict, context: dict) -> int: print(f"[action] {name}") if name == "vendor": return vendor_action(config, context) - if name == "sync_templates": - return sync_templates_action(config, context) + if name == "sync_rules": + return sync_rules_action(config, context) + if name == "sync_memory_bank": + return sync_memory_bank_action(config, context) + if name == "sync_prompts": + return sync_prompts_action(config, context) if name == "sync_standards": return sync_standards_action(config, context) if name == "install_skills": @@ -1002,6 +1106,11 @@ def main(argv: list[str]) -> int: "config": config, } + if should_sync_agents(config): + result = sync_agents_template(context) + if result != 0: + return result + for name in ORDER: if name in config: result = run_action(name, config[name], context) diff --git a/templates/README.md b/templates/README.md index 8132711..6a17cc4 100644 --- a/templates/README.md +++ b/templates/README.md @@ -37,16 +37,25 @@ templates/ ## 快速部署 -使用统一入口 `playbook.py`: +使用统一入口 `playbook.py`,配置节存在即启用: ```toml # playbook.toml [playbook] project_root = "/path/to/project" -[sync_templates] +# 同步 AGENT_RULES.md(配置节存在即启用) +[sync_rules] +# force = true # 可选,强制覆盖已存在的文件 + +# 同步 memory-bank/(配置节存在即启用) +[sync_memory_bank] project_name = "MyProject" -full = false +# force = true # 可选,强制覆盖(会先备份) + +# 同步 docs/prompts/(配置节存在即启用) +[sync_prompts] +# force = true # 可选,强制覆盖(会先备份) ``` ```bash @@ -55,14 +64,37 @@ python docs/standards/playbook/scripts/playbook.py -config playbook.toml 参数说明见 `playbook.toml.example`(仓库根目录)或 vendoring 后的 `docs/standards/playbook/playbook.toml.example`。 -### 部署行为 +### 配置节说明 -- **新项目**:创建完整的 AGENTS.md、AGENT_RULES.md、memory-bank/、docs/prompts/ -- **已有 AGENTS.md**: - - 默认:追加路由链接(``) - - `full = true`:追加完整框架(规则优先级 + 路由 + 新会话开始时) -- **其他文件**:如果已存在则跳过(使用 `force = true` 覆盖) -- **占位符替换**:自动替换 `{{DATE}}` 为当前日期 +| 配置节 | 部署内容 | 选项 | +| -------------------- | -------------- | ----------------------- | +| `[sync_rules]` | AGENT_RULES.md | `force` | +| `[sync_memory_bank]` | memory-bank/ | `project_name`, `force` | +| `[sync_prompts]` | docs/prompts/ | `force` | + +- **配置节存在即启用**:只写需要同步的配置节 +- **AGENTS.md**:始终按区块更新(``),不受配置节控制 +- **force**:默认 false,已存在则跳过;设为 true 时强制覆盖(memory-bank/ 和 prompts/ 会先备份) +- **占位符替换**:自动替换 `{{DATE}}`、`{{PLAYBOOK_SCRIPTS}}` 等 + +### 典型场景 + +```toml +# 场景 1:初次部署(全部) +[sync_rules] +[sync_memory_bank] +project_name = "MyProject" +[sync_prompts] + +# 场景 2:框架升级(只更新规则) +[sync_rules] +force = true + +# 场景 3:重置项目上下文 +[sync_memory_bank] +project_name = "MyProject" +force = true +``` ### 部署后的目录结构 @@ -99,9 +131,8 @@ project/ | `{{PLAYBOOK_SCRIPTS}}` | 脚本路径 | ✅ 是 | | 其他 `{{...}}` | 项目特定内容 | ❌ 手动 | -`{{PROJECT_NAME}}` 可通过 `sync_templates.project_name` 自动替换;未配置时保持原样。 -`{{MAIN_LANGUAGE}}` 可通过 `sync_templates.main_language` 或 `sync_standards.langs[0]` 自动替换; -未配置时默认 `tsl`。 +`{{PROJECT_NAME}}` 可通过 `sync_memory_bank.project_name` 自动替换;未配置时保持原样。 +`{{MAIN_LANGUAGE}}` 可通过 `sync_standards.langs[0]` 自动替换;未配置时默认 `tsl`。 `{{PLAYBOOK_SCRIPTS}}` 自动替换为 Playbook 脚本路径(默认 `docs/standards/playbook/scripts`)。 ## 模板说明 @@ -170,11 +201,11 @@ project/ **playbook 标记**(用于自动更新): -| 标记 | 用途 | 管理入口 | -| --------------------------------------- | --------------------- | ------------------------------ | -| `` | 语言规则链接 | playbook.py `[sync_standards]` | -| `` | 路由链接(默认追加) | playbook.py `[sync_templates]` | -| `` | 完整框架(full 追加) | playbook.py `[sync_templates]` | +| 标记 | 用途 | 说明 | +| --------------------------------------- | ------------ | -------------------------- | +| `` | 语言规则链接 | 由 `[sync_standards]` 管理 | +| `` | 路由链接 | AGENTS.md 始终按区块更新 | +| `` | 完整框架 | AGENTS.md 始终按区块更新 | ### ci/、cpp/、python/ @@ -213,7 +244,7 @@ playbook/ ├── docs/ # 权威静态文档 ├── templates/ # 本目录:项目架构模板 → 部署到 memory-bank/ 等 └── scripts/ - ├── playbook.py # 统一入口:vendor/sync_templates/sync_standards/... + ├── playbook.py # 统一入口:vendor/sync_rules/sync_memory_bank/sync_prompts/sync_standards/... └── plan_progress.py # Plan 选择与进度记录 ``` diff --git a/tests/README.md b/tests/README.md index 717c888..487689c 100644 --- a/tests/README.md +++ b/tests/README.md @@ -13,7 +13,7 @@ tests/ ├── test_gitattributes_modes.py # gitattr_mode 行为测试 ├── test_plan_progress_cli.py # plan_progress CLI 测试 ├── test_superpowers_list_sync.py # superpowers 列表一致性测试 -├── test_sync_templates_placeholders.py # 占位符替换测试 +├── test_sync_templates_placeholders.py # 占位符替换测试(sync_rules/sync_standards) ├── test_toml_edge_cases.py # TOML 解析边界测试 ├── templates/ # 模板验证测试 │ ├── validate_python_templates.sh # Python 模板验证 @@ -64,7 +64,7 @@ sh tests/integration/check_doc_links.sh - CLI 参数解析与帮助信息 - TOML 配置解析与动作顺序 -- vendor/sync/install 等基础动作落地 +- vendor/sync_rules/sync_memory_bank/sync_prompts/sync_standards 等基础动作落地 ### 2. 模板验证测试 (templates/) diff --git a/tests/cli/test_playbook_cli.py b/tests/cli/test_playbook_cli.py index f4dd2c7..d070fa9 100644 --- a/tests/cli/test_playbook_cli.py +++ b/tests/cli/test_playbook_cli.py @@ -65,13 +65,13 @@ langs = ["tsl"] self.assertEqual(result.returncode, 0) self.assertTrue(snapshot.is_file()) - def test_sync_templates_creates_memory_bank(self): + def test_sync_memory_bank_creates_memory_bank(self): with tempfile.TemporaryDirectory() as tmp_dir: config_body = f""" [playbook] project_root = "{tmp_dir}" -[sync_templates] +[sync_memory_bank] project_name = "Demo" """ config_path = Path(tmp_dir) / "playbook.toml" diff --git a/tests/test_sync_templates_placeholders.py b/tests/test_sync_templates_placeholders.py index 5737ccb..f39c67e 100644 --- a/tests/test_sync_templates_placeholders.py +++ b/tests/test_sync_templates_placeholders.py @@ -23,9 +23,7 @@ class SyncTemplatesPlaceholdersTests(unittest.TestCase): [playbook] project_root = \"{tmp_dir}\" -[sync_templates] -project_name = \"Demo\" -full = true +[sync_rules] [sync_standards] langs = [\"cpp\", \"tsl\"]