🔧 chore(playbook): split sync_templates into sections

This commit is contained in:
csh 2026-01-27 08:49:40 +08:00
parent 278750e3c9
commit 73d5c261b1
7 changed files with 263 additions and 113 deletions

View File

@ -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**:追加路由链接(使用 `<!-- playbook:templates:start/end -->` 标记)
- **full = true**:追加完整框架(规则优先级 + 新会话开始时)到已有 AGENTS.md
- **其他文件**:如果已存在则跳过(使用 `force = true` 覆盖)
- **配置节存在即启用**:只写需要同步的配置节
- **AGENTS.md**:始终按区块更新(`<!-- playbook:xxx:start/end -->`
- **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"
```

View File

@ -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"] # 必填:要同步的语言

View File

@ -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 "<!-- playbook:framework:start -->" in agents_text:
start_marker = "<!-- playbook:framework:start -->"
end_marker = "<!-- playbook:framework:end -->"
elif "<!-- playbook:templates:start -->" in agents_text:
start_marker = "<!-- playbook:templates:start -->"
end_marker = "<!-- playbook:templates:end -->"
else:
start_marker = "<!-- playbook:templates:start -->"
end_marker = "<!-- playbook:templates:end -->"
else:
start_marker = "<!-- playbook:framework:start -->"
end_marker = "<!-- playbook:framework:end -->"
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 = "<!-- playbook:framework:start -->"
end_marker = "<!-- playbook:framework:end -->"
else:
start_marker = "<!-- playbook:templates:start -->"
end_marker = "<!-- playbook:templates:end -->"
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)

View File

@ -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**
- 默认:追加路由链接(`<!-- playbook:templates:start/end -->`
- `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**:始终按区块更新(`<!-- playbook:xxx:start/end -->`),不受配置节控制
- **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:agents:start/end -->` | 语言规则链接 | playbook.py `[sync_standards]` |
| `<!-- playbook:templates:start/end -->` | 路由链接(默认追加) | playbook.py `[sync_templates]` |
| `<!-- playbook:framework:start/end -->` | 完整框架full 追加) | playbook.py `[sync_templates]` |
| 标记 | 用途 | 说明 |
| --------------------------------------- | ------------ | -------------------------- |
| `<!-- playbook:agents:start/end -->` | 语言规则链接 | 由 `[sync_standards]` 管理 |
| `<!-- playbook:templates:start/end -->` | 路由链接 | AGENTS.md 始终按区块更新 |
| `<!-- playbook:framework:start/end -->` | 完整框架 | 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 选择与进度记录
```

View File

@ -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/)

View File

@ -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"

View File

@ -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\"]