feat(playbook): add install_mode deployment config

BREAKING CHANGE: [vendor] and playbook.deploy_root are no longer supported. Use [playbook].install_mode and playbook.playbook_root instead.
This commit is contained in:
csh 2026-06-01 10:49:16 +08:00
parent 2c3a559a30
commit f2ab57b39f
15 changed files with 376 additions and 184 deletions

View File

@ -1,6 +1,7 @@
---
name: 🧪 Playbook 测试套件 name: 🧪 Playbook 测试套件
on: "on":
push: push:
branches: branches:
- main - main
@ -54,7 +55,8 @@ jobs:
git clone "$REPO_URL" "$REPO_DIR" git clone "$REPO_URL" "$REPO_DIR"
if git -C "$REPO_DIR" cat-file -e "${TARGET_SHA}^{commit}" 2>/dev/null; then if git -C "$REPO_DIR" cat-file -e \
"${TARGET_SHA}^{commit}" 2>/dev/null; then
git -C "$REPO_DIR" checkout -f "$TARGET_SHA" git -C "$REPO_DIR" checkout -f "$TARGET_SHA"
else else
if [ -n "$TARGET_REF" ]; then if [ -n "$TARGET_REF" ]; then
@ -90,16 +92,11 @@ jobs:
set -euo pipefail set -euo pipefail
echo "========================================" echo "========================================"
echo "🧪 Python CLI 测试" echo "🧪 Python 测试"
echo "========================================" echo "========================================"
cd "$REPO_DIR" cd "$REPO_DIR"
python3 -m unittest discover -s tests/cli -v echo "覆盖CLI、subtree/snapshot 部署路线、模板同步、文档一致性"
echo "========================================"
echo "🧪 Python 扩展测试"
echo "========================================"
python3 -m unittest discover -s tests -p "test_*.py" -v python3 -m unittest discover -s tests -p "test_*.py" -v
echo "========================================" echo "========================================"

View File

@ -58,6 +58,8 @@ python scripts/playbook.py -config playbook.toml
```toml ```toml
[playbook] [playbook]
project_root = "/path/to/project" project_root = "/path/to/project"
playbook_root = "docs/standards/playbook"
install_mode = "snapshot"
[sync_rules] [sync_rules]
# force = true # 可选 # force = true # 可选
@ -81,12 +83,12 @@ project_name = "MyProject"
```bash ```bash
# spec 写完后 # spec 写完后
python <deploy_root>/scripts/playbook.py \ python <playbook_root>/scripts/playbook.py \
-record-spec docs/superpowers/specs/<topic>-design.md \ -record-spec docs/superpowers/specs/<topic>-design.md \
-progress memory-bank/progress.md -progress memory-bank/progress.md
# plan 写完后 # plan 写完后
python <deploy_root>/scripts/playbook.py \ python <playbook_root>/scripts/playbook.py \
-record-plan docs/superpowers/plans/<topic>.md \ -record-plan docs/superpowers/plans/<topic>.md \
-progress memory-bank/progress.md -progress memory-bank/progress.md
``` ```
@ -199,10 +201,11 @@ TSL 相关问题直接查阅 `rulesets/tsl/index.md` 与 `docs/tsl/`。
先区分三个路径概念: 先区分三个路径概念:
- `project_root`:目标项目根目录。 - `project_root`:目标项目根目录。
- `deploy_root`:相对于 `project_root` 的项目内目标目录。 - `playbook_root`:相对于 `project_root` 的项目内 Playbook 根目录。
- 外部 clone 出来的 Playbook 路径(如 `/opt/playbook`):只是执行部署脚本的位置,不是部署目标。 - `install_mode``subtree` 表示 Playbook 已由 git subtree 放在项目内;`snapshot` 表示从外部 clone 安装裁剪快照。
- 外部 clone 出来的 Playbook 路径(如 `/opt/playbook`):只是执行安装脚本的位置,不是项目内 Playbook 根目录。
以 TSL 为例Playbook 在项目内的默认部署根是 `docs/standards/playbook`;如果你把 `deploy_root` 改成 `custom/playbook`,则部署结果会落到 `<project_root>/custom/playbook`,文档和脚本入口也会跟着变成 `custom/playbook/docs/...`、`custom/playbook/scripts/...`。 以 TSL 为例Playbook 在项目内的默认根是 `docs/standards/playbook`;如果你把 `playbook_root` 改成 `custom/playbook`,则快照会落到 `<project_root>/custom/playbook`,文档和脚本入口也会跟着变成 `custom/playbook/docs/...`、`custom/playbook/scripts/...`。
#### 方式一:`git subtree` #### 方式一:`git subtree`
@ -214,6 +217,8 @@ git subtree add --prefix docs/standards/playbook https://git.mytsl.cn/csh/playbo
cat <<'EOF' > playbook.toml cat <<'EOF' > playbook.toml
[playbook] [playbook]
project_root = "." project_root = "."
playbook_root = "docs/standards/playbook"
install_mode = "subtree"
[sync_standards] [sync_standards]
langs = ["tsl"] langs = ["tsl"]
@ -249,6 +254,8 @@ git commit -m ":package: deps(playbook): add tsl standards"
# playbook.toml # playbook.toml
[playbook] [playbook]
project_root = "." project_root = "."
playbook_root = "docs/standards/playbook"
install_mode = "subtree"
[sync_standards] [sync_standards]
langs = ["tsl", "cpp"] langs = ["tsl", "cpp"]
@ -277,19 +284,17 @@ git commit -m ":package: deps(playbook): add tsl standards"
git clone https://git.mytsl.cn/csh/playbook.git /opt/playbook git clone https://git.mytsl.cn/csh/playbook.git /opt/playbook
``` ```
2. 在目标项目根创建 `playbook.toml`,并用 `deploy_root` 指定项目内的部署根。例如: 2. 在目标项目根创建 `playbook.toml`,并用 `playbook_root` 指定项目内的 Playbook 根。例如:
- `project_root` 写目标项目根目录。 - `project_root` 写目标项目根目录。
- `deploy_root` 写目标项目内的相对路径。 - `playbook_root` 写目标项目内的相对路径。
- 不要把外部 clone 的路径(如 `/opt/playbook`)写进 `deploy_root`;那只是你执行脚本的位置。 - `install_mode = "snapshot"` 表示由外部 clone 安装项目内快照。
- 不要把外部 clone 的路径(如 `/opt/playbook`)写进 `playbook_root`;那只是你执行脚本的位置。
```toml ```toml
[playbook] [playbook]
project_root = "." project_root = "."
deploy_root = "custom/playbook" playbook_root = "custom/playbook"
install_mode = "snapshot"
[vendor]
langs = ["tsl"]
[sync_standards] [sync_standards]
langs = ["tsl"] langs = ["tsl"]
@ -308,10 +313,10 @@ git commit -m ":package: deps(playbook): add tsl standards"
说明: 说明:
- 这里的 `[vendor]` 是“把 Playbook 快照部署进目标项目”的执行步骤,不是第三种正式部署路线 - `install_mode = "snapshot"` 会先把 Playbook 裁剪快照安装到目标项目的 `playbook_root`,再执行后续同步动作
- `deploy_root` 永远表示目标项目内的部署目录;它不是外部 clone 出来的 Playbook 仓库路径。 - `playbook_root` 永远表示目标项目内的 Playbook 根;它不是外部 clone 出来的 Playbook 仓库路径。
- 外部 clone 场景下必须显式填写 `deploy_root`;脚本不会替你补默认部署目录。 - 外部 clone 场景下必须显式填写 `playbook_root`;脚本不会替你补默认项目内根目录。
- 如果 `deploy_root = "custom/playbook"`,部署后的项目内入口会是 `custom/playbook/scripts/playbook.py`、`custom/playbook/docs/index.md`。 - 如果 `playbook_root = "custom/playbook"`,部署后的项目内入口会是 `custom/playbook/scripts/playbook.py`、`custom/playbook/docs/index.md`。
--- ---
@ -347,7 +352,7 @@ git commit -m ":package: deps(playbook): add tsl standards"
├── .gitattributes # 行尾/文本规范 ├── .gitattributes # 行尾/文本规范
├── AGENTS.md # Codex 入口(由 playbook 自动生成/更新) ├── AGENTS.md # Codex 入口(由 playbook 自动生成/更新)
├── CLAUDE.md # Claude Code 入口(自动注入 @AGENTS.md ├── CLAUDE.md # Claude Code 入口(自动注入 @AGENTS.md
├── <deploy_root>/ # 本 Playbook 在项目内的部署根(默认 docs/standards/playbook ├── <playbook_root>/ # 本 Playbook 在项目内的根(默认 docs/standards/playbook
│ ├── docs/ │ ├── docs/
│ ├── rulesets/ │ ├── rulesets/
│ ├── scripts/ │ ├── scripts/

View File

@ -110,10 +110,10 @@ no_backup = true
如果你的项目已经把本 Playbook 部署到项目内,则在目标项目里执行: 如果你的项目已经把本 Playbook 部署到项目内,则在目标项目里执行:
```bash ```bash
python <deploy_root>/scripts/playbook.py -config playbook.toml python <playbook_root>/scripts/playbook.py -config playbook.toml
``` ```
其中 `<deploy_root>` 默认为 `docs/standards/playbook` 其中 `<playbook_root>` 默认为 `docs/standards/playbook`
--- ---
@ -158,7 +158,7 @@ python <deploy_root>/scripts/playbook.py -config playbook.toml
- Python`docs/python/style_guide.md`、`docs/python/tooling.md`、`docs/python/configuration.md` - Python`docs/python/style_guide.md`、`docs/python/tooling.md`、`docs/python/configuration.md`
若你的项目把本 Playbook 部署到项目内,文档根路径为 若你的项目把本 Playbook 部署到项目内,文档根路径为
`<deploy_root>/docs/...`;其中 `<deploy_root>` 默认为 `docs/standards/playbook`,也可以按项目配置改成 `custom/playbook` 等自定义目录。 `<playbook_root>/docs/...`;其中 `<playbook_root>` 默认为 `docs/standards/playbook`,也可以按项目配置改成 `custom/playbook` 等自定义目录。
--- ---

View File

@ -2,14 +2,10 @@
# 配置文件所在目录默认作为 project_root。 # 配置文件所在目录默认作为 project_root。
[playbook] [playbook]
# project_root = "." # 可选:目标项目根目录 # project_root = "." # 可选:目标项目根目录
# deploy_root = "docs/standards/playbook" # 项目内已部署时可省略;从外部 clone 执行时必填;值始终是相对于 project_root 的项目内路径,不是外部 clone 的 playbook 路径 # playbook_root = "docs/standards/playbook" # 项目内 Playbook 根;相对于 project_root不是外部 clone 的 playbook 路径
# claude_md = "CLAUDE.md" # 可选CLAUDE.md 位置(默认自动检测:根目录 → .claude/CLAUDE.md → 自动创建) # install_mode = "subtree" # subtree|snapshotsubtree 表示已在项目内snapshot 表示从外部 clone 安装快照
# claude_md = "CLAUDE.md" # 可选CLAUDE.md 位置(默认自动检测:根目录 → .claude/CLAUDE.md → 自动创建)
[vendor]
# 当从外部 clone 的 playbook 向项目内部署快照时启用
# 外部 clone 路径只用于执行脚本;快照仍会写入 <project_root>/<deploy_root>
# langs = ["tsl"] # 可选:默认仅 tsl
[sync_rules] [sync_rules]
# 同步 AGENT_RULES.md配置节存在即启用 # 同步 AGENT_RULES.md配置节存在即启用

View File

@ -14,7 +14,6 @@ except ModuleNotFoundError: # Python < 3.11
tomllib = None tomllib = None
ORDER = [ ORDER = [
"vendor",
"sync_rules", "sync_rules",
"sync_memory_bank", "sync_memory_bank",
"sync_prompts", "sync_prompts",
@ -29,7 +28,14 @@ MAIN_LOOP_SPEC = importlib.util.spec_from_file_location("playbook_main_loop", MA
assert MAIN_LOOP_SPEC and MAIN_LOOP_SPEC.loader assert MAIN_LOOP_SPEC and MAIN_LOOP_SPEC.loader
MAIN_LOOP = importlib.util.module_from_spec(MAIN_LOOP_SPEC) MAIN_LOOP = importlib.util.module_from_spec(MAIN_LOOP_SPEC)
MAIN_LOOP_SPEC.loader.exec_module(MAIN_LOOP) MAIN_LOOP_SPEC.loader.exec_module(MAIN_LOOP)
PATH_CONFIG_KEYS = {"project_root", "deploy_root", "agents_home", "codex_home", "skill_link"} PATH_CONFIG_KEYS = {
"project_root",
"playbook_root",
"deploy_root",
"agents_home",
"codex_home",
"skill_link",
}
DOCS_INDEX_SECTION_HEADINGS = { DOCS_INDEX_SECTION_HEADINGS = {
"common": "## 跨语言common", "common": "## 跨语言common",
"tsl": "## TSLtsl/tsf", "tsl": "## TSLtsl/tsf",
@ -275,13 +281,13 @@ def normalize_relative_dir(raw: object, label: str) -> str:
return "." if normalized == "" else normalized return "." if normalized == "" else normalized
def join_deploy_subpath(root: str, child: str) -> str: def join_playbook_subpath(root: str, child: str) -> str:
if root in ("", "."): if root in ("", "."):
return child.lstrip("/") return child.lstrip("/")
return f"{root.rstrip('/')}/{child.lstrip('/')}" return f"{root.rstrip('/')}/{child.lstrip('/')}"
def resolve_in_project_deploy_root(project_root: Path) -> str | None: def resolve_in_project_playbook_root(project_root: Path) -> str | None:
try: try:
rel = PLAYBOOK_ROOT.resolve().relative_to(project_root.resolve()) rel = PLAYBOOK_ROOT.resolve().relative_to(project_root.resolve())
if str(rel) != ".": if str(rel) != ".":
@ -291,9 +297,8 @@ def resolve_in_project_deploy_root(project_root: Path) -> str | None:
return None return None
def config_requires_deploy_root(config: dict) -> bool: def config_uses_playbook_root(config: dict) -> bool:
for key in ( for key in (
"vendor",
"sync_rules", "sync_rules",
"sync_memory_bank", "sync_memory_bank",
"sync_prompts", "sync_prompts",
@ -305,46 +310,74 @@ def config_requires_deploy_root(config: dict) -> bool:
return False return False
def resolve_configured_deploy_root(config: dict, project_root: Path) -> str: def validate_removed_config(config: dict) -> None:
if "vendor" in config:
raise ValueError(
"[vendor] is no longer supported; use [playbook].install_mode = "
'"snapshot" and configure languages in [sync_standards]'
)
playbook_config = config.get("playbook", {})
if not isinstance(playbook_config, dict):
raise ValueError("[playbook] must be a table")
if "deploy_root" in playbook_config:
raise ValueError(
"playbook.deploy_root is no longer supported; use playbook.playbook_root"
)
if "intall_mode" in playbook_config:
raise ValueError(
"playbook.intall_mode is not supported; use playbook.install_mode"
)
def resolve_install_mode(config: dict) -> str:
playbook_config = config.get("playbook", {})
raw = "subtree"
if (
isinstance(playbook_config, dict)
and playbook_config.get("install_mode") is not None
):
raw = playbook_config.get("install_mode")
mode = str(raw).strip().lower()
if mode not in ("subtree", "snapshot"):
raise ValueError(
"playbook.install_mode must be one of: subtree, snapshot"
)
return mode
def resolve_configured_playbook_root(config: dict, project_root: Path) -> str:
playbook_config = config.get("playbook", {}) playbook_config = config.get("playbook", {})
raw = None raw = None
if isinstance(playbook_config, dict): if isinstance(playbook_config, dict):
raw = playbook_config.get("deploy_root") raw = playbook_config.get("playbook_root")
vendor_config = config.get("vendor", {})
if isinstance(vendor_config, dict) and vendor_config.get("target_dir") is not None:
raise ValueError(
"vendor.target_dir is no longer supported; use [playbook].deploy_root"
)
if raw is not None and str(raw).strip(): if raw is not None and str(raw).strip():
return normalize_relative_dir(raw, "deploy_root") return normalize_relative_dir(raw, "playbook_root")
in_project_deploy_root = resolve_in_project_deploy_root(project_root) in_project_playbook_root = resolve_in_project_playbook_root(project_root)
if in_project_deploy_root is not None: if in_project_playbook_root is not None:
return in_project_deploy_root return in_project_playbook_root
if config_requires_deploy_root(config): if resolve_install_mode(config) == "snapshot" or config_uses_playbook_root(config):
raise ValueError( raise ValueError(
"playbook.deploy_root is required when running from an external clone; " "playbook.playbook_root is required when running from an external clone; "
"set it to the target project's relative deployment path" "set it to the target project's relative Playbook path"
) )
return "docs/standards/playbook" return "docs/standards/playbook"
def resolve_deploy_root(context: dict) -> str: def resolve_playbook_root(context: dict) -> str:
project_root: Path = context["project_root"] return context["playbook_root"]
in_project_deploy_root = resolve_in_project_deploy_root(project_root)
if in_project_deploy_root is not None:
return in_project_deploy_root
return context["deploy_root"]
def resolve_docs_prefix(context: dict) -> str: def resolve_docs_prefix(context: dict) -> str:
return join_deploy_subpath(resolve_deploy_root(context), "docs") return join_playbook_subpath(resolve_playbook_root(context), "docs")
def resolve_playbook_scripts(context: dict) -> str: def resolve_playbook_scripts(context: dict) -> str:
return join_deploy_subpath(resolve_deploy_root(context), "scripts") return join_playbook_subpath(resolve_playbook_root(context), "scripts")
def read_git_commit(root: Path) -> str: def read_git_commit(root: Path) -> str:
@ -408,9 +441,11 @@ def write_docs_index(dest_prefix: Path, langs: list[str]) -> None:
docs_index.write_text("\n".join(lines) + "\n", encoding="utf-8", newline="\n") docs_index.write_text("\n".join(lines) + "\n", encoding="utf-8", newline="\n")
def write_snapshot_readme(dest_prefix: Path, deploy_root: str, langs: list[str]) -> None: def write_snapshot_readme(
scripts_path = join_deploy_subpath(deploy_root, "scripts/playbook.py") dest_prefix: Path, playbook_root: str, langs: list[str]
docs_index_path = join_deploy_subpath(deploy_root, "docs/index.md") ) -> None:
scripts_path = join_playbook_subpath(playbook_root, "scripts/playbook.py")
docs_index_path = join_playbook_subpath(playbook_root, "docs/index.md")
lines = [ lines = [
"# Playbook裁剪快照", "# Playbook裁剪快照",
"", "",
@ -424,7 +459,7 @@ def write_snapshot_readme(dest_prefix: Path, deploy_root: str, langs: list[str])
f"python {scripts_path} -config playbook.toml", f"python {scripts_path} -config playbook.toml",
"```", "```",
"", "",
f"配置示例:`{join_deploy_subpath(deploy_root, 'playbook.toml.example')}`", f"配置示例:`{join_playbook_subpath(playbook_root, 'playbook.toml.example')}`",
"", "",
"文档入口:", "文档入口:",
"", "",
@ -453,23 +488,51 @@ def write_source_file(dest_prefix: Path, langs: list[str]) -> None:
) )
def vendor_action(config: dict, context: dict) -> int: def snapshot_langs(config: dict) -> list[str]:
sync_standards = config.get("sync_standards")
if isinstance(sync_standards, dict):
return normalize_langs(sync_standards.get("langs"))
return normalize_langs(None)
def is_generated_snapshot(path: Path) -> bool:
source = path / "SOURCE.md"
if not source.is_file():
return False
try: try:
langs = normalize_langs(config.get("langs")) return "Generated-by: scripts/playbook.py" in source.read_text(encoding="utf-8")
except OSError:
return False
def install_snapshot(config: dict, context: dict) -> int:
try:
langs = snapshot_langs(config)
except ValueError as exc: except ValueError as exc:
print(f"ERROR: {exc}", file=sys.stderr) print(f"ERROR: {exc}", file=sys.stderr)
return 2 return 2
deploy_root = context["deploy_root"] playbook_root = context["playbook_root"]
target_path = Path(deploy_root) target_path = Path(playbook_root)
project_root: Path = context["project_root"] project_root: Path = context["project_root"]
dest_prefix = project_root / target_path dest_prefix = project_root / target_path
dest_standards = dest_prefix.parent dest_standards = dest_prefix.parent
if dest_prefix.resolve() == PLAYBOOK_ROOT.resolve():
log(f"Snapshot already installed at {dest_prefix}")
return 0
ensure_dir(dest_standards) ensure_dir(dest_standards)
if dest_prefix.exists(): if dest_prefix.exists():
if not is_generated_snapshot(dest_prefix):
print(
"ERROR: refusing to replace existing playbook_root because it is "
"not a generated snapshot; use install_mode = \"subtree\" for a "
"git subtree-managed Playbook",
file=sys.stderr,
)
return 2
timestamp = datetime.now().strftime("%Y%m%d%H%M%S") timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
backup = dest_standards / f"{dest_prefix.name}.bak.{timestamp}" backup = dest_standards / f"{dest_prefix.name}.bak.{timestamp}"
dest_prefix.rename(backup) dest_prefix.rename(backup)
@ -535,13 +598,29 @@ def vendor_action(config: dict, context: dict) -> int:
copy2(example_config, dest_prefix / "playbook.toml.example") copy2(example_config, dest_prefix / "playbook.toml.example")
write_docs_index(dest_prefix, langs) write_docs_index(dest_prefix, langs)
write_snapshot_readme(dest_prefix, deploy_root, langs) write_snapshot_readme(dest_prefix, playbook_root, langs)
write_source_file(dest_prefix, langs) write_source_file(dest_prefix, langs)
log(f"Vendored snapshot -> {dest_prefix}") log(f"Installed snapshot -> {dest_prefix}")
return 0 return 0
def validate_subtree_install(context: dict) -> int:
project_root: Path = context["project_root"]
playbook_root = context["playbook_root"]
expected_root = (project_root / playbook_root).resolve()
if PLAYBOOK_ROOT.resolve() == expected_root:
return 0
print(
"ERROR: install_mode = \"subtree\" requires running the project-local "
f"Playbook script at "
f"{join_playbook_subpath(playbook_root, 'scripts/playbook.py')}",
file=sys.stderr,
)
return 2
def replace_placeholders( def replace_placeholders(
text: str, text: str,
project_name: str | None, project_name: str | None,
@ -1413,8 +1492,6 @@ def format_md_action(config: dict, context: dict) -> int:
def run_action(name: str, config: dict, context: dict) -> int: def run_action(name: str, config: dict, context: dict) -> int:
print(f"[action] {name}") print(f"[action] {name}")
if name == "vendor":
return vendor_action(config, context)
if name == "sync_rules": if name == "sync_rules":
return sync_rules_action(config, context) return sync_rules_action(config, context)
if name == "sync_memory_bank": if name == "sync_memory_bank":
@ -1489,6 +1566,13 @@ def main(argv: list[str]) -> int:
return 2 return 2
config = load_config(config_path) config = load_config(config_path)
try:
validate_removed_config(config)
install_mode = resolve_install_mode(config)
except ValueError as exc:
print(f"ERROR: {exc}", file=sys.stderr)
return 2
playbook_config = config.get("playbook", {}) playbook_config = config.get("playbook", {})
project_root = playbook_config.get("project_root") project_root = playbook_config.get("project_root")
if project_root: if project_root:
@ -1499,7 +1583,7 @@ def main(argv: list[str]) -> int:
root = config_path.parent root = config_path.parent
resolved_root = root.resolve() resolved_root = root.resolve()
try: try:
deploy_root = resolve_configured_deploy_root(config, resolved_root) playbook_root = resolve_configured_playbook_root(config, resolved_root)
except ValueError as exc: except ValueError as exc:
print(f"ERROR: {exc}", file=sys.stderr) print(f"ERROR: {exc}", file=sys.stderr)
return 2 return 2
@ -1507,9 +1591,19 @@ def main(argv: list[str]) -> int:
"project_root": resolved_root, "project_root": resolved_root,
"config_path": config_path.resolve(), "config_path": config_path.resolve(),
"config": config, "config": config,
"deploy_root": deploy_root, "install_mode": install_mode,
"playbook_root": playbook_root,
} }
if install_mode == "snapshot":
result = install_snapshot(config, context)
if result != 0:
return result
elif config_uses_playbook_root(config):
result = validate_subtree_install(context)
if result != 0:
return result
if should_sync_agents(config): if should_sync_agents(config):
result = sync_agents_template(context) result = sync_agents_template(context)
if result != 0: if result != 0:

View File

@ -82,6 +82,8 @@ templates/
# playbook.toml # playbook.toml
[playbook] [playbook]
project_root = "/path/to/project" project_root = "/path/to/project"
playbook_root = "docs/standards/playbook"
install_mode = "snapshot"
# 同步 AGENT_RULES.md配置节存在即启用 # 同步 AGENT_RULES.md配置节存在即启用
[sync_rules] [sync_rules]
@ -98,15 +100,15 @@ project_name = "MyProject"
``` ```
```bash ```bash
python <deploy_root>/scripts/playbook.py -config playbook.toml python <playbook_root>/scripts/playbook.py -config playbook.toml
``` ```
参数说明见 `playbook.toml.example`(仓库根目录)或项目内的 参数说明见 `playbook.toml.example`(仓库根目录)或项目内的
`<deploy_root>/playbook.toml.example`。 `<playbook_root>/playbook.toml.example`。
其中 `<deploy_root>` 默认为 `docs/standards/playbook` 其中 `<playbook_root>` 默认为 `docs/standards/playbook`
也可以按项目配置改成 `custom/playbook` 等自定义目录; 也可以按项目配置改成 `custom/playbook` 等自定义目录;
对应文档入口会变成 `<deploy_root>/docs/...`。 对应文档入口会变成 `<playbook_root>/docs/...`。
如果你当前是在 **外部 clone 的 playbook 仓库** 中执行,而不是在目标项目内执行快照,请使用: 如果你当前是在 **外部 clone 的 playbook 仓库** 中执行,而不是在目标项目内执行快照,请使用:
@ -114,8 +116,8 @@ python <deploy_root>/scripts/playbook.py -config playbook.toml
python scripts/playbook.py -config playbook.toml python scripts/playbook.py -config playbook.toml
``` ```
此时 `[vendor]` 会把快照写入 `<project_root>/<deploy_root>` 此时 `install_mode = "snapshot"` 会把快照写入 `<project_root>/<playbook_root>`
后续再在目标项目内使用 `<deploy_root>/scripts/playbook.py` 后续再在目标项目内使用 `<playbook_root>/scripts/playbook.py`
做同步更新。 做同步更新。
### 配置节说明 ### 配置节说明
@ -296,8 +298,7 @@ project/
### ci/、cpp/、python/ ### ci/、cpp/、python/
语言和 CI 配置模板。通过 playbook.py 的 `[vendor]` 语言和 CI 配置模板。`install_mode = "snapshot"` 安装快照时会复制这些模板:
复制到快照中:
- `ci/gitea/`Gitea Actions 工作流与辅助脚本。 - `ci/gitea/`Gitea Actions 工作流与辅助脚本。
部署到快照 `templates/ci/` 部署到快照 `templates/ci/`
@ -307,7 +308,7 @@ project/
- `python/``pyproject.toml`、`.editorconfig` 等文件。 - `python/``pyproject.toml`、`.editorconfig` 等文件。
部署到快照 `templates/python/` 部署到快照 `templates/python/`
> 注意:这些模板通过 `[vendor]` 复制到快照的 `templates/` 目录,需手动从快照复制到项目根目录使用。 > 注意:这些模板复制到快照的 `templates/` 目录,需手动从快照复制到项目根目录使用。
> 其中 `ci/gitea/` 应按 `templates/ci/README.md` 的说明,整块复制 `.gitea/` 目录,而不只是复制 workflows。 > 其中 `ci/gitea/` 应按 `templates/ci/README.md` 的说明,整块复制 `.gitea/` 目录,而不只是复制 workflows。
**使用方式** **使用方式**
@ -316,14 +317,16 @@ project/
# playbook.toml - 生成包含这些模板的快照 # playbook.toml - 生成包含这些模板的快照
[playbook] [playbook]
project_root = "/path/to/project" project_root = "/path/to/project"
playbook_root = "docs/standards/playbook"
install_mode = "snapshot"
[vendor] [sync_standards]
langs = ["tsl", "cpp", "python"] langs = ["tsl", "cpp", "python"]
``` ```
```bash ```bash
python scripts/playbook.py -config playbook.toml python scripts/playbook.py -config playbook.toml
# 然后手动从 <deploy_root>/templates/ 复制所需配置到项目根目录 # 然后手动从 <playbook_root>/templates/ 复制所需配置到项目根目录
``` ```
## 与 playbook 其他部分的关系 ## 与 playbook 其他部分的关系
@ -335,14 +338,14 @@ playbook/
├── docs/ # 权威静态文档 ├── docs/ # 权威静态文档
├── templates/ # 本目录:项目架构模板 → 部署到 memory-bank/ 等 ├── templates/ # 本目录:项目架构模板 → 部署到 memory-bank/ 等
└── scripts/ └── scripts/
└── playbook.py # 统一入口:vendor / sync_* └── playbook.py # 统一入口:snapshot install / sync_*
``` ```
## 完整部署流程 ## 完整部署流程
```bash ```bash
# 1. 准备配置并执行统一入口 # 1. 准备配置并执行统一入口
python <deploy_root>/scripts/playbook.py -config playbook.toml python <playbook_root>/scripts/playbook.py -config playbook.toml
# 2. 编辑 memory-bank/*.md 填写项目信息 # 2. 编辑 memory-bank/*.md 填写项目信息

View File

@ -16,7 +16,7 @@ tests/
├── test_no_backup_flags.py # no_backup 行为测试 ├── test_no_backup_flags.py # no_backup 行为测试
├── test_playbook_typing_imports.py # playbook.py typing 导入兼容性测试 ├── test_playbook_typing_imports.py # playbook.py typing 导入兼容性测试
├── test_sync_directory_actions.py # sync_memory_bank/sync_prompts 行为测试 ├── test_sync_directory_actions.py # sync_memory_bank/sync_prompts 行为测试
├── test_vendor_snapshot_templates.py # vendor 快照模板完整性测试 ├── test_vendor_snapshot_templates.py # snapshot 快照模板完整性测试
├── test_main_loop_cli.py # main_loop CLI 测试 ├── test_main_loop_cli.py # main_loop CLI 测试
├── test_thirdparty_skills_pipeline.py # thirdparty skills 流水线配置与同步产物测试 ├── test_thirdparty_skills_pipeline.py # thirdparty skills 流水线配置与同步产物测试
├── test_sync_templates_placeholders.py # 占位符替换测试sync_rules/sync_standards ├── test_sync_templates_placeholders.py # 占位符替换测试sync_rules/sync_standards
@ -70,7 +70,7 @@ sh tests/integration/check_doc_links.sh
- CLI 参数解析与帮助信息 - CLI 参数解析与帮助信息
- TOML 配置解析与动作顺序 - TOML 配置解析与动作顺序
- vendor/sync_rules/sync_memory_bank/sync_prompts/sync_standards 等基础动作落地 - snapshot install/sync_rules/sync_memory_bank/sync_prompts/sync_standards 等基础动作落地
### 2. 模板验证测试 (templates/) ### 2. 模板验证测试 (templates/)

View File

@ -125,7 +125,8 @@ class PlaybookCliTests(unittest.TestCase):
config_body = f""" config_body = f"""
[playbook] [playbook]
project_root = "." project_root = "."
deploy_root = "{CUSTOM_DEPLOY_ROOT}" playbook_root = "{CUSTOM_DEPLOY_ROOT}"
install_mode = "snapshot"
[format_md] [format_md]
@ -142,7 +143,7 @@ langs = ["tsl"]
self.assertIn("sync_standards", output) self.assertIn("sync_standards", output)
self.assertIn("format_md", output) self.assertIn("format_md", output)
def test_format_md_only_does_not_require_deploy_root(self): def test_format_md_only_does_not_require_playbook_root(self):
with tempfile.TemporaryDirectory() as tmp_dir: with tempfile.TemporaryDirectory() as tmp_dir:
config_body = f""" config_body = f"""
[playbook] [playbook]
@ -157,15 +158,55 @@ project_root = "{tmp_dir}"
self.assertEqual(result.returncode, 0) self.assertEqual(result.returncode, 0)
def test_vendor_creates_snapshot(self): def test_vendor_section_is_rejected(self):
with tempfile.TemporaryDirectory() as tmp_dir:
root = Path(tmp_dir)
config_body = f"""
[playbook]
project_root = "{tmp_dir}"
playbook_root = "{CUSTOM_DEPLOY_ROOT}"
install_mode = "snapshot"
[vendor]
langs = ["tsl"]
"""
config_path = write_config(root, "playbook.toml", config_body)
result = run_cli("-config", str(config_path))
self.assertNotEqual(result.returncode, 0)
self.assertIn("[vendor]", result.stdout + result.stderr)
def test_deploy_root_is_rejected(self):
with tempfile.TemporaryDirectory() as tmp_dir: with tempfile.TemporaryDirectory() as tmp_dir:
root = Path(tmp_dir) root = Path(tmp_dir)
config_body = f""" config_body = f"""
[playbook] [playbook]
project_root = "{tmp_dir}" project_root = "{tmp_dir}"
deploy_root = "{CUSTOM_DEPLOY_ROOT}" deploy_root = "{CUSTOM_DEPLOY_ROOT}"
install_mode = "snapshot"
[vendor] [sync_standards]
langs = ["tsl"]
"""
config_path = write_config(root, "playbook.toml", config_body)
result = run_cli("-config", str(config_path))
self.assertNotEqual(result.returncode, 0)
self.assertIn("deploy_root", result.stdout + result.stderr)
self.assertIn("playbook_root", result.stdout + result.stderr)
def test_snapshot_install_creates_snapshot(self):
with tempfile.TemporaryDirectory() as tmp_dir:
root = Path(tmp_dir)
config_body = f"""
[playbook]
project_root = "{tmp_dir}"
playbook_root = "{CUSTOM_DEPLOY_ROOT}"
install_mode = "snapshot"
[sync_standards]
langs = ["tsl"] langs = ["tsl"]
""" """
config_path = write_config(root, "playbook.toml", config_body) config_path = write_config(root, "playbook.toml", config_body)
@ -176,15 +217,16 @@ langs = ["tsl"]
self.assertEqual(result.returncode, 0) self.assertEqual(result.returncode, 0)
self.assertTrue(snapshot.is_file()) self.assertTrue(snapshot.is_file())
def test_vendor_docs_index_uses_new_tsl_entrypoints(self): def test_snapshot_docs_index_uses_new_tsl_entrypoints(self):
with tempfile.TemporaryDirectory() as tmp_dir: with tempfile.TemporaryDirectory() as tmp_dir:
root = Path(tmp_dir) root = Path(tmp_dir)
config_body = f""" config_body = f"""
[playbook] [playbook]
project_root = "{tmp_dir}" project_root = "{tmp_dir}"
deploy_root = "{CUSTOM_DEPLOY_ROOT}" playbook_root = "{CUSTOM_DEPLOY_ROOT}"
install_mode = "snapshot"
[vendor] [sync_standards]
langs = ["tsl"] langs = ["tsl"]
""" """
config_path = write_config(root, "playbook.toml", config_body) config_path = write_config(root, "playbook.toml", config_body)
@ -201,14 +243,15 @@ langs = ["tsl"]
self.assertIn("`tsl/reference/index.md`", text) self.assertIn("`tsl/reference/index.md`", text)
self.assertNotIn("`tsl/syntax_book/index.md`", text) self.assertNotIn("`tsl/syntax_book/index.md`", text)
def test_external_clone_requires_explicit_deploy_root(self): def test_external_clone_requires_explicit_playbook_root(self):
with tempfile.TemporaryDirectory() as tmp_dir: with tempfile.TemporaryDirectory() as tmp_dir:
root = Path(tmp_dir) root = Path(tmp_dir)
config_body = f""" config_body = f"""
[playbook] [playbook]
project_root = "{tmp_dir}" project_root = "{tmp_dir}"
install_mode = "snapshot"
[vendor] [sync_standards]
langs = ["tsl"] langs = ["tsl"]
""" """
config_path = write_config(root, "playbook.toml", config_body) config_path = write_config(root, "playbook.toml", config_body)
@ -216,14 +259,34 @@ langs = ["tsl"]
result = run_cli("-config", str(config_path)) result = run_cli("-config", str(config_path))
self.assertNotEqual(result.returncode, 0) self.assertNotEqual(result.returncode, 0)
self.assertIn("deploy_root", result.stdout + result.stderr) self.assertIn("playbook_root", result.stdout + result.stderr)
def test_subtree_mode_requires_project_local_script(self):
with tempfile.TemporaryDirectory() as tmp_dir:
root = Path(tmp_dir)
config_body = f"""
[playbook]
project_root = "{tmp_dir}"
playbook_root = "{CUSTOM_DEPLOY_ROOT}"
install_mode = "subtree"
[sync_standards]
langs = ["tsl"]
"""
config_path = write_config(root, "playbook.toml", config_body)
result = run_cli("-config", str(config_path))
self.assertNotEqual(result.returncode, 0)
self.assertIn("project-local Playbook script", result.stdout + result.stderr)
def test_sync_memory_bank_creates_memory_bank(self): def test_sync_memory_bank_creates_memory_bank(self):
with tempfile.TemporaryDirectory() as tmp_dir: with tempfile.TemporaryDirectory() as tmp_dir:
config_body = f""" config_body = f"""
[playbook] [playbook]
project_root = "{tmp_dir}" project_root = "{tmp_dir}"
deploy_root = "{CUSTOM_DEPLOY_ROOT}" playbook_root = "{CUSTOM_DEPLOY_ROOT}"
install_mode = "snapshot"
[sync_memory_bank] [sync_memory_bank]
project_name = "Demo" project_name = "Demo"
@ -242,7 +305,8 @@ project_name = "Demo"
config_body = f""" config_body = f"""
[playbook] [playbook]
project_root = "{tmp_dir}" project_root = "{tmp_dir}"
deploy_root = "{CUSTOM_DEPLOY_ROOT}" playbook_root = "{CUSTOM_DEPLOY_ROOT}"
install_mode = "snapshot"
[sync_standards] [sync_standards]
langs = ["tsl"] langs = ["tsl"]
@ -265,7 +329,8 @@ langs = ["tsl"]
f""" f"""
[playbook] [playbook]
project_root = "{tmp_dir}" project_root = "{tmp_dir}"
deploy_root = "{CUSTOM_DEPLOY_ROOT}" playbook_root = "{CUSTOM_DEPLOY_ROOT}"
install_mode = "snapshot"
[sync_standards] [sync_standards]
langs = ["tsl"] langs = ["tsl"]
@ -282,7 +347,8 @@ no_backup = true
f""" f"""
[playbook] [playbook]
project_root = "{tmp_dir}" project_root = "{tmp_dir}"
deploy_root = "{CUSTOM_DEPLOY_ROOT}" playbook_root = "{CUSTOM_DEPLOY_ROOT}"
install_mode = "snapshot"
[sync_standards] [sync_standards]
langs = ["tsl", "cpp"] langs = ["tsl", "cpp"]
@ -304,7 +370,8 @@ no_backup = true
config_body = f""" config_body = f"""
[playbook] [playbook]
project_root = "{tmp_dir}" project_root = "{tmp_dir}"
deploy_root = "{CUSTOM_DEPLOY_ROOT}" playbook_root = "{CUSTOM_DEPLOY_ROOT}"
install_mode = "snapshot"
[sync_standards] [sync_standards]
langs = ["tsl", "markdown"] langs = ["tsl", "markdown"]
@ -326,7 +393,8 @@ langs = ["tsl", "markdown"]
config_body = f""" config_body = f"""
[playbook] [playbook]
project_root = "{tmp_dir}" project_root = "{tmp_dir}"
deploy_root = "{CUSTOM_DEPLOY_ROOT}" playbook_root = "{CUSTOM_DEPLOY_ROOT}"
install_mode = "snapshot"
[sync_standards] [sync_standards]
langs = ["tsl"] langs = ["tsl"]
@ -352,7 +420,8 @@ langs = ["tsl"]
config_body = f""" config_body = f"""
[playbook] [playbook]
project_root = "{tmp_dir}" project_root = "{tmp_dir}"
deploy_root = "{CUSTOM_DEPLOY_ROOT}" playbook_root = "{CUSTOM_DEPLOY_ROOT}"
install_mode = "snapshot"
[install_skills] [install_skills]
agents_home = "{target}" agents_home = "{target}"
@ -453,7 +522,8 @@ skills = ["brainstorming"]
config_body = f""" config_body = f"""
[playbook] [playbook]
project_root = "{tmp_root}" project_root = "{tmp_root}"
deploy_root = "{CUSTOM_DEPLOY_ROOT}" playbook_root = "{CUSTOM_DEPLOY_ROOT}"
install_mode = "snapshot"
[install_skills] [install_skills]
agents_home = "{target}" agents_home = "{target}"
@ -475,7 +545,8 @@ skills = ["karpathy-guidelines"]
config_body = f""" config_body = f"""
[playbook] [playbook]
project_root = "{tmp_dir}" project_root = "{tmp_dir}"
deploy_root = "{CUSTOM_DEPLOY_ROOT}" playbook_root = "{CUSTOM_DEPLOY_ROOT}"
install_mode = "snapshot"
[install_skills] [install_skills]
agents_home = "{target}" agents_home = "{target}"
@ -496,7 +567,8 @@ skills = ["tsl-guide"]
config_body = f""" config_body = f"""
[playbook] [playbook]
project_root = "{tmp_dir}" project_root = "{tmp_dir}"
deploy_root = "{CUSTOM_DEPLOY_ROOT}" playbook_root = "{CUSTOM_DEPLOY_ROOT}"
install_mode = "snapshot"
[install_skills] [install_skills]
codex_home = "{target}" codex_home = "{target}"
@ -511,17 +583,15 @@ skills = ["brainstorming"]
self.assertNotEqual(result.returncode, 0) self.assertNotEqual(result.returncode, 0)
self.assertIn("codex_home", result.stdout + result.stderr) self.assertIn("codex_home", result.stdout + result.stderr)
def test_external_clone_flow_rewrites_links_with_configured_deploy_root(self): def test_external_clone_flow_rewrites_links_with_configured_playbook_root(self):
with tempfile.TemporaryDirectory() as tmp_dir: with tempfile.TemporaryDirectory() as tmp_dir:
root = Path(tmp_dir) root = Path(tmp_dir)
agents_home = root / "agents-home" agents_home = root / "agents-home"
config_body = f""" config_body = f"""
[playbook] [playbook]
project_root = "{tmp_dir}" project_root = "{tmp_dir}"
deploy_root = "{CUSTOM_DEPLOY_ROOT}" playbook_root = "{CUSTOM_DEPLOY_ROOT}"
install_mode = "snapshot"
[vendor]
langs = ["tsl"]
[sync_standards] [sync_standards]
langs = ["tsl"] langs = ["tsl"]
@ -545,23 +615,28 @@ skills = ["style-cleanup"]
def test_deployed_snapshot_rewrites_links_from_snapshot_location(self): def test_deployed_snapshot_rewrites_links_from_snapshot_location(self):
with tempfile.TemporaryDirectory() as tmp_dir: with tempfile.TemporaryDirectory() as tmp_dir:
root = Path(tmp_dir) root = Path(tmp_dir)
vendor_config = write_config( install_config = write_config(
root, root,
"vendor.toml", "install.toml",
f""" f"""
[playbook] [playbook]
project_root = "{tmp_dir}" project_root = "{tmp_dir}"
deploy_root = "{CUSTOM_DEPLOY_ROOT}" playbook_root = "{CUSTOM_DEPLOY_ROOT}"
install_mode = "snapshot"
[vendor] [sync_standards]
langs = ["tsl"] langs = ["tsl"]
""", """,
) )
vendor_result = run_cli("-config", str(vendor_config)) install_result = run_cli("-config", str(install_config))
self.assertEqual(vendor_result.returncode, 0, msg=vendor_result.stdout + vendor_result.stderr) self.assertEqual(
install_result.returncode,
0,
msg=install_result.stdout + install_result.stderr,
)
vendored_script = root / CUSTOM_DEPLOY_ROOT / "scripts" / "playbook.py" snapshot_script = root / CUSTOM_DEPLOY_ROOT / "scripts" / "playbook.py"
agents_home = root / "local-agents" agents_home = root / "local-agents"
sync_config = write_config( sync_config = write_config(
root, root,
@ -569,7 +644,8 @@ langs = ["tsl"]
f""" f"""
[playbook] [playbook]
project_root = "{tmp_dir}" project_root = "{tmp_dir}"
deploy_root = "{CUSTOM_DEPLOY_ROOT}" playbook_root = "{CUSTOM_DEPLOY_ROOT}"
install_mode = "snapshot"
[sync_standards] [sync_standards]
langs = ["tsl"] langs = ["tsl"]
@ -582,7 +658,7 @@ skills = ["style-cleanup"]
""", """,
) )
sync_result = run_script(vendored_script, "-config", str(sync_config)) sync_result = run_script(snapshot_script, "-config", str(sync_config))
self.assertEqual(sync_result.returncode, 0, msg=sync_result.stdout + sync_result.stderr) self.assertEqual(sync_result.returncode, 0, msg=sync_result.stdout + sync_result.stderr)
self.assert_style_cleanup_tsl_docs_prefix( self.assert_style_cleanup_tsl_docs_prefix(
root, agents_home, f"{CUSTOM_DEPLOY_ROOT}/docs" root, agents_home, f"{CUSTOM_DEPLOY_ROOT}/docs"
@ -593,7 +669,8 @@ skills = ["style-cleanup"]
config_body = f""" config_body = f"""
[playbook] [playbook]
project_root = "{tmp_dir}" project_root = "{tmp_dir}"
deploy_root = "{CUSTOM_DEPLOY_ROOT}" playbook_root = "{CUSTOM_DEPLOY_ROOT}"
install_mode = "snapshot"
[sync_memory_bank] [sync_memory_bank]
project_name = "Demo" project_name = "Demo"
@ -619,7 +696,8 @@ project_name = "Demo"
config_body = f""" config_body = f"""
[playbook] [playbook]
project_root = "{tmp_dir}" project_root = "{tmp_dir}"
deploy_root = "{CUSTOM_DEPLOY_ROOT}" playbook_root = "{CUSTOM_DEPLOY_ROOT}"
install_mode = "snapshot"
[sync_memory_bank] [sync_memory_bank]
project_name = "Demo" project_name = "Demo"
@ -650,7 +728,8 @@ project_name = "Demo"
config_body = f""" config_body = f"""
[playbook] [playbook]
project_root = "{tmp_dir}" project_root = "{tmp_dir}"
deploy_root = "{CUSTOM_DEPLOY_ROOT}" playbook_root = "{CUSTOM_DEPLOY_ROOT}"
install_mode = "snapshot"
[sync_memory_bank] [sync_memory_bank]
project_name = "Demo" project_name = "Demo"
@ -682,7 +761,8 @@ project_name = "Demo"
config_body = f""" config_body = f"""
[playbook] [playbook]
project_root = "{tmp_dir}" project_root = "{tmp_dir}"
deploy_root = "{CUSTOM_DEPLOY_ROOT}" playbook_root = "{CUSTOM_DEPLOY_ROOT}"
install_mode = "snapshot"
[sync_memory_bank] [sync_memory_bank]
project_name = "Demo" project_name = "Demo"
@ -708,7 +788,8 @@ project_name = "Demo"
config_body = f""" config_body = f"""
[playbook] [playbook]
project_root = "{tmp_dir}" project_root = "{tmp_dir}"
deploy_root = "{CUSTOM_DEPLOY_ROOT}" playbook_root = "{CUSTOM_DEPLOY_ROOT}"
install_mode = "snapshot"
[sync_memory_bank] [sync_memory_bank]
project_name = "Demo" project_name = "Demo"
@ -730,7 +811,8 @@ project_name = "Demo"
config_body = f""" config_body = f"""
[playbook] [playbook]
project_root = "{tmp_dir}" project_root = "{tmp_dir}"
deploy_root = "{CUSTOM_DEPLOY_ROOT}" playbook_root = "{CUSTOM_DEPLOY_ROOT}"
install_mode = "snapshot"
[install_skills] [install_skills]
agents_home = "{agents_home}" agents_home = "{agents_home}"
@ -759,7 +841,8 @@ skills = ["commit-message"]
config_body = f""" config_body = f"""
[playbook] [playbook]
project_root = "{tmp_dir}" project_root = "{tmp_dir}"
deploy_root = "{CUSTOM_DEPLOY_ROOT}" playbook_root = "{CUSTOM_DEPLOY_ROOT}"
install_mode = "snapshot"
[install_skills] [install_skills]
agents_home = "{agents_home}" agents_home = "{agents_home}"
@ -784,7 +867,8 @@ no_backup = true
config_body = f""" config_body = f"""
[playbook] [playbook]
project_root = "{tmp_dir}" project_root = "{tmp_dir}"
deploy_root = "{CUSTOM_DEPLOY_ROOT}" playbook_root = "{CUSTOM_DEPLOY_ROOT}"
install_mode = "snapshot"
[install_skills] [install_skills]
agents_home = "{agents_home}" agents_home = "{agents_home}"

View File

@ -85,6 +85,8 @@ class DeploymentRoutesE2ETests(unittest.TestCase):
""" """
[playbook] [playbook]
project_root = "." project_root = "."
playbook_root = "docs/standards/playbook"
install_mode = "subtree"
[sync_rules] [sync_rules]
no_backup = true no_backup = true
@ -119,7 +121,7 @@ no_backup = true
self.assertIn("@AGENT_RULES.md", text) self.assertIn("@AGENT_RULES.md", text)
self.assertIn("<!-- playbook:claude:start -->", text) self.assertIn("<!-- playbook:claude:start -->", text)
def test_external_clone_deployment_vendors_snapshot_and_updates_claude(self): def test_external_clone_deployment_installs_snapshot_and_updates_claude(self):
with tempfile.TemporaryDirectory() as tmp_dir: with tempfile.TemporaryDirectory() as tmp_dir:
tmp_root = Path(tmp_dir) tmp_root = Path(tmp_dir)
external_clone = tmp_root / "playbook" external_clone = tmp_root / "playbook"
@ -137,10 +139,8 @@ no_backup = true
f""" f"""
[playbook] [playbook]
project_root = "." project_root = "."
deploy_root = "{CUSTOM_DEPLOY_ROOT.as_posix()}" playbook_root = "{CUSTOM_DEPLOY_ROOT.as_posix()}"
install_mode = "snapshot"
[vendor]
langs = ["tsl", "markdown"]
[sync_rules] [sync_rules]
no_backup = true no_backup = true

View File

@ -34,7 +34,8 @@ class GitattributesModeTests(unittest.TestCase):
config_body = f""" config_body = f"""
[playbook] [playbook]
project_root = \"{root}\" project_root = \"{root}\"
deploy_root = "{DEFAULT_DEPLOY_ROOT}" playbook_root = "{DEFAULT_DEPLOY_ROOT}"
install_mode = "snapshot"
[sync_standards] [sync_standards]
langs = [\"tsl\"] langs = [\"tsl\"]

View File

@ -27,7 +27,8 @@ class NoBackupFlagsTests(unittest.TestCase):
config_body = f""" config_body = f"""
[playbook] [playbook]
project_root = "{tmp_dir}" project_root = "{tmp_dir}"
deploy_root = "{DEFAULT_DEPLOY_ROOT}" playbook_root = "{DEFAULT_DEPLOY_ROOT}"
install_mode = "snapshot"
[sync_rules] [sync_rules]
force = true force = true
@ -56,7 +57,8 @@ no_backup = true
config_body = f""" config_body = f"""
[playbook] [playbook]
project_root = "{tmp_dir}" project_root = "{tmp_dir}"
deploy_root = "{DEFAULT_DEPLOY_ROOT}" playbook_root = "{DEFAULT_DEPLOY_ROOT}"
install_mode = "snapshot"
[sync_standards] [sync_standards]
langs = ["tsl"] langs = ["tsl"]
@ -85,7 +87,8 @@ no_backup = true
config_body = f""" config_body = f"""
[playbook] [playbook]
project_root = "{tmp_dir}" project_root = "{tmp_dir}"
deploy_root = "{DEFAULT_DEPLOY_ROOT}" playbook_root = "{DEFAULT_DEPLOY_ROOT}"
install_mode = "snapshot"
[install_skills] [install_skills]
agents_home = "{root / 'agents'}" agents_home = "{root / 'agents'}"

View File

@ -29,7 +29,8 @@ class SyncDirectoryActionsTests(unittest.TestCase):
config_body = f""" config_body = f"""
[playbook] [playbook]
project_root = "{tmp_dir}" project_root = "{tmp_dir}"
deploy_root = "{DEFAULT_DEPLOY_ROOT}" playbook_root = "{DEFAULT_DEPLOY_ROOT}"
install_mode = "snapshot"
[sync_memory_bank] [sync_memory_bank]
project_name = "Demo" project_name = "Demo"
@ -54,7 +55,8 @@ project_name = "Demo"
config_body = f""" config_body = f"""
[playbook] [playbook]
project_root = "{tmp_dir}" project_root = "{tmp_dir}"
deploy_root = "{DEFAULT_DEPLOY_ROOT}" playbook_root = "{DEFAULT_DEPLOY_ROOT}"
install_mode = "snapshot"
[sync_prompts] [sync_prompts]
""" """
@ -82,7 +84,8 @@ deploy_root = "{DEFAULT_DEPLOY_ROOT}"
config_body = f""" config_body = f"""
[playbook] [playbook]
project_root = "{tmp_dir}" project_root = "{tmp_dir}"
deploy_root = "{DEFAULT_DEPLOY_ROOT}" playbook_root = "{DEFAULT_DEPLOY_ROOT}"
install_mode = "snapshot"
[sync_memory_bank] [sync_memory_bank]
project_name = "Demo" project_name = "Demo"

View File

@ -222,7 +222,8 @@ class SyncTemplatesPlaceholdersTests(unittest.TestCase):
config_body = f""" config_body = f"""
[playbook] [playbook]
project_root = \"{tmp_dir}\" project_root = \"{tmp_dir}\"
deploy_root = "{DEFAULT_DEPLOY_ROOT}" playbook_root = "{DEFAULT_DEPLOY_ROOT}"
install_mode = "snapshot"
[sync_rules] [sync_rules]
@ -267,31 +268,16 @@ langs = [\"cpp\", \"tsl\"]
self.assertNotIn("{{PLAYBOOK_SCRIPTS}}", rules_text) self.assertNotIn("{{PLAYBOOK_SCRIPTS}}", rules_text)
self.assertFalse(rules_text.endswith("\n\n")) self.assertFalse(rules_text.endswith("\n\n"))
def test_sync_standards_rewrites_typescript_docs_prefix_for_vendored_playbook(self): def test_sync_standards_rewrites_typescript_docs_prefix_for_snapshot_playbook(self):
with tempfile.TemporaryDirectory() as tmp_dir: with tempfile.TemporaryDirectory() as tmp_dir:
root = Path(tmp_dir) root = Path(tmp_dir)
vendor_config = root / "vendor.toml" install_config = root / "install.toml"
vendor_config.write_text( install_config.write_text(
f""" f"""
[playbook] [playbook]
project_root = "{tmp_dir}" project_root = "{tmp_dir}"
deploy_root = "{DEFAULT_DEPLOY_ROOT}" playbook_root = "{DEFAULT_DEPLOY_ROOT}"
install_mode = "snapshot"
[vendor]
langs = ["typescript"]
""",
encoding="utf-8",
)
vendor_result = run_cli("-config", str(vendor_config))
self.assertEqual(vendor_result.returncode, 0, msg=vendor_result.stderr)
sync_config = root / "sync.toml"
sync_config.write_text(
f"""
[playbook]
project_root = "{tmp_dir}"
deploy_root = "{DEFAULT_DEPLOY_ROOT}"
[sync_standards] [sync_standards]
langs = ["typescript"] langs = ["typescript"]
@ -299,11 +285,28 @@ langs = ["typescript"]
encoding="utf-8", encoding="utf-8",
) )
vendored_script = ( install_result = run_cli("-config", str(install_config))
self.assertEqual(install_result.returncode, 0, msg=install_result.stderr)
sync_config = root / "sync.toml"
sync_config.write_text(
f"""
[playbook]
project_root = "{tmp_dir}"
playbook_root = "{DEFAULT_DEPLOY_ROOT}"
install_mode = "snapshot"
[sync_standards]
langs = ["typescript"]
""",
encoding="utf-8",
)
snapshot_script = (
root / "docs" / "standards" / "playbook" / "scripts" / "playbook.py" root / "docs" / "standards" / "playbook" / "scripts" / "playbook.py"
) )
sync_result = run_script( sync_result = run_script(
vendored_script, "-config", str(sync_config), cwd=root snapshot_script, "-config", str(sync_config), cwd=root
) )
self.assertEqual(sync_result.returncode, 0, msg=sync_result.stderr) self.assertEqual(sync_result.returncode, 0, msg=sync_result.stderr)
@ -317,7 +320,8 @@ langs = ["typescript"]
config_body = f""" config_body = f"""
[playbook] [playbook]
project_root = "{tmp_dir}" project_root = "{tmp_dir}"
deploy_root = "{DEFAULT_DEPLOY_ROOT}" playbook_root = "{DEFAULT_DEPLOY_ROOT}"
install_mode = "snapshot"
[sync_rules] [sync_rules]

View File

@ -66,19 +66,20 @@ class TslEntrypointsConsistencyTests(unittest.TestCase):
self.assertIn("方式一git subtree", text) self.assertIn("方式一git subtree", text)
self.assertIn("方式二:外部 clone 后执行部署", text) self.assertIn("方式二:外部 clone 后执行部署", text)
self.assertIn("`project_root`:目标项目根目录", text) self.assertIn("`project_root`:目标项目根目录", text)
self.assertIn("`deploy_root`:相对于 `project_root` 的项目内目标目录", text) self.assertIn("`playbook_root`:相对于 `project_root` 的项目内 Playbook 根目录", text)
self.assertIn("不是外部 clone 出来的 Playbook 仓库路径", text) self.assertIn("不是外部 clone 出来的 Playbook 仓库路径", text)
self.assertIn("外部 clone 场景下必须显式填写 `deploy_root`", text) self.assertIn("外部 clone 场景下必须显式填写 `playbook_root`", text)
self.assertNotIn("方式二:手动复制快照", text) self.assertNotIn("方式二:手动复制快照", text)
self.assertNotIn("方式三CLI 裁剪复制", text) self.assertNotIn("方式三CLI 裁剪复制", text)
self.assertNotIn("如果省略 `deploy_root`,默认仍部署到 `docs/standards/playbook`", text) self.assertNotIn("如果省略 `playbook_root`,默认仍部署到 `docs/standards/playbook`", text)
def test_playbook_example_defines_deploy_root_as_target_path(self): def test_playbook_example_defines_playbook_root_as_target_path(self):
text = PLAYBOOK_EXAMPLE.read_text(encoding="utf-8") text = PLAYBOOK_EXAMPLE.read_text(encoding="utf-8")
self.assertIn('deploy_root = "docs/standards/playbook"', text) self.assertIn('playbook_root = "docs/standards/playbook"', text)
self.assertIn('install_mode = "subtree"', text)
self.assertIn("相对于 project_root", text) self.assertIn("相对于 project_root", text)
self.assertIn("不是外部 clone 的 playbook 路径", text) self.assertIn("不是外部 clone 的 playbook 路径", text)
self.assertIn("从外部 clone 执行时必填", text) self.assertIn("snapshot 表示从外部 clone 安装快照", text)
self.assertNotIn("target_dir", text) self.assertNotIn("target_dir", text)
def test_deployment_docs_do_not_reference_legacy_terms(self): def test_deployment_docs_do_not_reference_legacy_terms(self):

View File

@ -17,15 +17,16 @@ def run_cli(*args):
) )
class VendorSnapshotTemplatesTests(unittest.TestCase): class SnapshotTemplatesTests(unittest.TestCase):
def test_vendor_includes_core_templates(self): def test_snapshot_install_includes_core_templates(self):
with tempfile.TemporaryDirectory() as tmp_dir: with tempfile.TemporaryDirectory() as tmp_dir:
config_body = f""" config_body = f"""
[playbook] [playbook]
project_root = "{tmp_dir}" project_root = "{tmp_dir}"
deploy_root = "{DEFAULT_DEPLOY_ROOT}" playbook_root = "{DEFAULT_DEPLOY_ROOT}"
install_mode = "snapshot"
[vendor] [sync_standards]
langs = ["tsl"] langs = ["tsl"]
""" """
config_path = Path(tmp_dir) / "playbook.toml" config_path = Path(tmp_dir) / "playbook.toml"