✨ 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"
|
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
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
|
## 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
|
└─ 指向 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 行(→ 拆分)
|
||||||
|
|
||||||
|
**Skills(Layer 2)创建规则**:
|
||||||
|
|
||||||
|
- 可做:增加新流程、从零教授新语言、添加跨语言通用知识
|
||||||
|
|
||||||
## SKILLS(Codex CLI)
|
## SKILLS(Codex CLI)
|
||||||
|
|
||||||
|
|
|
||||||
41
SKILLS.md
41
SKILLS.md
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -162,6 +162,39 @@ col_0 := matrix[:, 0];
|
||||||
2. **按需加载**:只读取一个子文档(避免贪婪加载)
|
2. **按需加载**:只读取一个子文档(避免贪婪加载)
|
||||||
3. **必要时检索函数库**:先索引,再定位片段
|
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]
|
[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
|
||||||
|
|
|
||||||
|
|
@ -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 仓库**,作为**规则集模板源**。
|
||||||
>
|
>
|
||||||
> - **模板源**:`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/(规则集模板库 - 三层架构)”。
|
||||||
|
|
|
||||||
|
|
@ -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
|
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:
|
||||||
|
|
|
||||||
|
|
@ -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 -->
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
|
||||||
|
|
@ -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`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,9 @@
|
||||||
|
|
||||||
- `gitea/`:Gitea Actions(GitHub Actions 语法)
|
- `gitea/`:Gitea Actions(GitHub Actions 语法)
|
||||||
|
|
||||||
|
说明:`templates/ci/gitea/.gitea/` 结构用于与目标项目根目录的 `.gitea/`
|
||||||
|
保持一致,便于直接复制到项目根目录。
|
||||||
|
|
||||||
## 使用(Gitea Actions)
|
## 使用(Gitea Actions)
|
||||||
|
|
||||||
前提:目标项目已经 vendoring Playbook(例如 `docs/standards/playbook/`)。
|
前提:目标项目已经 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}}
|
#### {{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}}
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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