✨ feat(playbook): add plan progress tracking and rules updates
This commit is contained in:
parent
6efd637119
commit
278750e3c9
|
|
@ -107,6 +107,12 @@ jobs:
|
|||
cd "$REPO_DIR"
|
||||
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 "========================================"
|
||||
|
|
|
|||
143
AGENTS.md
143
AGENTS.md
|
|
@ -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.md(44 行)
|
||||
2. 触发 $tsl-guide,加载 SKILL.md(192 行)
|
||||
3. 生成代码
|
||||
|
||||
Token 消耗:~6,000 tokens
|
||||
```
|
||||
|
||||
### 场景 2:编写 TSL 类
|
||||
|
||||
```text
|
||||
1. 自动读取 .agents/tsl/index.md(44 行)
|
||||
2. 触发 $tsl-guide,加载 SKILL.md + references/advanced.md
|
||||
3. 生成代码
|
||||
|
||||
Token 消耗:~10,000 tokens
|
||||
```
|
||||
|
||||
### 场景 3:查询 TSL 函数库条目
|
||||
|
||||
```text
|
||||
1. 自动读取 .agents/tsl/index.md(44 行)
|
||||
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?
|
||||
A:TSL 非预训练语言,需要从零教学。
|
||||
|
||||
Q:如果项目有自定义约定怎么办?
|
||||
A:将 playbook 以 git subtree 方式引入项目,并修改项目的 `.agents/`。
|
||||
|
||||
---
|
||||
|
||||
## 相关文档
|
||||
|
||||
- Skills 使用指南:`SKILLS.md`
|
||||
- 开发规范:`docs/index.md`
|
||||
- 项目 README:`README.md`
|
||||
|
|
@ -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
|
||||
|
||||
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.
|
||||
|
|
|
|||
30
README.md
30
README.md
|
|
@ -98,7 +98,7 @@ Layer 1: rulesets/ (≤50 行/语言,模板源)
|
|||
└─ 指向 Skills 和 docs
|
||||
|
||||
Layer 2: codex/skills/ (按需加载,$skill-name 触发)
|
||||
├─ tsl-guide: TSL 渐进式语法教学(962 行)
|
||||
├─ tsl-guide: TSL 渐进式语法教学
|
||||
├─ commit-message: 提交信息规范
|
||||
├─ style-cleanup: 代码风格整理
|
||||
└─ bulk-refactor-workflow: 批量重构流程
|
||||
|
|
@ -107,6 +107,14 @@ Layer 3: docs/ (权威静态文档)
|
|||
└─ 完整语法手册/代码风格/工具链配置
|
||||
```
|
||||
|
||||
**各层职责**:
|
||||
|
||||
| 层级 | 加载方式 | 内容 | 作用 |
|
||||
| ------- | ------------------------------ | ------------------------------ | -------------------------- |
|
||||
| Layer 1 | 自动,始终在上下文 | 硬约束与安全红线 | 快速判断能做/不能做 |
|
||||
| Layer 2 | `$<skill-name>` 触发或代理判定 | 操作指南、最佳实践、工作流 | 指导具体怎么做 |
|
||||
| Layer 3 | 按需读取特定章节 | 完整语言手册、代码风格、工具链 | 最终权威(冲突时以此为准) |
|
||||
|
||||
**目录结构**:
|
||||
|
||||
- `rulesets/index.md`:规则集索引(跨语言)
|
||||
|
|
@ -115,7 +123,25 @@ Layer 3: docs/ (权威静态文档)
|
|||
- `rulesets/python/index.md`:Python 核心约定(45 行)
|
||||
- `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 行(→ 拆分)
|
||||
|
||||
**Skills(Layer 2)创建规则**:
|
||||
|
||||
- 可做:增加新流程、从零教授新语言、添加跨语言通用知识
|
||||
|
||||
## SKILLS(Codex CLI)
|
||||
|
||||
|
|
|
|||
41
SKILLS.md
41
SKILLS.md
|
|
@ -1,8 +1,7 @@
|
|||
# SKILLS
|
||||
|
||||
本文件定义:如何在仓库中落地与维护 **Codex CLI
|
||||
skills**(实验功能),并给出与本 Playbook(`docs/` +
|
||||
`.agents/`)配套的技能编写建议与内置技能清单。
|
||||
本文件定义:如何在仓库中落地与维护 **Codex CLI skills**(实验功能),
|
||||
并给出与本 Playbook(`docs/` + `rulesets/`)配套的技能编写建议与内置技能清单。
|
||||
|
||||
> 提示:Codex 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
|
||||
|
||||
- **`tsl-guide`**:TSL/TSF 语法与编码完整指南
|
||||
- 渐进式教学体系:基础语法 → 高级特性 → 函数库 → 最佳实践
|
||||
- 包含 4 个子文档:primer.md / advanced.md / functions_index.md / common_patterns.md
|
||||
- 总计 962 行,按需加载
|
||||
- 总计约 1000 行,按需加载
|
||||
- 触发词:TSL 语法, 写 TSL, TSL 函数, TSL class, 矩阵操作, TS-SQL 等
|
||||
|
||||
### 通用工作流 Skills
|
||||
|
|
@ -156,17 +156,10 @@ python docs/standards/playbook/scripts/playbook.py -config playbook.toml
|
|||
|
||||
---
|
||||
|
||||
## 9. 运行时排障
|
||||
## 9. Third-party Skills (superpowers)
|
||||
|
||||
- 不触发:
|
||||
- 确认已启用 `[features] skills = true`
|
||||
- 确认 skill 已安装到 `$CODEX_HOME/skills/<name>/SKILL.md`
|
||||
- 重启 `codex`(skills 只在启动时加载)
|
||||
- 触发错:减少不同 skill 的 `description`
|
||||
关键词重叠;让触发词更具体(语言/工具/目录名/流程名)。
|
||||
- 启动报错:通常是 YAML frontmatter 不合法或字段超长;修复后重启即可。
|
||||
|
||||
### Third-party Skills (superpowers)
|
||||
来源:`codex/skills/.sources/superpowers.list`(第三方来源清单)。
|
||||
本节仅列出 superpowers 体系 skills,与本 Playbook 原生 skills 分离。
|
||||
|
||||
<!-- superpowers:skills:start -->
|
||||
|
||||
|
|
@ -185,3 +178,19 @@ python docs/standards/playbook/scripts/playbook.py -config playbook.toml
|
|||
- writing-plans
|
||||
- writing-skills
|
||||
<!-- superpowers:skills:end -->
|
||||
|
||||
---
|
||||
|
||||
## 10. 运行时排障
|
||||
|
||||
- 不触发:
|
||||
- 确认已启用 `[features] skills = true`
|
||||
- 确认 skill 已安装到 `$CODEX_HOME/skills/<name>/SKILL.md`
|
||||
- 重启 `codex`(skills 只在启动时加载)
|
||||
- 触发错:减少不同 skill 的 `description`
|
||||
关键词重叠;让触发词更具体(语言/工具/目录名/流程名)。
|
||||
- 启动报错:通常是 YAML frontmatter 不合法或字段超长;修复后重启即可。
|
||||
|
||||
---
|
||||
|
||||
**最后更新**:2026-01-26
|
||||
|
|
|
|||
|
|
@ -162,6 +162,39 @@ col_0 := matrix[:, 0];
|
|||
2. **按需加载**:只读取一个子文档(避免贪婪加载)
|
||||
3. **必要时检索函数库**:先索引,再定位片段
|
||||
|
||||
### 典型场景与 Token 消耗
|
||||
|
||||
**场景 1:编写简单的 TSL 函数**
|
||||
|
||||
```text
|
||||
1. 自动读取 .agents/tsl/index.md(44 行)
|
||||
2. 触发 $tsl-guide,加载 SKILL.md
|
||||
3. 生成代码
|
||||
|
||||
Token 消耗:~6,000 tokens
|
||||
```
|
||||
|
||||
**场景 2:编写 TSL 类**
|
||||
|
||||
```text
|
||||
1. 自动读取 .agents/tsl/index.md(44 行)
|
||||
2. 触发 $tsl-guide,加载 SKILL.md + references/advanced.md
|
||||
3. 生成代码
|
||||
|
||||
Token 消耗:~10,000 tokens
|
||||
```
|
||||
|
||||
**场景 3:查询 TSL 函数库条目**
|
||||
|
||||
```text
|
||||
1. 自动读取 .agents/tsl/index.md(44 行)
|
||||
2. 触发 $tsl-guide,加载 references/functions_index.md
|
||||
3. 使用 rg 定位函数片段
|
||||
4. 返回答案
|
||||
|
||||
Token 消耗:~8,000 tokens
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 函数库使用规则
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
|
||||
[sync_templates]
|
||||
# project_name = "MyProject" # 可选:替换 {{PROJECT_NAME}}
|
||||
# main_language = "tsl" # 可选:替换 {{MAIN_LANGUAGE}}(未配置时取 sync_standards.langs[0],否则 tsl)
|
||||
# date = "2026-01-23" # 可选:替换 {{DATE}},默认今天
|
||||
# force = false # 可选:覆盖已有目录
|
||||
# no_backup = false # 可选:跳过备份
|
||||
|
|
@ -20,7 +21,7 @@
|
|||
|
||||
[sync_standards]
|
||||
# langs = ["tsl", "cpp"] # 必填:要同步的语言
|
||||
# gitattr_mode = "append" # append|overwrite|block|skip
|
||||
# gitattr_mode = "append" # append(补全缺失)|overwrite(覆盖)|block(插入块)|skip(跳过)
|
||||
|
||||
[install_skills]
|
||||
# mode = "list" # list|all
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
[project]
|
||||
name = "playbook"
|
||||
version = "0.0.0"
|
||||
description = "Playbook templates and tooling"
|
||||
requires-python = ">=3.10"
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
> **重要**:本目录位于 **playbook 仓库**,作为**规则集模板源**。
|
||||
>
|
||||
> - **模板源**:`playbook/rulesets/` → 通过 `sync_standards.*` 同步 → 目标项目的 `.agents/`
|
||||
> - **模板源**:`playbook/rulesets/` → 通过 `scripts/playbook.py` 的 `[sync_standards]` 同步 → 目标项目的 `.agents/`
|
||||
> - **AI 读取**:目标项目根目录的 `.agents/`,而非此处
|
||||
> - **使用流程**:
|
||||
>
|
||||
|
|
@ -20,5 +20,10 @@
|
|||
- `rulesets/python/`:Python 相关规则集
|
||||
- `rulesets/markdown/`:Markdown 相关规则集(仅代码格式化)
|
||||
|
||||
目标项目落地时,通过 `scripts/sync_standards.*`
|
||||
目标项目落地时,通过 `scripts/playbook.py` 的 `[sync_standards]`
|
||||
将规则集从 `rulesets/<lang>/` 同步到目标项目根目录的 `.agents/<lang>/`。
|
||||
|
||||
## 三层架构(分层知识库)
|
||||
|
||||
`rulesets/` 是三层架构中的 **Layer 1**(语言级硬规则,≤50 行/语言)。
|
||||
完整分层说明见 `README.md` 的“rulesets/(规则集模板库 - 三层架构)”。
|
||||
|
|
|
|||
|
|
@ -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:]))
|
||||
|
|
@ -168,6 +168,45 @@ def normalize_langs(raw: object) -> list[str]:
|
|||
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:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
|
|
@ -348,10 +387,20 @@ def vendor_action(config: dict, context: dict) -> int:
|
|||
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)
|
||||
if 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
|
||||
|
||||
|
||||
|
|
@ -370,10 +419,18 @@ def rename_template_files(root: Path) -> None:
|
|||
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"):
|
||||
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:
|
||||
file_path.write_text(updated, encoding="utf-8")
|
||||
|
||||
|
|
@ -401,9 +458,13 @@ def update_agents_section(
|
|||
end_marker: str,
|
||||
project_name: str | None,
|
||||
date_value: str,
|
||||
main_language: str | None,
|
||||
playbook_scripts: str | None,
|
||||
) -> None:
|
||||
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)
|
||||
if not block:
|
||||
log("Skip: markers not found in template")
|
||||
|
|
@ -454,6 +515,8 @@ def sync_templates_action(config: dict, context: dict) -> int:
|
|||
return 2
|
||||
|
||||
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")
|
||||
force = bool(config.get("force", 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)
|
||||
copytree(memory_src, 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/")
|
||||
|
||||
if prompts_src.is_dir():
|
||||
|
|
@ -484,7 +553,13 @@ def sync_templates_action(config: dict, context: dict) -> int:
|
|||
ensure_dir(prompts_dst.parent)
|
||||
copytree(prompts_src, prompts_dst)
|
||||
rename_template_files(prompts_dst)
|
||||
replace_placeholders_in_dir(prompts_dst, project_name, date_value)
|
||||
replace_placeholders_in_dir(
|
||||
prompts_dst,
|
||||
project_name,
|
||||
date_value,
|
||||
main_language,
|
||||
playbook_scripts,
|
||||
)
|
||||
log("Synced: docs/prompts/")
|
||||
|
||||
if agents_src.is_file():
|
||||
|
|
@ -496,7 +571,14 @@ def sync_templates_action(config: dict, context: dict) -> int:
|
|||
start_marker = "<!-- playbook:templates:start -->"
|
||||
end_marker = "<!-- playbook:templates:end -->"
|
||||
update_agents_section(
|
||||
agents_dst, agents_src, start_marker, end_marker, project_name, date_value
|
||||
agents_dst,
|
||||
agents_src,
|
||||
start_marker,
|
||||
end_marker,
|
||||
project_name,
|
||||
date_value,
|
||||
main_language,
|
||||
playbook_scripts,
|
||||
)
|
||||
|
||||
if rules_src.is_file():
|
||||
|
|
@ -506,7 +588,9 @@ def sync_templates_action(config: dict, context: dict) -> int:
|
|||
else:
|
||||
backup_path(rules_dst, no_backup)
|
||||
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")
|
||||
log("Synced: AGENT_RULES.md")
|
||||
|
||||
|
|
@ -912,7 +996,11 @@ def main(argv: list[str]) -> int:
|
|||
root = (config_path.parent / root).resolve()
|
||||
else:
|
||||
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:
|
||||
if name in config:
|
||||
|
|
|
|||
|
|
@ -2,15 +2,6 @@
|
|||
|
||||
<!-- playbook:framework:start -->
|
||||
|
||||
## 规则优先级
|
||||
|
||||
1. 系统/开发者指令与安全约束
|
||||
2. 项目私有规则:`AGENT_RULES.local.md`(如存在)
|
||||
3. 仓库规则:`.agents/` 与本文件
|
||||
4. `AGENT_RULES.md` - 执行流程
|
||||
|
||||
---
|
||||
|
||||
## 快速导航
|
||||
|
||||
<!-- playbook:agents:start -->
|
||||
|
|
@ -25,35 +16,23 @@
|
|||
|
||||
### 核心规则
|
||||
|
||||
- **项目私有规则**:[AGENT_RULES.local.md](./AGENT_RULES.local.md)
|
||||
- **执行流程**:[AGENT_RULES.md](./AGENT_RULES.md)
|
||||
- **AI 行为规范**:[docs/prompts/system/agent-behavior.md](docs/prompts/system/agent-behavior.md)
|
||||
- [AGENT_RULES.md](./AGENT_RULES.md) - 执行流程与优先级
|
||||
|
||||
### 项目上下文
|
||||
|
||||
- **项目定位**:[memory-bank/project-brief.md](memory-bank/project-brief.md)
|
||||
- **技术栈**:[memory-bank/tech-stack.md](memory-bank/tech-stack.md)
|
||||
- **架构设计**:[memory-bank/architecture.md](memory-bank/architecture.md)
|
||||
- **进度追踪**:[memory-bank/progress.md](memory-bank/progress.md)
|
||||
- **架构决策**:[memory-bank/decisions.md](memory-bank/decisions.md)
|
||||
- [memory-bank/project-brief.md](memory-bank/project-brief.md) - 项目定位
|
||||
- [memory-bank/tech-stack.md](memory-bank/tech-stack.md) - 技术栈
|
||||
- [memory-bank/architecture.md](memory-bank/architecture.md) - 架构设计
|
||||
- [memory-bank/progress.md](memory-bank/progress.md) - 进度追踪
|
||||
- [memory-bank/decisions.md](memory-bank/decisions.md) - 架构决策
|
||||
|
||||
### 工作流程
|
||||
|
||||
- **需求澄清**:[docs/prompts/coding/clarify.md](docs/prompts/coding/clarify.md)
|
||||
- **验证检查**:[docs/prompts/coding/verify.md](docs/prompts/coding/verify.md)
|
||||
- [docs/prompts/coding/clarify.md](docs/prompts/coding/clarify.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 -->
|
||||
|
||||
---
|
||||
|
||||
## 新会话开始时
|
||||
|
||||
**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 -->
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -9,28 +9,44 @@
|
|||
3. 仓库规则:`.agents/` 与 `AGENTS.md`
|
||||
4. 本文件
|
||||
|
||||
## 安全红线
|
||||
|
||||
- 不得在代码/日志/注释中写入明文密钥、密码、Token
|
||||
- 修改鉴权/权限逻辑必须说明动机与风险
|
||||
- 不确定是否敏感时按敏感信息处理
|
||||
|
||||
## 上下文加载(每次会话开始)
|
||||
|
||||
**必读文档**(按顺序):
|
||||
|
||||
1. `AGENT_RULES.local.md` - 项目私有规则(如存在,优先级高于本文件)
|
||||
2. `memory-bank/project-brief.md` - 项目定位、边界、约束
|
||||
3. `memory-bank/tech-stack.md` - 技术栈、工具链
|
||||
4. `memory-bank/architecture.md` - 架构设计、模块职责
|
||||
5. `docs/plans/` - 最新实施计划(如存在)
|
||||
2. `.agents/index.md` - 语言规则入口(如存在)
|
||||
3. `memory-bank/project-brief.md` - 项目定位、边界、约束
|
||||
4. `memory-bank/tech-stack.md` - 技术栈、工具链
|
||||
5. `memory-bank/architecture.md` - 架构设计、模块职责
|
||||
6. `memory-bank/decisions.md` - 重要决策记录(如存在)
|
||||
7. `memory-bank/progress.md` - 执行进度与状态(如存在)
|
||||
8. `docs/plans/` - 最新实施计划(如存在)
|
||||
|
||||
**目的**:让 AI 快速理解项目全貌,避免重复解释。
|
||||
|
||||
## 主循环
|
||||
|
||||
1. 选择当前 Plan 文档(优先 `docs/plans/` 最新计划)
|
||||
2. 阅读 Plan 内容与执行顺序
|
||||
3. 执行该 Plan 内所有可执行子任务
|
||||
4. 校验输出结果(运行测试/检查日志)
|
||||
5. **更新 `memory-bank/progress.md`**(记录已完成事项)
|
||||
6. 如存在歧义/风险/决策点,在回复中明确提出,并视需要记录到 `memory-bank/decisions.md`
|
||||
7. 若 Plan 已全部完成,更新 Plan 状态并在 `memory-bank/progress.md` 记录完成
|
||||
8. 若 Plan 因缺少信息而阻塞,在 `memory-bank/progress.md` 标记阻塞原因
|
||||
0. 选择 Plan:
|
||||
- 运行 `python {{PLAYBOOK_SCRIPTS}}/plan_progress.py select -plans docs/plans -progress memory-bank/progress.md`
|
||||
- 如无可执行 Plan,说明情况并询问用户下一步(新增 Plan/切换任务/结束)
|
||||
1. 标记开始:
|
||||
- `python {{PLAYBOOK_SCRIPTS}}/plan_progress.py record -plan <plan> -status in-progress -progress memory-bank/progress.md`
|
||||
2. 阅读 Plan:
|
||||
- 理解目标、子任务与验证标准
|
||||
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 规则
|
||||
|
||||
|
|
@ -39,11 +55,9 @@
|
|||
- `Parent Plan`(上层/集成计划链接)
|
||||
- `Verification Scope`(local 或 integration)
|
||||
- `Verification Gate`(must-pass)
|
||||
- **不允许中断任务**:Plan 中不应包含必然失败或依赖未确认的信息;未确认项必须在 brainstorming 阶段解决后再产出 Plan
|
||||
- **不允许中断任务**:Plan 中不应包含必然失败或依赖未确认的信息;未确认项必须在 `$brainstorming` 阶段解决后再产出 Plan
|
||||
- **验证必须可通过**:Plan 内验证应为当前阶段可通过的局部验证;需要集成验证的内容放入上层/集成 Plan
|
||||
- 不因等待确认而中断可执行步骤;待确认事项在回复中列出
|
||||
- 执行并验证该 Plan 中所有可执行的子任务
|
||||
- 若因缺少信息/决策而阻塞:在 `memory-bank/progress.md` 记录阻塞原因
|
||||
- 每轮只处理一个 Plan
|
||||
- **小步快跑**:每个 Plan 应该可快速完成
|
||||
- **可验证**:每个 Plan 必须包含验证步骤
|
||||
|
|
@ -60,7 +74,7 @@
|
|||
|
||||
- **重要决策**:记录到 `memory-bank/decisions.md`(ADR 格式)
|
||||
- **待确认事项**:在回复中列出并等待确认
|
||||
- **进度留痕**:记录到 `memory-bank/progress.md`(持久化)
|
||||
- **进度留痕**:通过 `{{PLAYBOOK_SCRIPTS}}/plan_progress.py` 写入 `memory-bank/progress.md`,该文件为 Plan 状态唯一权威
|
||||
|
||||
## 需要确认的场景
|
||||
|
||||
|
|
@ -75,11 +89,11 @@
|
|||
|
||||
每个 Plan 完成后,必须验证:
|
||||
|
||||
- [ ] 代码修改符合 `.agents/` 下的规则
|
||||
- [ ] 相关测试通过
|
||||
- [ ] 代码修改符合 `.agents/` 下的规则(如有)
|
||||
- [ ] 相关测试通过(如有测试且未被豁免)
|
||||
- [ ] 换行符正确
|
||||
- [ ] 无语法错误
|
||||
- [ ] 更新了 `memory-bank/progress.md`
|
||||
- [ ] 已更新 `memory-bank/progress.md`
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ full = false
|
|||
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}}` | 日期 | ✅ 是 |
|
||||
| `{{PROJECT_NAME}}` | 项目名称 | ❌ 手动 |
|
||||
| `{{PROJECT_NAME}}` | 项目名称 | ✅ 可选 |
|
||||
| `{{PROJECT_GOAL}}` | 项目目标 | ❌ 手动 |
|
||||
| `{{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/
|
||||
|
|
@ -126,7 +132,9 @@ project/
|
|||
|
||||
执行流程规范,定义 AI 的工作循环和约束。
|
||||
如需项目私有规则,建议创建 `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 配置模板。通过 playbook.py 的 `[sync_templates]` 部署:
|
||||
语言和 CI 配置模板。通过 playbook.py 的 `[vendor]` 复制到快照中:
|
||||
|
||||
| 目录 | 内容 | 部署位置 |
|
||||
| ----------- | ----------------------------------------- | ---------- |
|
||||
| `ci/gitea/` | Gitea Actions 工作流 | `.gitea/` |
|
||||
| `cpp/` | .clang-format, .clangd, CMakeLists.txt 等 | 项目根目录 |
|
||||
| `python/` | pyproject.toml, .editorconfig 等 | 项目根目录 |
|
||||
| 目录 | 内容 | 部署位置 |
|
||||
| ----------- | ----------------------------------------- | ------------------------ |
|
||||
| `ci/gitea/` | Gitea Actions 工作流 | 快照 `templates/ci/` |
|
||||
| `cpp/` | .clang-format, .clangd, CMakeLists.txt 等 | 快照 `templates/cpp/` |
|
||||
| `python/` | pyproject.toml, .editorconfig 等 | 快照 `templates/python/` |
|
||||
|
||||
> 注意:这些模板通过 `[vendor]` 复制到快照的 `templates/` 目录,需手动从快照复制到项目根目录使用。
|
||||
|
||||
**使用方式**:
|
||||
|
||||
```toml
|
||||
# playbook.toml
|
||||
# playbook.toml - 生成包含这些模板的快照
|
||||
[playbook]
|
||||
project_root = "/path/to/project"
|
||||
|
||||
[sync_templates]
|
||||
project_name = "MyProject"
|
||||
[vendor]
|
||||
langs = ["tsl", "cpp", "python"]
|
||||
```
|
||||
|
||||
```bash
|
||||
python docs/standards/playbook/scripts/playbook.py -config playbook.toml
|
||||
python scripts/playbook.py -config playbook.toml
|
||||
# 然后手动从 docs/standards/playbook/templates/ 复制所需配置到项目根目录
|
||||
```
|
||||
|
||||
## 与 playbook 其他部分的关系
|
||||
|
|
@ -202,7 +213,8 @@ playbook/
|
|||
├── docs/ # 权威静态文档
|
||||
├── templates/ # 本目录:项目架构模板 → 部署到 memory-bank/ 等
|
||||
└── 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
|
||||
|
|
|
|||
|
|
@ -6,6 +6,9 @@
|
|||
|
||||
- `gitea/`:Gitea Actions(GitHub Actions 语法)
|
||||
|
||||
说明:`templates/ci/gitea/.gitea/` 结构用于与目标项目根目录的 `.gitea/`
|
||||
保持一致,便于直接复制到项目根目录。
|
||||
|
||||
## 使用(Gitea Actions)
|
||||
|
||||
前提:目标项目已经 vendoring Playbook(例如 `docs/standards/playbook/`)。
|
||||
|
|
|
|||
|
|
@ -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}}
|
||||
|
||||
|
|
@ -34,7 +10,7 @@
|
|||
- **临时方案**:{{WORKAROUND_1}}
|
||||
- **长期方案**:{{SOLUTION_1}}
|
||||
|
||||
### 里程碑
|
||||
## 里程碑
|
||||
|
||||
#### M1: {{MILESTONE_1}}(目标:{{TARGET_DATE_1}})
|
||||
|
||||
|
|
@ -46,14 +22,9 @@
|
|||
- [ ] {{MILESTONE_2_TASK_1}}
|
||||
- [ ] {{MILESTONE_2_TASK_2}}
|
||||
|
||||
---
|
||||
## Plan 状态记录
|
||||
|
||||
## 更新日志
|
||||
|
||||
### {{DATE}}
|
||||
|
||||
- {{LOG_1}}
|
||||
- {{LOG_2}}
|
||||
<!-- 由 plan_progress.py 自动管理,请勿手动编辑此节内容 -->
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,12 @@ tests/
|
|||
├── README.md # 本文件:测试文档
|
||||
├── cli/ # Python CLI 测试(unittest)
|
||||
│ └── 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/ # 模板验证测试
|
||||
│ ├── validate_python_templates.sh # Python 模板验证
|
||||
│ ├── validate_cpp_templates.sh # C++ 模板验证
|
||||
|
|
@ -27,6 +33,9 @@ cd /path/to/playbook
|
|||
# 1. 运行 Python CLI 测试
|
||||
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. 运行模板验证测试
|
||||
sh tests/templates/validate_python_templates.sh
|
||||
sh tests/templates/validate_cpp_templates.sh
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
Loading…
Reference in New Issue