feat(playbook): add plan progress tracking and rules updates

This commit is contained in:
csh 2026-01-26 16:51:23 +08:00
parent 6efd637119
commit 278750e3c9
23 changed files with 919 additions and 275 deletions

View File

@ -107,6 +107,12 @@ jobs:
cd "$REPO_DIR" cd "$REPO_DIR"
python3 -m unittest discover -s tests/cli -v python3 -m unittest discover -s tests/cli -v
echo "========================================"
echo "🧪 Python 扩展测试"
echo "========================================"
python3 -m unittest discover -s tests -p "test_*.py" -v
echo "========================================" echo "========================================"
echo "📄 模板验证测试" echo "📄 模板验证测试"
echo "========================================" echo "========================================"

143
AGENTS.md
View File

@ -1,143 +0,0 @@
# 代理指引playbook
> 关于 playbook 仓库的特殊性:
>
> - 在 playbook 仓库中:规则集模板位于 `rulesets/`
> - 在目标项目中:同步后规则集位于 `.agents/`
> - AI 代理读取目标项目根目录的 `.agents/`(由 playbook.py 的 `[sync_standards]` 生成)
>
> 本文适用于目标项目。playbook 仓库自身没有源代码,不需要 AI 代理规则。
`.agents/` 下规则为准:
- 入口:`.agents/index.md`
- 语言规则:`.agents/tsl/index.md`、`.agents/cpp/index.md`、
`.agents/python/index.md`、`.agents/markdown/index.md`
---
## 三层架构(分层知识库)
本仓库将代理规则与知识分为三层:
### Layer 1: `.agents/`(最小硬规则,每种语言 ≤ 50 行)
- 加载:自动,始终在上下文中
- 内容:硬约束与安全红线
- 作用:快速判断能做/不能做
- 规模控制TSL 44 行 | Python 45 行 | C++ 47 行 | Markdown 31 行
### Layer 2: `codex/skills/`(按需加载,每个 skill 100-1000 行)
- 加载:由 `$<skill-name>` 触发或由代理判定
- 内容:操作指南、最佳实践、工作流
- 作用:指导具体怎么做
关键技能:
- `$tsl-guide` - TSL 渐进式语法训练(基础/高级/函数/最佳实践)
- `$commit-message` - 提交信息规范
- `$style-cleanup` - 格式与风格整理
- `$bulk-refactor-workflow` - 安全批量重构流程
### Layer 3: `docs/`(权威静态文档)
- 加载:按需读取特定章节
- 内容:完整语言手册、代码风格、工具链配置
- 作用:最终权威
- 冲突处理:规则冲突时以 `docs/` 为准
注:函数库拆分在 `docs/tsl/syntax_book/function/` 下。
不要加载整个目录,只加载需要的片段。
---
## 使用场景
### 场景 1编写简单的 TSL 函数
```text
1. 自动读取 .agents/tsl/index.md44 行)
2. 触发 $tsl-guide加载 SKILL.md192 行)
3. 生成代码
Token 消耗:~6,000 tokens
```
### 场景 2编写 TSL 类
```text
1. 自动读取 .agents/tsl/index.md44 行)
2. 触发 $tsl-guide加载 SKILL.md + references/advanced.md
3. 生成代码
Token 消耗:~10,000 tokens
```
### 场景 3查询 TSL 函数库条目
```text
1. 自动读取 .agents/tsl/index.md44 行)
2. 触发 $tsl-guide加载 references/functions_index.md
3. 使用 rg 定位函数片段
4. 返回答案
Token 消耗:~8,000 tokens
```
---
## 性能指标
| 指标 | 之前 | 现在 | 改善 |
| --------------- | ------- | ------- | ---- |
| .agents 规模 | ~500 行 | 168 行 | -66% |
| 持久化 tokens | ~12,500 | ~4,200 | -66% |
| 场景平均 tokens | ~12,500 | ~10,500 | -16% |
---
## 维护原则
### .agents/ 修改规则
可做:
- 增加新的安全漏洞类型
- 更新核心约定(文件名、格式规则)
- 添加不可妥协的硬性约束
不可做:
- 添加推荐型最佳实践(放到 skill
- 添加详细语法解释(放到 skill 或 docs
- 超过 50 行限制(拆分为 skill
### Skills 创建规则
可做:
- 增加新流程(如 code-review
- 从零教授新语言(如 tsl-guide
- 添加跨语言通用知识(如 style-cleanup
---
## FAQ
Q为什么 .agents 这么小?
A因为它会在每次对话加载。控制在 50 行内可减少约 71% 的持久 token 消耗。
Q为什么 TSL 需要专门的 tsl-guide
ATSL 非预训练语言,需要从零教学。
Q如果项目有自定义约定怎么办
A将 playbook 以 git subtree 方式引入项目,并修改项目的 `.agents/`
---
## 相关文档
- Skills 使用指南:`SKILLS.md`
- 开发规范:`docs/index.md`
- 项目 README`README.md`

View File

@ -1,7 +1,34 @@
# Contributing # Contributing
Thanks for improving the Playbook templates and tooling. This repo is a template
source for downstream projects, so changes should stay small, predictable, and
backwards compatible when possible.
## What to change
- Templates: `templates/`, `rulesets/`, `docs/`
- Tooling: `scripts/`
- Tests: `tests/`
## Commit messages ## Commit messages
Follow the repository commit message standard: Follow `docs/common/commit_message.md` and use the required emoji/type mapping.
- `docs/common/commit_message.md` ## Tests
Run the relevant checks before pushing:
```bash
python -m unittest discover -s tests/cli -v
python -m unittest discover -s tests -p "test_*.py" -v
sh tests/templates/validate_python_templates.sh
sh tests/templates/validate_cpp_templates.sh
sh tests/templates/validate_ci_templates.sh
sh tests/templates/validate_project_templates.sh
sh tests/integration/check_doc_links.sh
```
## Templates and docs
- Keep placeholder definitions documented in `templates/README.md`.
- Update template last-updated dates when changing template content.

View File

@ -98,7 +98,7 @@ Layer 1: rulesets/ (≤50 行/语言,模板源)
└─ 指向 Skills 和 docs └─ 指向 Skills 和 docs
Layer 2: codex/skills/ (按需加载,$skill-name 触发) Layer 2: codex/skills/ (按需加载,$skill-name 触发)
├─ tsl-guide: TSL 渐进式语法教学962 行) ├─ tsl-guide: TSL 渐进式语法教学
├─ commit-message: 提交信息规范 ├─ commit-message: 提交信息规范
├─ style-cleanup: 代码风格整理 ├─ style-cleanup: 代码风格整理
└─ bulk-refactor-workflow: 批量重构流程 └─ bulk-refactor-workflow: 批量重构流程
@ -107,6 +107,14 @@ Layer 3: docs/ (权威静态文档)
└─ 完整语法手册/代码风格/工具链配置 └─ 完整语法手册/代码风格/工具链配置
``` ```
**各层职责**
| 层级 | 加载方式 | 内容 | 作用 |
| ------- | ------------------------------ | ------------------------------ | -------------------------- |
| Layer 1 | 自动,始终在上下文 | 硬约束与安全红线 | 快速判断能做/不能做 |
| Layer 2 | `$<skill-name>` 触发或代理判定 | 操作指南、最佳实践、工作流 | 指导具体怎么做 |
| Layer 3 | 按需读取特定章节 | 完整语言手册、代码风格、工具链 | 最终权威(冲突时以此为准) |
**目录结构** **目录结构**
- `rulesets/index.md`:规则集索引(跨语言) - `rulesets/index.md`:规则集索引(跨语言)
@ -115,7 +123,25 @@ Layer 3: docs/ (权威静态文档)
- `rulesets/python/index.md`Python 核心约定45 行) - `rulesets/python/index.md`Python 核心约定45 行)
- `rulesets/markdown/index.md`Markdown 核心约定31 行,仅代码格式化) - `rulesets/markdown/index.md`Markdown 核心约定31 行,仅代码格式化)
详见:`AGENTS.md` 更多说明:`rulesets/index.md`
### 性能指标
| 指标 | 优化前 | 优化后 | 改善 |
| ------------- | ------- | ------ | ---- |
| .agents 规模 | ~500 行 | 167 行 | -67% |
| 持久化 tokens | ~12,500 | ~4,200 | -66% |
### 维护原则
**.agents/Layer 1修改规则**
- 可做:增加安全漏洞类型、更新核心约定、添加硬性约束
- 不可做:添加推荐型最佳实践(→ skill、详细语法解释→ skill/docs、超过 50 行(→ 拆分)
**SkillsLayer 2创建规则**
- 可做:增加新流程、从零教授新语言、添加跨语言通用知识
## SKILLSCodex CLI ## SKILLSCodex CLI

View File

@ -1,8 +1,7 @@
# SKILLS # SKILLS
本文件定义:如何在仓库中落地与维护 **Codex CLI 本文件定义:如何在仓库中落地与维护 **Codex CLI skills**(实验功能),
skills**(实验功能),并给出与本 Playbook`docs/` + 并给出与本 Playbook`docs/` + `rulesets/`)配套的技能编写建议与内置技能清单。
`.agents/`)配套的技能编写建议与内置技能清单。
> 提示Codex skills 是“按用户安装”的(默认在 > 提示Codex skills 是“按用户安装”的(默认在
> `~/.codex/skills`)。本仓库将 skills 以可分发的形式放在 > `~/.codex/skills`)。本仓库将 skills 以可分发的形式放在
@ -136,16 +135,17 @@ python docs/standards/playbook/scripts/playbook.py -config playbook.toml
--- ---
## 8. 本 Playbook 内置 skills ## 8. 本 Playbook 原生 skills
位于 `codex/skills/` 位于 `codex/skills/`Playbook 自维护部分),当前共 4 个。
第三方 superpowers 列表见第 9 节。
### 语言特定 Skills ### 语言特定 Skills
- **`tsl-guide`**TSL/TSF 语法与编码完整指南 - **`tsl-guide`**TSL/TSF 语法与编码完整指南
- 渐进式教学体系:基础语法 → 高级特性 → 函数库 → 最佳实践 - 渐进式教学体系:基础语法 → 高级特性 → 函数库 → 最佳实践
- 包含 4 个子文档primer.md / advanced.md / functions_index.md / common_patterns.md - 包含 4 个子文档primer.md / advanced.md / functions_index.md / common_patterns.md
- 总计 962 行,按需加载 - 总计约 1000 行,按需加载
- 触发词TSL 语法, 写 TSL, TSL 函数, TSL class, 矩阵操作, TS-SQL 等 - 触发词TSL 语法, 写 TSL, TSL 函数, TSL class, 矩阵操作, TS-SQL 等
### 通用工作流 Skills ### 通用工作流 Skills
@ -156,17 +156,10 @@ python docs/standards/playbook/scripts/playbook.py -config playbook.toml
--- ---
## 9. 运行时排障 ## 9. Third-party Skills (superpowers)
- 不触发: 来源:`codex/skills/.sources/superpowers.list`(第三方来源清单)。
- 确认已启用 `[features] skills = true` 本节仅列出 superpowers 体系 skills与本 Playbook 原生 skills 分离。
- 确认 skill 已安装到 `$CODEX_HOME/skills/<name>/SKILL.md`
- 重启 `codex`skills 只在启动时加载)
- 触发错:减少不同 skill 的 `description`
关键词重叠;让触发词更具体(语言/工具/目录名/流程名)。
- 启动报错:通常是 YAML frontmatter 不合法或字段超长;修复后重启即可。
### Third-party Skills (superpowers)
<!-- superpowers:skills:start --> <!-- superpowers:skills:start -->
@ -185,3 +178,19 @@ python docs/standards/playbook/scripts/playbook.py -config playbook.toml
- writing-plans - writing-plans
- writing-skills - writing-skills
<!-- superpowers:skills:end --> <!-- superpowers:skills:end -->
---
## 10. 运行时排障
- 不触发:
- 确认已启用 `[features] skills = true`
- 确认 skill 已安装到 `$CODEX_HOME/skills/<name>/SKILL.md`
- 重启 `codex`skills 只在启动时加载)
- 触发错:减少不同 skill 的 `description`
关键词重叠;让触发词更具体(语言/工具/目录名/流程名)。
- 启动报错:通常是 YAML frontmatter 不合法或字段超长;修复后重启即可。
---
**最后更新**2026-01-26

View File

@ -162,6 +162,39 @@ col_0 := matrix[:, 0];
2. **按需加载**:只读取一个子文档(避免贪婪加载) 2. **按需加载**:只读取一个子文档(避免贪婪加载)
3. **必要时检索函数库**:先索引,再定位片段 3. **必要时检索函数库**:先索引,再定位片段
### 典型场景与 Token 消耗
**场景 1编写简单的 TSL 函数**
```text
1. 自动读取 .agents/tsl/index.md44 行)
2. 触发 $tsl-guide加载 SKILL.md
3. 生成代码
Token 消耗:~6,000 tokens
```
**场景 2编写 TSL 类**
```text
1. 自动读取 .agents/tsl/index.md44 行)
2. 触发 $tsl-guide加载 SKILL.md + references/advanced.md
3. 生成代码
Token 消耗:~10,000 tokens
```
**场景 3查询 TSL 函数库条目**
```text
1. 自动读取 .agents/tsl/index.md44 行)
2. 触发 $tsl-guide加载 references/functions_index.md
3. 使用 rg 定位函数片段
4. 返回答案
Token 消耗:~8,000 tokens
```
--- ---
## ⚠️ 函数库使用规则 ## ⚠️ 函数库使用规则

View File

@ -13,6 +13,7 @@
[sync_templates] [sync_templates]
# project_name = "MyProject" # 可选:替换 {{PROJECT_NAME}} # project_name = "MyProject" # 可选:替换 {{PROJECT_NAME}}
# main_language = "tsl" # 可选:替换 {{MAIN_LANGUAGE}}(未配置时取 sync_standards.langs[0],否则 tsl
# date = "2026-01-23" # 可选:替换 {{DATE}},默认今天 # date = "2026-01-23" # 可选:替换 {{DATE}},默认今天
# force = false # 可选:覆盖已有目录 # force = false # 可选:覆盖已有目录
# no_backup = false # 可选:跳过备份 # no_backup = false # 可选:跳过备份
@ -20,7 +21,7 @@
[sync_standards] [sync_standards]
# langs = ["tsl", "cpp"] # 必填:要同步的语言 # langs = ["tsl", "cpp"] # 必填:要同步的语言
# gitattr_mode = "append" # append|overwrite|block|skip # gitattr_mode = "append" # append(补全缺失)|overwrite(覆盖)|block(插入块)|skip(跳过)
[install_skills] [install_skills]
# mode = "list" # list|all # mode = "list" # list|all

5
pyproject.toml Normal file
View File

@ -0,0 +1,5 @@
[project]
name = "playbook"
version = "0.0.0"
description = "Playbook templates and tooling"
requires-python = ">=3.10"

View File

@ -2,7 +2,7 @@
> **重要**:本目录位于 **playbook 仓库**,作为**规则集模板源**。 > **重要**:本目录位于 **playbook 仓库**,作为**规则集模板源**。
> >
> - **模板源**`playbook/rulesets/` → 通过 `sync_standards.*` 同步 → 目标项目的 `.agents/` > - **模板源**`playbook/rulesets/` → 通过 `scripts/playbook.py` 的 `[sync_standards]` 同步 → 目标项目的 `.agents/`
> - **AI 读取**:目标项目根目录的 `.agents/`,而非此处 > - **AI 读取**:目标项目根目录的 `.agents/`,而非此处
> - **使用流程** > - **使用流程**
> >
@ -20,5 +20,10 @@
- `rulesets/python/`Python 相关规则集 - `rulesets/python/`Python 相关规则集
- `rulesets/markdown/`Markdown 相关规则集(仅代码格式化) - `rulesets/markdown/`Markdown 相关规则集(仅代码格式化)
目标项目落地时,通过 `scripts/sync_standards.*` 目标项目落地时,通过 `scripts/playbook.py` 的 `[sync_standards]`
将规则集从 `rulesets/<lang>/` 同步到目标项目根目录的 `.agents/<lang>/` 将规则集从 `rulesets/<lang>/` 同步到目标项目根目录的 `.agents/<lang>/`
## 三层架构(分层知识库)
`rulesets/` 是三层架构中的 **Layer 1**语言级硬规则≤50 行/语言)。
完整分层说明见 `README.md` 的“rulesets/(规则集模板库 - 三层架构)”。

225
scripts/plan_progress.py Normal file
View File

@ -0,0 +1,225 @@
#!/usr/bin/env python3
import re
import sys
from datetime import datetime
from pathlib import Path
from typing import Optional
PLAN_PREFIX = "[PLAN]"
PLAN_SECTION_HEADER = "## Plan 状态记录"
PLAN_FILE_RE = re.compile(r"^(\d{4}-\d{2}-\d{2})-.+\.md$")
VALID_STATUSES = {"in-progress", "done", "blocked"}
def usage() -> str:
return (
"Usage:\\n"
" python scripts/plan_progress.py select -plans <dir> -progress <file>\\n"
" python scripts/plan_progress.py record -plan <path> -status <status> -progress <file> [-note <text>]\\n"
" python scripts/plan_progress.py -h\\n"
"Options:\\n"
" -plans DIR\\n"
" -plan PATH\\n"
" -status in-progress|done|blocked\\n"
" -progress FILE\\n"
" -note TEXT\\n"
" -h, -help Show this help.\\n"
)
def parse_flags(args: list[str]) -> dict[str, str]:
flags: dict[str, str] = {}
idx = 0
while idx < len(args):
arg = args[idx]
if arg in ("-h", "-help"):
raise ValueError("help")
if not arg.startswith("-"):
raise ValueError(f"unexpected arg: {arg}")
if idx + 1 >= len(args):
raise ValueError(f"missing value for {arg}")
flags[arg] = args[idx + 1]
idx += 2
return flags
def normalize_plan_key(plan_value: str, cwd: Path) -> str:
try:
return Path(plan_value).resolve().relative_to(cwd.resolve()).as_posix()
except ValueError:
return Path(plan_value).as_posix()
def load_plan_records(progress_path: Path, cwd: Path) -> dict[str, str]:
if not progress_path.exists():
return {}
text = progress_path.read_text(encoding="utf-8")
records: dict[str, str] = {}
for line in text.splitlines():
if not line.startswith(PLAN_PREFIX):
continue
payload = line[len(PLAN_PREFIX) :].strip()
if not payload:
continue
segments = [seg.strip() for seg in payload.split("|")]
if not segments:
continue
plan_path = segments[0]
status = None
for seg in segments[1:]:
if "=" not in seg:
continue
key, value = seg.split("=", 1)
if key.strip() == "status":
status = value.strip()
if not plan_path or status is None:
continue
records[normalize_plan_key(plan_path, cwd)] = status
return records
def list_plan_files(plans_dir: Path, cwd: Path) -> list[tuple[str, Path, str]]:
entries: list[tuple[str, Path, str]] = []
for path in plans_dir.iterdir():
if not path.is_file():
continue
match = PLAN_FILE_RE.match(path.name)
if not match:
continue
date_value = match.group(1)
try:
rel = path.resolve().relative_to(cwd.resolve()).as_posix()
except ValueError:
rel = path.as_posix()
entries.append((date_value, path, rel))
return entries
def select_plan(plans_dir: Path, progress_path: Path) -> tuple[int, str]:
cwd = Path.cwd()
if not plans_dir.is_dir():
return 2, f"ERROR: plans dir not found: {plans_dir}"
plans = list_plan_files(plans_dir, cwd)
if not plans:
return 2, "ERROR: no plan files found"
records = load_plan_records(progress_path, cwd)
in_progress = [item for item in plans if records.get(item[2]) == "in-progress"]
if in_progress:
in_progress.sort(key=lambda item: (item[0], item[2]))
return 0, in_progress[-1][2]
pending = [
item
for item in plans
if records.get(item[2]) not in ("done", "blocked")
]
if not pending:
return 2, "ERROR: no pending plans"
pending.sort(key=lambda item: (item[0], item[2]))
return 0, pending[-1][2]
def ensure_plan_section(text: str) -> str:
if PLAN_SECTION_HEADER in text:
return text
suffix = text
if suffix and not suffix.endswith("\n"):
suffix += "\n"
if suffix:
suffix += "\n"
suffix += PLAN_SECTION_HEADER + "\n"
return suffix
def normalize_note(note: str) -> str:
cleaned = note.replace("\n", " ").replace("|", " ").strip()
return cleaned
def record_status(plan: str, status: str, progress_path: Path, note: Optional[str]) -> tuple[int, str]:
if status not in VALID_STATUSES:
return 2, f"ERROR: invalid status: {status}"
if not plan:
return 2, "ERROR: plan is required"
progress_path.parent.mkdir(parents=True, exist_ok=True)
if progress_path.exists():
text = progress_path.read_text(encoding="utf-8")
else:
text = "# 开发进度追踪\n"
text = ensure_plan_section(text)
if not text.endswith("\n"):
text += "\n"
date_value = datetime.now().strftime("%Y-%m-%d")
plan_path = Path(plan).as_posix()
line = f"{PLAN_PREFIX} {plan_path} | status={status} | date={date_value}"
if note:
cleaned = normalize_note(note)
if cleaned:
line += f" | note={cleaned}"
text += line + "\n"
progress_path.write_text(text, encoding="utf-8")
return 0, line
def main(argv: list[str]) -> int:
if not argv:
print(usage(), file=sys.stderr)
return 2
if argv[0] in ("-h", "-help"):
print(usage())
return 0
mode = argv[0]
if mode not in ("select", "record"):
print(f"ERROR: unknown mode: {mode}", file=sys.stderr)
print(usage(), file=sys.stderr)
return 2
try:
flags = parse_flags(argv[1:])
except ValueError as exc:
if str(exc) == "help":
print(usage())
return 0
print(f"ERROR: {exc}", file=sys.stderr)
print(usage(), file=sys.stderr)
return 2
if mode == "select":
plans = flags.get("-plans")
progress = flags.get("-progress")
if not plans or not progress:
print("ERROR: -plans and -progress are required", file=sys.stderr)
print(usage(), file=sys.stderr)
return 2
code, message = select_plan(Path(plans), Path(progress))
if code != 0:
print(message, file=sys.stderr)
return code
print(message)
return 0
plan = flags.get("-plan")
status = flags.get("-status")
progress = flags.get("-progress")
note = flags.get("-note")
if not plan or not status or not progress:
print("ERROR: -plan, -status, and -progress are required", file=sys.stderr)
print(usage(), file=sys.stderr)
return 2
code, message = record_status(plan, status, Path(progress), note)
if code != 0:
print(message, file=sys.stderr)
return code
print(message)
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))

View File

@ -168,6 +168,45 @@ def normalize_langs(raw: object) -> list[str]:
return cleaned return cleaned
def resolve_main_language(config: dict, context: dict) -> str:
raw = config.get("main_language")
if raw is not None and str(raw).strip():
return str(raw).strip()
full_config = context.get("config", {})
if isinstance(full_config, dict):
sync_conf = full_config.get("sync_standards")
if isinstance(sync_conf, dict):
langs_raw = sync_conf.get("langs")
if langs_raw is not None:
try:
langs = normalize_langs(langs_raw)
except ValueError:
langs = []
if langs:
return langs[0]
return "tsl"
def resolve_playbook_scripts(project_root: Path, context: dict) -> str:
playbook_scripts = PLAYBOOK_ROOT / "scripts"
try:
rel = playbook_scripts.resolve().relative_to(project_root.resolve())
return rel.as_posix()
except ValueError:
full_config = context.get("config", {})
if isinstance(full_config, dict):
vendor_conf = full_config.get("vendor")
if isinstance(vendor_conf, dict):
target_dir = vendor_conf.get("target_dir")
if target_dir:
target_str = str(target_dir).strip().rstrip("/").rstrip("\\")
if target_str:
return f"{target_str}/scripts"
return "docs/standards/playbook/scripts"
def read_git_commit(root: Path) -> str: def read_git_commit(root: Path) -> str:
try: try:
result = subprocess.run( result = subprocess.run(
@ -348,10 +387,20 @@ def vendor_action(config: dict, context: dict) -> int:
return 0 return 0
def replace_placeholders(text: str, project_name: str | None, date_value: str) -> str: def replace_placeholders(
text: str,
project_name: str | None,
date_value: str,
main_language: str | None,
playbook_scripts: str | None,
) -> str:
result = text.replace("{{DATE}}", date_value) result = text.replace("{{DATE}}", date_value)
if project_name: if project_name:
result = result.replace("{{PROJECT_NAME}}", project_name) result = result.replace("{{PROJECT_NAME}}", project_name)
if main_language:
result = result.replace("{{MAIN_LANGUAGE}}", main_language)
if playbook_scripts:
result = result.replace("{{PLAYBOOK_SCRIPTS}}", playbook_scripts)
return result return result
@ -370,10 +419,18 @@ def rename_template_files(root: Path) -> None:
template.rename(target) template.rename(target)
def replace_placeholders_in_dir(root: Path, project_name: str | None, date_value: str) -> None: def replace_placeholders_in_dir(
root: Path,
project_name: str | None,
date_value: str,
main_language: str | None,
playbook_scripts: str | None,
) -> None:
for file_path in root.rglob("*.md"): for file_path in root.rglob("*.md"):
text = file_path.read_text(encoding="utf-8") text = file_path.read_text(encoding="utf-8")
updated = replace_placeholders(text, project_name, date_value) updated = replace_placeholders(
text, project_name, date_value, main_language, playbook_scripts
)
if updated != text: if updated != text:
file_path.write_text(updated, encoding="utf-8") file_path.write_text(updated, encoding="utf-8")
@ -401,9 +458,13 @@ def update_agents_section(
end_marker: str, end_marker: str,
project_name: str | None, project_name: str | None,
date_value: str, date_value: str,
main_language: str | None,
playbook_scripts: str | None,
) -> None: ) -> None:
template_text = template_path.read_text(encoding="utf-8") template_text = template_path.read_text(encoding="utf-8")
template_text = replace_placeholders(template_text, project_name, date_value) template_text = replace_placeholders(
template_text, project_name, date_value, main_language, playbook_scripts
)
block = extract_block_lines(template_text, start_marker, end_marker) block = extract_block_lines(template_text, start_marker, end_marker)
if not block: if not block:
log("Skip: markers not found in template") log("Skip: markers not found in template")
@ -454,6 +515,8 @@ def sync_templates_action(config: dict, context: dict) -> int:
return 2 return 2
project_name = config.get("project_name") project_name = config.get("project_name")
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") date_value = config.get("date") or datetime.now().strftime("%Y-%m-%d")
force = bool(config.get("force", False)) force = bool(config.get("force", False))
no_backup = bool(config.get("no_backup", False)) no_backup = bool(config.get("no_backup", False))
@ -472,7 +535,13 @@ def sync_templates_action(config: dict, context: dict) -> int:
backup_path(memory_dst, no_backup) backup_path(memory_dst, no_backup)
copytree(memory_src, memory_dst) copytree(memory_src, memory_dst)
rename_template_files(memory_dst) rename_template_files(memory_dst)
replace_placeholders_in_dir(memory_dst, project_name, date_value) replace_placeholders_in_dir(
memory_dst,
project_name,
date_value,
main_language,
playbook_scripts,
)
log("Synced: memory-bank/") log("Synced: memory-bank/")
if prompts_src.is_dir(): if prompts_src.is_dir():
@ -484,7 +553,13 @@ def sync_templates_action(config: dict, context: dict) -> int:
ensure_dir(prompts_dst.parent) ensure_dir(prompts_dst.parent)
copytree(prompts_src, prompts_dst) copytree(prompts_src, prompts_dst)
rename_template_files(prompts_dst) rename_template_files(prompts_dst)
replace_placeholders_in_dir(prompts_dst, project_name, date_value) replace_placeholders_in_dir(
prompts_dst,
project_name,
date_value,
main_language,
playbook_scripts,
)
log("Synced: docs/prompts/") log("Synced: docs/prompts/")
if agents_src.is_file(): if agents_src.is_file():
@ -496,7 +571,14 @@ def sync_templates_action(config: dict, context: dict) -> int:
start_marker = "<!-- playbook:templates:start -->" start_marker = "<!-- playbook:templates:start -->"
end_marker = "<!-- playbook:templates:end -->" end_marker = "<!-- playbook:templates:end -->"
update_agents_section( update_agents_section(
agents_dst, agents_src, start_marker, end_marker, project_name, date_value agents_dst,
agents_src,
start_marker,
end_marker,
project_name,
date_value,
main_language,
playbook_scripts,
) )
if rules_src.is_file(): if rules_src.is_file():
@ -506,7 +588,9 @@ def sync_templates_action(config: dict, context: dict) -> int:
else: else:
backup_path(rules_dst, no_backup) backup_path(rules_dst, no_backup)
text = rules_src.read_text(encoding="utf-8") text = rules_src.read_text(encoding="utf-8")
text = replace_placeholders(text, project_name, date_value) text = replace_placeholders(
text, project_name, date_value, main_language, playbook_scripts
)
rules_dst.write_text(text + "\n", encoding="utf-8") rules_dst.write_text(text + "\n", encoding="utf-8")
log("Synced: AGENT_RULES.md") log("Synced: AGENT_RULES.md")
@ -912,7 +996,11 @@ def main(argv: list[str]) -> int:
root = (config_path.parent / root).resolve() root = (config_path.parent / root).resolve()
else: else:
root = config_path.parent root = config_path.parent
context = {"project_root": root.resolve(), "config_path": config_path.resolve()} context = {
"project_root": root.resolve(),
"config_path": config_path.resolve(),
"config": config,
}
for name in ORDER: for name in ORDER:
if name in config: if name in config:

View File

@ -2,15 +2,6 @@
<!-- playbook:framework:start --> <!-- playbook:framework:start -->
## 规则优先级
1. 系统/开发者指令与安全约束
2. 项目私有规则:`AGENT_RULES.local.md`(如存在)
3. 仓库规则:`.agents/` 与本文件
4. `AGENT_RULES.md` - 执行流程
---
## 快速导航 ## 快速导航
<!-- playbook:agents:start --> <!-- playbook:agents:start -->
@ -25,35 +16,23 @@
### 核心规则 ### 核心规则
- **项目私有规则**[AGENT_RULES.local.md](./AGENT_RULES.local.md) - [AGENT_RULES.md](./AGENT_RULES.md) - 执行流程与优先级
- **执行流程**[AGENT_RULES.md](./AGENT_RULES.md)
- **AI 行为规范**[docs/prompts/system/agent-behavior.md](docs/prompts/system/agent-behavior.md)
### 项目上下文 ### 项目上下文
- **项目定位**[memory-bank/project-brief.md](memory-bank/project-brief.md) - [memory-bank/project-brief.md](memory-bank/project-brief.md) - 项目定位
- **技术栈**[memory-bank/tech-stack.md](memory-bank/tech-stack.md) - [memory-bank/tech-stack.md](memory-bank/tech-stack.md) - 技术栈
- **架构设计**[memory-bank/architecture.md](memory-bank/architecture.md) - [memory-bank/architecture.md](memory-bank/architecture.md) - 架构设计
- **进度追踪**[memory-bank/progress.md](memory-bank/progress.md) - [memory-bank/progress.md](memory-bank/progress.md) - 进度追踪
- **架构决策**[memory-bank/decisions.md](memory-bank/decisions.md) - [memory-bank/decisions.md](memory-bank/decisions.md) - 架构决策
### 工作流程 ### 工作流程
- **需求澄清**[docs/prompts/coding/clarify.md](docs/prompts/coding/clarify.md) - [docs/prompts/coding/clarify.md](docs/prompts/coding/clarify.md) - 需求澄清
- **验证检查**[docs/prompts/coding/verify.md](docs/prompts/coding/verify.md) - [docs/prompts/coding/verify.md](docs/prompts/coding/verify.md) - 验证检查
- [docs/prompts/system/agent-behavior.md](docs/prompts/system/agent-behavior.md) - AI 行为规范
<!-- playbook:templates:end --> <!-- playbook:templates:end -->
---
## 新会话开始时
**AI 应该做的**
1. 读取 [AGENT_RULES.local.md](./AGENT_RULES.local.md)(如存在)
2. 读取 [AGENT_RULES.md](./AGENT_RULES.md)
3. 读取 [memory-bank/](memory-bank/) 核心文档
4. 读取 [docs/prompts/system/agent-behavior.md](docs/prompts/system/agent-behavior.md)
5. 查看 `docs/plans/` 下最新计划(如有)
<!-- playbook:framework:end --> <!-- playbook:framework:end -->
--- ---

View File

@ -9,28 +9,44 @@
3. 仓库规则:`.agents/` 与 `AGENTS.md` 3. 仓库规则:`.agents/` 与 `AGENTS.md`
4. 本文件 4. 本文件
## 安全红线
- 不得在代码/日志/注释中写入明文密钥、密码、Token
- 修改鉴权/权限逻辑必须说明动机与风险
- 不确定是否敏感时按敏感信息处理
## 上下文加载(每次会话开始) ## 上下文加载(每次会话开始)
**必读文档**(按顺序): **必读文档**(按顺序):
1. `AGENT_RULES.local.md` - 项目私有规则(如存在,优先级高于本文件) 1. `AGENT_RULES.local.md` - 项目私有规则(如存在,优先级高于本文件)
2. `memory-bank/project-brief.md` - 项目定位、边界、约束 2. `.agents/index.md` - 语言规则入口(如存在)
3. `memory-bank/tech-stack.md` - 技术栈、工具链 3. `memory-bank/project-brief.md` - 项目定位、边界、约束
4. `memory-bank/architecture.md` - 架构设计、模块职责 4. `memory-bank/tech-stack.md` - 技术栈、工具链
5. `docs/plans/` - 最新实施计划(如存在) 5. `memory-bank/architecture.md` - 架构设计、模块职责
6. `memory-bank/decisions.md` - 重要决策记录(如存在)
7. `memory-bank/progress.md` - 执行进度与状态(如存在)
8. `docs/plans/` - 最新实施计划(如存在)
**目的**:让 AI 快速理解项目全貌,避免重复解释。 **目的**:让 AI 快速理解项目全貌,避免重复解释。
## 主循环 ## 主循环
1. 选择当前 Plan 文档(优先 `docs/plans/` 最新计划) 0. 选择 Plan
2. 阅读 Plan 内容与执行顺序 - 运行 `python {{PLAYBOOK_SCRIPTS}}/plan_progress.py select -plans docs/plans -progress memory-bank/progress.md`
3. 执行该 Plan 内所有可执行子任务 - 如无可执行 Plan说明情况并询问用户下一步新增 Plan/切换任务/结束)
4. 校验输出结果(运行测试/检查日志) 1. 标记开始:
5. **更新 `memory-bank/progress.md`**(记录已完成事项) - `python {{PLAYBOOK_SCRIPTS}}/plan_progress.py record -plan <plan> -status in-progress -progress memory-bank/progress.md`
6. 如存在歧义/风险/决策点,在回复中明确提出,并视需要记录到 `memory-bank/decisions.md` 2. 阅读 Plan
7. 若 Plan 已全部完成,更新 Plan 状态并在 `memory-bank/progress.md` 记录完成 - 理解目标、子任务与验证标准
8. 若 Plan 因缺少信息而阻塞,在 `memory-bank/progress.md` 标记阻塞原因 3. 逐步执行:
- 按顺序执行子任务
- 每步完成后进行必要验证(测试/日志/diff
- 遇到阻塞立即记录并停止
4. 记录结果(写入 `memory-bank/progress.md`
- 完成:`python {{PLAYBOOK_SCRIPTS}}/plan_progress.py record -plan <plan> -status done -progress memory-bank/progress.md`
- 阻塞:`python {{PLAYBOOK_SCRIPTS}}/plan_progress.py record -plan <plan> -status blocked -progress memory-bank/progress.md -note <原因>`
5. 如存在歧义/风险/决策点,在回复中明确提出,并视需要记录到 `memory-bank/decisions.md`
## Plan 规则 ## Plan 规则
@ -39,11 +55,9 @@
- `Parent Plan`(上层/集成计划链接) - `Parent Plan`(上层/集成计划链接)
- `Verification Scope`local 或 integration - `Verification Scope`local 或 integration
- `Verification Gate`must-pass - `Verification Gate`must-pass
- **不允许中断任务**Plan 中不应包含必然失败或依赖未确认的信息;未确认项必须在 brainstorming 阶段解决后再产出 Plan - **不允许中断任务**Plan 中不应包含必然失败或依赖未确认的信息;未确认项必须在 `$brainstorming` 阶段解决后再产出 Plan
- **验证必须可通过**Plan 内验证应为当前阶段可通过的局部验证;需要集成验证的内容放入上层/集成 Plan - **验证必须可通过**Plan 内验证应为当前阶段可通过的局部验证;需要集成验证的内容放入上层/集成 Plan
- 不因等待确认而中断可执行步骤;待确认事项在回复中列出 - 不因等待确认而中断可执行步骤;待确认事项在回复中列出
- 执行并验证该 Plan 中所有可执行的子任务
- 若因缺少信息/决策而阻塞:在 `memory-bank/progress.md` 记录阻塞原因
- 每轮只处理一个 Plan - 每轮只处理一个 Plan
- **小步快跑**:每个 Plan 应该可快速完成 - **小步快跑**:每个 Plan 应该可快速完成
- **可验证**:每个 Plan 必须包含验证步骤 - **可验证**:每个 Plan 必须包含验证步骤
@ -60,7 +74,7 @@
- **重要决策**:记录到 `memory-bank/decisions.md`ADR 格式) - **重要决策**:记录到 `memory-bank/decisions.md`ADR 格式)
- **待确认事项**:在回复中列出并等待确认 - **待确认事项**:在回复中列出并等待确认
- **进度留痕**记录到 `memory-bank/progress.md`(持久化) - **进度留痕**通过 `{{PLAYBOOK_SCRIPTS}}/plan_progress.py` 写入 `memory-bank/progress.md`,该文件为 Plan 状态唯一权威
## 需要确认的场景 ## 需要确认的场景
@ -75,11 +89,11 @@
每个 Plan 完成后,必须验证: 每个 Plan 完成后,必须验证:
- [ ] 代码修改符合 `.agents/` 下的规则 - [ ] 代码修改符合 `.agents/` 下的规则(如有)
- [ ] 相关测试通过 - [ ] 相关测试通过(如有测试且未被豁免)
- [ ] 换行符正确 - [ ] 换行符正确
- [ ] 无语法错误 - [ ] 无语法错误
- [ ] 更新 `memory-bank/progress.md` - [ ] 更新 `memory-bank/progress.md`
--- ---

View File

@ -53,7 +53,7 @@ full = false
python docs/standards/playbook/scripts/playbook.py -config playbook.toml python docs/standards/playbook/scripts/playbook.py -config playbook.toml
``` ```
参数说明见 `docs/standards/playbook/playbook.toml.example`。 参数说明见 `playbook.toml.example`(仓库根目录)或 vendoring 后的 `docs/standards/playbook/playbook.toml.example`。
### 部署行为 ### 部署行为
@ -92,12 +92,18 @@ project/
| 占位符 | 说明 | 自动替换 | | 占位符 | 说明 | 自动替换 |
| ------------------------- | ------------ | -------- | | ------------------------- | ------------ | -------- |
| `{{DATE}}` | 日期 | ✅ 是 | | `{{DATE}}` | 日期 | ✅ 是 |
| `{{PROJECT_NAME}}` | 项目名称 | ❌ 手动 | | `{{PROJECT_NAME}}` | 项目名称 | ✅ 可选 |
| `{{PROJECT_GOAL}}` | 项目目标 | ❌ 手动 | | `{{PROJECT_GOAL}}` | 项目目标 | ❌ 手动 |
| `{{PROJECT_DESCRIPTION}}` | 项目描述 | ❌ 手动 | | `{{PROJECT_DESCRIPTION}}` | 项目描述 | ❌ 手动 |
| `{{MAIN_LANGUAGE}}` | 主语言 | ❌ 手动 | | `{{MAIN_LANGUAGE}}` | 主语言 | ✅ 可选 |
| `{{PLAYBOOK_SCRIPTS}}` | 脚本路径 | ✅ 是 |
| 其他 `{{...}}` | 项目特定内容 | ❌ 手动 | | 其他 `{{...}}` | 项目特定内容 | ❌ 手动 |
`{{PROJECT_NAME}}` 可通过 `sync_templates.project_name` 自动替换;未配置时保持原样。
`{{MAIN_LANGUAGE}}` 可通过 `sync_templates.main_language``sync_standards.langs[0]` 自动替换;
未配置时默认 `tsl`
`{{PLAYBOOK_SCRIPTS}}` 自动替换为 Playbook 脚本路径(默认 `docs/standards/playbook/scripts`)。
## 模板说明 ## 模板说明
### memory-bank/ ### memory-bank/
@ -126,7 +132,9 @@ project/
执行流程规范,定义 AI 的工作循环和约束。 执行流程规范,定义 AI 的工作循环和约束。
如需项目私有规则,建议创建 `AGENT_RULES.local.md`,其优先级高于 `AGENT_RULES.md` 如需项目私有规则,建议创建 `AGENT_RULES.local.md`,其优先级高于 `AGENT_RULES.md`
且不会被同步脚本覆盖。 且不会被 `playbook.py` 覆盖。
主循环会根据 `memory-bank/progress.md` 的 Plan 状态与 `docs/plans/` 文件名日期,
自动选择最新未完成的 Plan并要求通过 `scripts/plan_progress.py` 写入进度。
### 示例:不跑测试的计划提示词 ### 示例:不跑测试的计划提示词
@ -170,27 +178,30 @@ project/
### ci/、cpp/、python/ ### ci/、cpp/、python/
语言和 CI 配置模板。通过 playbook.py 的 `[sync_templates]` 部署 语言和 CI 配置模板。通过 playbook.py 的 `[vendor]` 复制到快照中
| 目录 | 内容 | 部署位置 | | 目录 | 内容 | 部署位置 |
| ----------- | ----------------------------------------- | ---------- | | ----------- | ----------------------------------------- | ------------------------ |
| `ci/gitea/` | Gitea Actions 工作流 | `.gitea/` | | `ci/gitea/` | Gitea Actions 工作流 | 快照 `templates/ci/` |
| `cpp/` | .clang-format, .clangd, CMakeLists.txt 等 | 项目根目录 | | `cpp/` | .clang-format, .clangd, CMakeLists.txt 等 | 快照 `templates/cpp/` |
| `python/` | pyproject.toml, .editorconfig 等 | 项目根目录 | | `python/` | pyproject.toml, .editorconfig 等 | 快照 `templates/python/` |
> 注意:这些模板通过 `[vendor]` 复制到快照的 `templates/` 目录,需手动从快照复制到项目根目录使用。
**使用方式** **使用方式**
```toml ```toml
# playbook.toml # playbook.toml - 生成包含这些模板的快照
[playbook] [playbook]
project_root = "/path/to/project" project_root = "/path/to/project"
[sync_templates] [vendor]
project_name = "MyProject" langs = ["tsl", "cpp", "python"]
``` ```
```bash ```bash
python docs/standards/playbook/scripts/playbook.py -config playbook.toml python scripts/playbook.py -config playbook.toml
# 然后手动从 docs/standards/playbook/templates/ 复制所需配置到项目根目录
``` ```
## 与 playbook 其他部分的关系 ## 与 playbook 其他部分的关系
@ -202,7 +213,8 @@ playbook/
├── docs/ # 权威静态文档 ├── docs/ # 权威静态文档
├── templates/ # 本目录:项目架构模板 → 部署到 memory-bank/ 等 ├── templates/ # 本目录:项目架构模板 → 部署到 memory-bank/ 等
└── scripts/ └── scripts/
└── playbook.py # 统一入口vendor/sync_templates/sync_standards/... ├── playbook.py # 统一入口vendor/sync_templates/sync_standards/...
└── plan_progress.py # Plan 选择与进度记录
``` ```
## 完整部署流程 ## 完整部署流程
@ -218,4 +230,4 @@ python docs/standards/playbook/scripts/playbook.py -config playbook.toml
--- ---
**最后更新**2026-01-21 **最后更新**2026-01-26

View File

@ -6,6 +6,9 @@
- `gitea/`Gitea ActionsGitHub Actions 语法) - `gitea/`Gitea ActionsGitHub Actions 语法)
说明:`templates/ci/gitea/.gitea/` 结构用于与目标项目根目录的 `.gitea/`
保持一致,便于直接复制到项目根目录。
## 使用Gitea Actions ## 使用Gitea Actions
前提:目标项目已经 vendoring Playbook例如 `docs/standards/playbook/`)。 前提:目标项目已经 vendoring Playbook例如 `docs/standards/playbook/`)。

View File

@ -1,32 +1,8 @@
# 开发进度追踪 # 开发进度追踪
## 当前阶段:{{CURRENT_PHASE}} ## 已知问题
### 最近完成 <!-- 记录已知但暂不解决的问题 -->
#### {{DATE}}
- [x] {{COMPLETED_1}}
- [x] {{COMPLETED_2}}
### 进行中
- [ ] {{IN_PROGRESS_1}}
- [ ] {{IN_PROGRESS_2}}
### 待办
#### {{CATEGORY_1}}
- [ ] {{TODO_1}}
- [ ] {{TODO_2}}
#### {{CATEGORY_2}}
- [ ] {{TODO_3}}
- [ ] {{TODO_4}}
### 已知问题
#### {{ISSUE_CATEGORY_1}} #### {{ISSUE_CATEGORY_1}}
@ -34,7 +10,7 @@
- **临时方案**{{WORKAROUND_1}} - **临时方案**{{WORKAROUND_1}}
- **长期方案**{{SOLUTION_1}} - **长期方案**{{SOLUTION_1}}
### 里程碑 ## 里程碑
#### M1: {{MILESTONE_1}}(目标:{{TARGET_DATE_1}} #### M1: {{MILESTONE_1}}(目标:{{TARGET_DATE_1}}
@ -46,14 +22,9 @@
- [ ] {{MILESTONE_2_TASK_1}} - [ ] {{MILESTONE_2_TASK_1}}
- [ ] {{MILESTONE_2_TASK_2}} - [ ] {{MILESTONE_2_TASK_2}}
--- ## Plan 状态记录
## 更新日志 <!-- 由 plan_progress.py 自动管理,请勿手动编辑此节内容 -->
### {{DATE}}
- {{LOG_1}}
- {{LOG_2}}
--- ---

View File

@ -9,6 +9,12 @@ tests/
├── README.md # 本文件:测试文档 ├── README.md # 本文件:测试文档
├── cli/ # Python CLI 测试unittest ├── cli/ # Python CLI 测试unittest
│ └── test_playbook_cli.py # playbook.py 基础功能测试 │ └── test_playbook_cli.py # playbook.py 基础功能测试
├── test_format_md_action.py # format_md 动作测试
├── 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_toml_edge_cases.py # TOML 解析边界测试
├── templates/ # 模板验证测试 ├── templates/ # 模板验证测试
│ ├── validate_python_templates.sh # Python 模板验证 │ ├── validate_python_templates.sh # Python 模板验证
│ ├── validate_cpp_templates.sh # C++ 模板验证 │ ├── validate_cpp_templates.sh # C++ 模板验证
@ -27,6 +33,9 @@ cd /path/to/playbook
# 1. 运行 Python CLI 测试 # 1. 运行 Python CLI 测试
python -m unittest discover -s tests/cli -v python -m unittest discover -s tests/cli -v
# 1.1 运行其他 Python 测试tests/ 下的 test_*.py
python -m unittest discover -s tests -p "test_*.py" -v
# 2. 运行模板验证测试 # 2. 运行模板验证测试
sh tests/templates/validate_python_templates.sh sh tests/templates/validate_python_templates.sh
sh tests/templates/validate_cpp_templates.sh sh tests/templates/validate_cpp_templates.sh

View File

@ -0,0 +1,58 @@
import os
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
SCRIPT = ROOT / "scripts" / "playbook.py"
def run_cli(*args, env=None):
return subprocess.run(
[sys.executable, str(SCRIPT), *args],
capture_output=True,
text=True,
env=env,
)
class FormatMdActionTests(unittest.TestCase):
def test_format_md_invokes_prettier_from_path(self):
with tempfile.TemporaryDirectory() as tmp_dir:
root = Path(tmp_dir)
(root / "README.md").write_text("# Title\n", encoding="utf-8")
bin_dir = root / "bin"
bin_dir.mkdir()
prettier = bin_dir / "prettier"
prettier.write_text(
"#!/usr/bin/env python3\n"
"from pathlib import Path\n"
"Path(\".prettier_called\").write_text(\"ok\")\n",
encoding="utf-8",
)
prettier.chmod(0o755)
config_body = f"""
[playbook]
project_root = \"{tmp_dir}\"
[format_md]
# tool defaults to prettier
# keep default globs
"""
config_path = root / "playbook.toml"
config_path.write_text(config_body, encoding="utf-8")
env = os.environ.copy()
env["PATH"] = f"{bin_dir}:{env.get('PATH', '')}"
result = run_cli("-config", str(config_path), env=env)
self.assertEqual(result.returncode, 0, msg=result.stderr)
self.assertTrue((root / ".prettier_called").exists())
if __name__ == "__main__":
unittest.main()

View File

@ -0,0 +1,94 @@
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
SCRIPT = ROOT / "scripts" / "playbook.py"
SOURCE_GITATTR = ROOT / ".gitattributes"
def run_cli(*args, cwd=None):
return subprocess.run(
[sys.executable, str(SCRIPT), *args],
capture_output=True,
text=True,
cwd=cwd,
)
def read_entries(path: Path) -> list[str]:
entries = []
for line in path.read_text(encoding="utf-8").splitlines():
stripped = line.strip()
if not stripped or stripped.startswith("#"):
continue
entries.append(stripped)
return entries
class GitattributesModeTests(unittest.TestCase):
def _run_sync(self, root: Path, mode: str) -> subprocess.CompletedProcess:
config_body = f"""
[playbook]
project_root = \"{root}\"
[sync_standards]
langs = [\"tsl\"]
gitattr_mode = \"{mode}\"
"""
config_path = root / "playbook.toml"
config_path.write_text(config_body, encoding="utf-8")
return run_cli("-config", str(config_path), cwd=root)
def test_gitattr_mode_skip(self):
with tempfile.TemporaryDirectory() as tmp_dir:
root = Path(tmp_dir)
sentinel = "*.keep text eol=lf\n"
(root / ".gitattributes").write_text(sentinel, encoding="utf-8")
result = self._run_sync(root, "skip")
self.assertEqual(result.returncode, 0)
self.assertEqual(
(root / ".gitattributes").read_text(encoding="utf-8"),
sentinel,
)
def test_gitattr_mode_overwrite(self):
with tempfile.TemporaryDirectory() as tmp_dir:
root = Path(tmp_dir)
(root / ".gitattributes").write_text("bad\n", encoding="utf-8")
result = self._run_sync(root, "overwrite")
self.assertEqual(result.returncode, 0)
self.assertEqual(
(root / ".gitattributes").read_text(encoding="utf-8"),
SOURCE_GITATTR.read_text(encoding="utf-8"),
)
def test_gitattr_mode_block(self):
with tempfile.TemporaryDirectory() as tmp_dir:
root = Path(tmp_dir)
result = self._run_sync(root, "block")
self.assertEqual(result.returncode, 0)
content = (root / ".gitattributes").read_text(encoding="utf-8")
self.assertIn("# BEGIN playbook .gitattributes", content)
self.assertIn("# END playbook .gitattributes", content)
def test_gitattr_mode_append(self):
with tempfile.TemporaryDirectory() as tmp_dir:
root = Path(tmp_dir)
src_entries = read_entries(SOURCE_GITATTR)
(root / ".gitattributes").write_text(
src_entries[0] + "\n", encoding="utf-8"
)
result = self._run_sync(root, "append")
self.assertEqual(result.returncode, 0)
content = (root / ".gitattributes").read_text(encoding="utf-8")
self.assertIn("Added from playbook .gitattributes", content)
if __name__ == "__main__":
unittest.main()

View File

@ -0,0 +1,106 @@
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
SCRIPT = ROOT / "scripts" / "plan_progress.py"
def run_cli(*args, cwd=None):
return subprocess.run(
[sys.executable, str(SCRIPT), *args],
capture_output=True,
text=True,
cwd=cwd,
)
class PlanProgressCliTests(unittest.TestCase):
def test_select_prefers_in_progress(self):
with tempfile.TemporaryDirectory() as tmp_dir:
root = Path(tmp_dir)
plans_dir = root / "docs" / "plans"
plans_dir.mkdir(parents=True)
(plans_dir / "2026-01-01-old.md").write_text("old", encoding="utf-8")
(plans_dir / "2026-01-02-new.md").write_text("new", encoding="utf-8")
progress = root / "memory-bank" / "progress.md"
progress.parent.mkdir(parents=True)
progress.write_text(
"[PLAN] docs/plans/2026-01-01-old.md | status=in-progress | date=2026-01-03\n",
encoding="utf-8",
)
result = run_cli(
"select",
"-plans",
"docs/plans",
"-progress",
"memory-bank/progress.md",
cwd=root,
)
self.assertEqual(result.returncode, 0)
self.assertEqual(result.stdout.strip(), "docs/plans/2026-01-01-old.md")
def test_select_skips_done_and_blocked(self):
with tempfile.TemporaryDirectory() as tmp_dir:
root = Path(tmp_dir)
plans_dir = root / "docs" / "plans"
plans_dir.mkdir(parents=True)
(plans_dir / "2026-01-01-a.md").write_text("a", encoding="utf-8")
(plans_dir / "2026-01-02-b.md").write_text("b", encoding="utf-8")
progress = root / "memory-bank" / "progress.md"
progress.parent.mkdir(parents=True)
progress.write_text(
"\n".join(
[
"[PLAN] docs/plans/2026-01-02-b.md | status=done | date=2026-01-03",
"[PLAN] docs/plans/2026-01-01-a.md | status=blocked | date=2026-01-03",
"",
]
),
encoding="utf-8",
)
result = run_cli(
"select",
"-plans",
"docs/plans",
"-progress",
"memory-bank/progress.md",
cwd=root,
)
self.assertNotEqual(result.returncode, 0)
self.assertIn("no pending plans", (result.stdout + result.stderr).lower())
def test_record_creates_section(self):
with tempfile.TemporaryDirectory() as tmp_dir:
root = Path(tmp_dir)
progress = root / "memory-bank" / "progress.md"
result = run_cli(
"record",
"-plan",
"docs/plans/2026-01-03-demo.md",
"-status",
"done",
"-progress",
"memory-bank/progress.md",
"-note",
"done",
cwd=root,
)
self.assertEqual(result.returncode, 0)
text = progress.read_text(encoding="utf-8")
self.assertIn("## Plan 状态记录", text)
self.assertIn("status=done", text)
if __name__ == "__main__":
unittest.main()

View File

@ -0,0 +1,42 @@
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
SKILLS_MD = ROOT / "SKILLS.md"
SOURCES_LIST = ROOT / "codex" / "skills" / ".sources" / "superpowers.list"
def read_sources_list() -> list[str]:
return [
line.strip()
for line in SOURCES_LIST.read_text(encoding="utf-8").splitlines()
if line.strip() and not line.strip().startswith("#")
]
def read_skills_md_list() -> list[str]:
lines = SKILLS_MD.read_text(encoding="utf-8").splitlines()
start = "<!-- superpowers:skills:start -->"
end = "<!-- superpowers:skills:end -->"
try:
start_idx = lines.index(start) + 1
end_idx = lines.index(end)
except ValueError as exc:
raise AssertionError("superpowers markers missing in SKILLS.md") from exc
items = []
for line in lines[start_idx:end_idx]:
stripped = line.strip()
if not stripped.startswith("-"):
continue
items.append(stripped.lstrip("- ").strip())
return items
class SuperpowersListSyncTests(unittest.TestCase):
def test_superpowers_list_matches_skills_md(self):
self.assertEqual(read_sources_list(), read_skills_md_list())
if __name__ == "__main__":
unittest.main()

View File

@ -0,0 +1,51 @@
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
SCRIPT = ROOT / "scripts" / "playbook.py"
def run_cli(*args):
return subprocess.run(
[sys.executable, str(SCRIPT), *args],
capture_output=True,
text=True,
)
class SyncTemplatesPlaceholdersTests(unittest.TestCase):
def test_main_language_placeholder_replaced(self):
with tempfile.TemporaryDirectory() as tmp_dir:
config_body = f"""
[playbook]
project_root = \"{tmp_dir}\"
[sync_templates]
project_name = \"Demo\"
full = true
[sync_standards]
langs = [\"cpp\", \"tsl\"]
"""
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, msg=result.stderr)
agents_md = Path(tmp_dir) / "AGENTS.md"
text = agents_md.read_text(encoding="utf-8")
self.assertIn(".agents/cpp/index.md", text)
self.assertNotIn("{{MAIN_LANGUAGE}}", text)
rules_md = Path(tmp_dir) / "AGENT_RULES.md"
rules_text = rules_md.read_text(encoding="utf-8")
self.assertIn("docs/standards/playbook/scripts/plan_progress.py", rules_text)
self.assertNotIn("{{PLAYBOOK_SCRIPTS}}", rules_text)
if __name__ == "__main__":
unittest.main()

View File

@ -0,0 +1,23 @@
import unittest
from scripts import playbook
class TomlEdgeCaseTests(unittest.TestCase):
def test_minimal_parser_allows_dotted_section_name(self):
raw = """
[a.b]
key = 1
"""
data = playbook.loads_toml_minimal(raw)
self.assertIn("a.b", data)
self.assertEqual(data["a.b"]["key"], 1)
def test_minimal_parser_rejects_multiline_string(self):
raw = '[section]\nvalue = """line1\nline2"""\n'
with self.assertRaises(ValueError):
playbook.loads_toml_minimal(raw)
if __name__ == "__main__":
unittest.main()